Compare commits
2 Commits
4fa7ed2a48
...
911143dfc5
| Author | SHA1 | Date | |
|---|---|---|---|
| 911143dfc5 | |||
| 29fd68f732 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
# Ignore the virtual environment folder
|
# Ignore the virtual environment folder
|
||||||
track/
|
track/
|
||||||
|
resurces/projects/
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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.
|
|
||||||
|
|||||||
@@ -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.
|
|
||||||
|
|||||||
0
junk_files/reqirements.txt
Normal file
0
junk_files/reqirements.txt
Normal file
0
junk_files/test_enhanced_video.py
Normal file
0
junk_files/test_enhanced_video.py
Normal file
83
junk_files/test_google_earth.py
Normal file
83
junk_files/test_google_earth.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for Google Earth-style flythrough animation
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
sys.path.append('/home/pi/Desktop/traccar_animation')
|
||||||
|
|
||||||
|
from py_scripts.advanced_3d_generator import Advanced3DGenerator
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
def test_google_earth_animation():
|
||||||
|
"""Test the new Google Earth flythrough animation"""
|
||||||
|
|
||||||
|
# Find a project with GPS data
|
||||||
|
projects_folder = "/home/pi/Desktop/traccar_animation/resources/projects"
|
||||||
|
|
||||||
|
if not os.path.exists(projects_folder):
|
||||||
|
print("Projects folder not found!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Look for projects
|
||||||
|
projects = [d for d in os.listdir(projects_folder) if os.path.isdir(os.path.join(projects_folder, d))]
|
||||||
|
|
||||||
|
if not projects:
|
||||||
|
print("No projects found!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Use the first project found
|
||||||
|
project_name = projects[0]
|
||||||
|
project_folder = os.path.join(projects_folder, project_name)
|
||||||
|
positions_file = os.path.join(project_folder, "positions.json")
|
||||||
|
|
||||||
|
if not os.path.exists(positions_file):
|
||||||
|
print(f"No positions.json found in project {project_name}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Testing Google Earth animation with project: {project_name}")
|
||||||
|
|
||||||
|
# Create generator
|
||||||
|
generator = Advanced3DGenerator(project_folder)
|
||||||
|
|
||||||
|
# Check dependencies
|
||||||
|
try:
|
||||||
|
generator.check_dependencies()
|
||||||
|
print("✅ All dependencies available")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Dependency error: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Generate Google Earth-style animation
|
||||||
|
output_video = os.path.join(project_folder, f"{project_name}_google_earth_test_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
|
||||||
|
|
||||||
|
def progress_callback(progress, message):
|
||||||
|
print(f"Progress: {progress:.1f}% - {message}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
print("Starting Google Earth flythrough generation...")
|
||||||
|
success = generator.generate_3d_animation(
|
||||||
|
positions_file,
|
||||||
|
output_video,
|
||||||
|
style='google_earth',
|
||||||
|
progress_callback=progress_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
if success and os.path.exists(output_video):
|
||||||
|
print(f"✅ SUCCESS! Google Earth flythrough created: {output_video}")
|
||||||
|
|
||||||
|
# Get file size
|
||||||
|
file_size = os.path.getsize(output_video) / (1024 * 1024) # MB
|
||||||
|
print(f"📹 Video size: {file_size:.1f} MB")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("❌ Failed to create video")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error during generation: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_google_earth_animation()
|
||||||
0
junk_files/test_space_entry_fix.py
Normal file
0
junk_files/test_space_entry_fix.py
Normal file
0
junk_files/test_transition.py
Normal file
0
junk_files/test_transition.py
Normal file
0
junk_files/test_video_generator.py
Normal file
0
junk_files/test_video_generator.py
Normal file
0
junk_files/test_video_modes.py
Normal file
0
junk_files/test_video_modes.py
Normal file
0
junk_files/video_3d_generator.py
Normal file
0
junk_files/video_3d_generator.py
Normal file
Binary file not shown.
@@ -34,44 +34,45 @@ except ImportError:
|
|||||||
print("Warning: pydeck not available. Install with: pip install pydeck")
|
print("Warning: pydeck not available. Install with: pip install pydeck")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from moviepy import VideoFileClip, ImageSequenceClip
|
import cv2
|
||||||
MOVIEPY_AVAILABLE = True
|
OPENCV_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
MOVIEPY_AVAILABLE = False
|
OPENCV_AVAILABLE = False
|
||||||
print("Warning: moviepy not available. Install with: pip install moviepy")
|
print("Warning: opencv-python not available. Install with: pip install opencv-python")
|
||||||
|
|
||||||
class Advanced3DGenerator:
|
class NavigationAnimationGenerator:
|
||||||
"""
|
"""
|
||||||
Advanced 3D animation generator using Pydeck + Plotly + Blender pipeline
|
Professional navigation animation generator with satellite view and 3D camera following
|
||||||
for high-quality GPS track visualizations
|
Creates Google Earth-style entry scene and detailed terrain navigation
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, output_folder):
|
def __init__(self, output_folder):
|
||||||
self.output_folder = output_folder
|
self.output_folder = output_folder
|
||||||
self.frames_folder = os.path.join(output_folder, "frames")
|
self.frames_folder = os.path.join(output_folder, "nav_frames")
|
||||||
self.temp_folder = os.path.join(output_folder, "temp")
|
self.temp_folder = os.path.join(output_folder, "temp")
|
||||||
|
|
||||||
# Create necessary folders
|
# Create necessary folders
|
||||||
os.makedirs(self.frames_folder, exist_ok=True)
|
os.makedirs(self.frames_folder, exist_ok=True)
|
||||||
os.makedirs(self.temp_folder, exist_ok=True)
|
os.makedirs(self.temp_folder, exist_ok=True)
|
||||||
|
|
||||||
# Animation settings
|
# Navigation animation settings
|
||||||
self.fps = 30
|
self.fps = 30
|
||||||
self.duration_per_point = 0.5 # seconds per GPS point
|
self.entry_duration = 4 # seconds for Google Earth entry
|
||||||
self.camera_height = 1000 # meters
|
self.camera_height_min = 1000 # meters
|
||||||
self.trail_length = 50 # number of previous points to show
|
self.camera_height_max = 2000 # meters
|
||||||
|
self.follow_distance = 500 # meters behind navigation point
|
||||||
|
|
||||||
def check_dependencies(self):
|
def check_dependencies(self):
|
||||||
"""Check if all required dependencies are available"""
|
"""Check if all required dependencies are available"""
|
||||||
missing = []
|
missing = []
|
||||||
if not PANDAS_AVAILABLE:
|
if not PANDAS_AVAILABLE:
|
||||||
missing.append("pandas, geopandas")
|
missing.append("pandas geopandas")
|
||||||
if not PLOTLY_AVAILABLE:
|
if not PLOTLY_AVAILABLE:
|
||||||
missing.append("plotly")
|
missing.append("plotly")
|
||||||
if not PYDECK_AVAILABLE:
|
if not PYDECK_AVAILABLE:
|
||||||
missing.append("pydeck")
|
missing.append("pydeck")
|
||||||
if not MOVIEPY_AVAILABLE:
|
if not OPENCV_AVAILABLE:
|
||||||
missing.append("moviepy")
|
missing.append("opencv-python")
|
||||||
|
|
||||||
if missing:
|
if missing:
|
||||||
raise ImportError(f"Missing required dependencies: {', '.join(missing)}. Please install them with: pip install {' '.join(missing)}")
|
raise ImportError(f"Missing required dependencies: {', '.join(missing)}. Please install them with: pip install {' '.join(missing)}")
|
||||||
@@ -79,7 +80,7 @@ class Advanced3DGenerator:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def load_gps_data(self, positions_file):
|
def load_gps_data(self, positions_file):
|
||||||
"""Load and preprocess GPS data"""
|
"""Load and preprocess GPS data for navigation"""
|
||||||
with open(positions_file, 'r') as f:
|
with open(positions_file, 'r') as f:
|
||||||
positions = json.load(f)
|
positions = json.load(f)
|
||||||
|
|
||||||
@@ -92,11 +93,360 @@ class Advanced3DGenerator:
|
|||||||
|
|
||||||
# Calculate speed and bearing
|
# Calculate speed and bearing
|
||||||
df['speed_kmh'] = df['speed'] * 1.852 # Convert knots to km/h
|
df['speed_kmh'] = df['speed'] * 1.852 # Convert knots to km/h
|
||||||
df['elevation'] = df.get('altitude', 0)
|
df['elevation'] = df.get('altitude', 100)
|
||||||
|
|
||||||
# Calculate distance between points
|
# Calculate bearings and distances
|
||||||
distances = []
|
|
||||||
bearings = []
|
bearings = []
|
||||||
|
distances = []
|
||||||
|
|
||||||
|
for i in range(len(df)):
|
||||||
|
if i == 0:
|
||||||
|
bearings.append(0)
|
||||||
|
distances.append(0)
|
||||||
|
else:
|
||||||
|
# Calculate bearing to next point
|
||||||
|
lat1, lon1 = df.iloc[i-1]['latitude'], df.iloc[i-1]['longitude']
|
||||||
|
lat2, lon2 = df.iloc[i]['latitude'], df.iloc[i]['longitude']
|
||||||
|
|
||||||
|
bearing = self.calculate_bearing(lat1, lon1, lat2, lon2)
|
||||||
|
distance = geodesic((lat1, lon1), (lat2, lon2)).meters
|
||||||
|
|
||||||
|
bearings.append(bearing)
|
||||||
|
distances.append(distance)
|
||||||
|
|
||||||
|
df['bearing'] = bearings
|
||||||
|
df['distance'] = distances
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
def calculate_bearing(self, lat1, lon1, lat2, lon2):
|
||||||
|
"""Calculate bearing between two GPS points"""
|
||||||
|
lat1, lon1, lat2, lon2 = map(math.radians, [lat1, lon1, lat2, lon2])
|
||||||
|
dlon = lon2 - lon1
|
||||||
|
|
||||||
|
y = math.sin(dlon) * math.cos(lat2)
|
||||||
|
x = math.cos(lat1) * math.sin(lat2) - math.sin(lat1) * math.cos(lat2) * math.cos(dlon)
|
||||||
|
|
||||||
|
bearing = math.atan2(y, x)
|
||||||
|
bearing = math.degrees(bearing)
|
||||||
|
bearing = (bearing + 360) % 360
|
||||||
|
|
||||||
|
return bearing
|
||||||
|
|
||||||
|
def create_google_earth_entry_scene(self, df, frame_num):
|
||||||
|
"""Create Google Earth-style entry scene (zooming in from space)"""
|
||||||
|
if not PLOTLY_AVAILABLE:
|
||||||
|
raise ImportError("Plotly is required for entry scene")
|
||||||
|
|
||||||
|
# Get route bounds
|
||||||
|
center_lat = df['latitude'].mean()
|
||||||
|
center_lon = df['longitude'].mean()
|
||||||
|
|
||||||
|
# Entry animation: start from very high altitude and zoom in
|
||||||
|
total_entry_frames = self.entry_duration * self.fps
|
||||||
|
zoom_progress = frame_num / total_entry_frames
|
||||||
|
|
||||||
|
# Camera altitude decreases exponentially
|
||||||
|
start_altitude = 50000 # Start from 50km
|
||||||
|
end_altitude = 3000 # End at 3km
|
||||||
|
current_altitude = start_altitude * (1 - zoom_progress) + end_altitude * zoom_progress
|
||||||
|
|
||||||
|
# Create figure
|
||||||
|
fig = go.Figure()
|
||||||
|
|
||||||
|
# Add Earth-like surface with satellite imagery simulation
|
||||||
|
terrain_size = 0.5 - (0.4 * zoom_progress) # Zoom in effect
|
||||||
|
resolution = int(30 + (50 * zoom_progress)) # More detail as we zoom
|
||||||
|
|
||||||
|
lat_range = np.linspace(center_lat - terrain_size, center_lat + terrain_size, resolution)
|
||||||
|
lon_range = np.linspace(center_lon - terrain_size, center_lon + terrain_size, resolution)
|
||||||
|
lat_mesh, lon_mesh = np.meshgrid(lat_range, lon_range)
|
||||||
|
|
||||||
|
# Generate satellite-like terrain
|
||||||
|
terrain_heights = self.generate_satellite_terrain(lat_mesh, lon_mesh, center_lat, center_lon)
|
||||||
|
|
||||||
|
# Add terrain surface
|
||||||
|
fig.add_trace(
|
||||||
|
go.Surface(
|
||||||
|
x=lon_mesh,
|
||||||
|
y=lat_mesh,
|
||||||
|
z=terrain_heights,
|
||||||
|
colorscale=[
|
||||||
|
[0.0, 'rgb(0,100,0)'], # Deep green (forests)
|
||||||
|
[0.2, 'rgb(34,139,34)'], # Forest green
|
||||||
|
[0.4, 'rgb(255,215,0)'], # Gold (fields)
|
||||||
|
[0.6, 'rgb(139,69,19)'], # Brown (earth)
|
||||||
|
[0.8, 'rgb(105,105,105)'], # Gray (rock)
|
||||||
|
[1.0, 'rgb(255,255,255)'] # White (snow)
|
||||||
|
],
|
||||||
|
opacity=0.95,
|
||||||
|
showscale=False,
|
||||||
|
lighting=dict(
|
||||||
|
ambient=0.4,
|
||||||
|
diffuse=0.8,
|
||||||
|
specular=0.2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Show partial route (fading in)
|
||||||
|
route_alpha = min(1.0, zoom_progress * 2)
|
||||||
|
if route_alpha > 0:
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter3d(
|
||||||
|
x=df['longitude'],
|
||||||
|
y=df['latitude'],
|
||||||
|
z=df['elevation'] + 100,
|
||||||
|
mode='lines',
|
||||||
|
line=dict(
|
||||||
|
color='red',
|
||||||
|
width=8,
|
||||||
|
),
|
||||||
|
opacity=route_alpha,
|
||||||
|
name='Route'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Camera position for entry effect
|
||||||
|
camera_distance = current_altitude / 10000
|
||||||
|
|
||||||
|
fig.update_layout(
|
||||||
|
title=dict(
|
||||||
|
text=f'Navigation Overview - Approaching Destination',
|
||||||
|
x=0.5,
|
||||||
|
font=dict(size=28, color='white', family="Arial Black")
|
||||||
|
),
|
||||||
|
scene=dict(
|
||||||
|
camera=dict(
|
||||||
|
eye=dict(x=0, y=-camera_distance, z=camera_distance),
|
||||||
|
center=dict(x=0, y=0, z=0),
|
||||||
|
up=dict(x=0, y=0, z=1)
|
||||||
|
),
|
||||||
|
xaxis=dict(visible=False),
|
||||||
|
yaxis=dict(visible=False),
|
||||||
|
zaxis=dict(visible=False),
|
||||||
|
aspectmode='cube',
|
||||||
|
bgcolor='rgb(0,0,50)', # Space-like background
|
||||||
|
),
|
||||||
|
paper_bgcolor='black',
|
||||||
|
showlegend=False,
|
||||||
|
width=1920,
|
||||||
|
height=1080,
|
||||||
|
margin=dict(l=0, r=0, t=60, b=0)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save frame
|
||||||
|
frame_path = os.path.join(self.frames_folder, f"NavEntry_{frame_num:04d}.png")
|
||||||
|
fig.write_image(frame_path, engine="kaleido")
|
||||||
|
return frame_path
|
||||||
|
|
||||||
|
def create_navigation_frame(self, df, current_index, frame_num):
|
||||||
|
"""Create detailed navigation frame with 3D following camera"""
|
||||||
|
if not PLOTLY_AVAILABLE:
|
||||||
|
raise ImportError("Plotly is required for navigation frames")
|
||||||
|
|
||||||
|
current_row = df.iloc[current_index]
|
||||||
|
current_lat = current_row['latitude']
|
||||||
|
current_lon = current_row['longitude']
|
||||||
|
current_alt = current_row['elevation']
|
||||||
|
current_speed = current_row['speed_kmh']
|
||||||
|
current_bearing = current_row['bearing']
|
||||||
|
|
||||||
|
# Get route progress
|
||||||
|
completed_route = df.iloc[:current_index + 1]
|
||||||
|
remaining_route = df.iloc[current_index:]
|
||||||
|
|
||||||
|
# Create detailed terrain around current position
|
||||||
|
terrain_radius = 0.01 # degrees around current position
|
||||||
|
resolution = 60
|
||||||
|
|
||||||
|
lat_range = np.linspace(current_lat - terrain_radius, current_lat + terrain_radius, resolution)
|
||||||
|
lon_range = np.linspace(current_lon - terrain_radius, current_lon + terrain_radius, resolution)
|
||||||
|
lat_mesh, lon_mesh = np.meshgrid(lat_range, lon_range)
|
||||||
|
|
||||||
|
# Generate high-detail satellite terrain
|
||||||
|
terrain_heights = self.generate_detailed_terrain(lat_mesh, lon_mesh, current_lat, current_lon)
|
||||||
|
|
||||||
|
# Create navigation display figure
|
||||||
|
fig = go.Figure()
|
||||||
|
|
||||||
|
# Add detailed terrain
|
||||||
|
fig.add_trace(
|
||||||
|
go.Surface(
|
||||||
|
x=lon_mesh,
|
||||||
|
y=lat_mesh,
|
||||||
|
z=terrain_heights,
|
||||||
|
colorscale=[
|
||||||
|
[0.0, 'rgb(34,139,34)'], # Forest green
|
||||||
|
[0.2, 'rgb(107,142,35)'], # Olive drab
|
||||||
|
[0.4, 'rgb(255,215,0)'], # Gold (fields)
|
||||||
|
[0.5, 'rgb(210,180,140)'], # Tan (roads/clearings)
|
||||||
|
[0.7, 'rgb(139,69,19)'], # Brown (earth)
|
||||||
|
[0.9, 'rgb(105,105,105)'], # Gray (rock)
|
||||||
|
[1.0, 'rgb(255,255,255)'] # White (peaks)
|
||||||
|
],
|
||||||
|
opacity=0.9,
|
||||||
|
showscale=False,
|
||||||
|
lighting=dict(
|
||||||
|
ambient=0.3,
|
||||||
|
diffuse=0.9,
|
||||||
|
specular=0.1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add completed route (green)
|
||||||
|
if len(completed_route) > 1:
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter3d(
|
||||||
|
x=completed_route['longitude'],
|
||||||
|
y=completed_route['latitude'],
|
||||||
|
z=completed_route['elevation'] + 50,
|
||||||
|
mode='lines',
|
||||||
|
line=dict(color='lime', width=12),
|
||||||
|
name='Completed'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add remaining route (blue, semi-transparent)
|
||||||
|
if len(remaining_route) > 1:
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter3d(
|
||||||
|
x=remaining_route['longitude'],
|
||||||
|
y=remaining_route['latitude'],
|
||||||
|
z=remaining_route['elevation'] + 50,
|
||||||
|
mode='lines',
|
||||||
|
line=dict(color='cyan', width=8),
|
||||||
|
opacity=0.6,
|
||||||
|
name='Remaining'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add navigation point (current vehicle position)
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter3d(
|
||||||
|
x=[current_lon],
|
||||||
|
y=[current_lat],
|
||||||
|
z=[current_alt + 100],
|
||||||
|
mode='markers',
|
||||||
|
marker=dict(
|
||||||
|
color='red',
|
||||||
|
size=25,
|
||||||
|
symbol='diamond',
|
||||||
|
line=dict(color='white', width=4)
|
||||||
|
),
|
||||||
|
name='Vehicle'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add direction indicator
|
||||||
|
bearing_rad = math.radians(current_bearing)
|
||||||
|
arrow_length = 0.002
|
||||||
|
arrow_end_lat = current_lat + arrow_length * math.cos(bearing_rad)
|
||||||
|
arrow_end_lon = current_lon + arrow_length * math.sin(bearing_rad)
|
||||||
|
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter3d(
|
||||||
|
x=[current_lon, arrow_end_lon],
|
||||||
|
y=[current_lat, arrow_end_lat],
|
||||||
|
z=[current_alt + 120, current_alt + 120],
|
||||||
|
mode='lines',
|
||||||
|
line=dict(color='yellow', width=15),
|
||||||
|
name='Direction'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Calculate dynamic camera position for 3D following
|
||||||
|
# Camera follows behind and above at specified height
|
||||||
|
camera_height = self.camera_height_min + (self.camera_height_max - self.camera_height_min) * (current_speed / 100)
|
||||||
|
follow_distance_deg = 0.005 # degrees behind vehicle
|
||||||
|
|
||||||
|
# Position camera behind vehicle based on bearing
|
||||||
|
camera_bearing = (current_bearing + 180) % 360 # Opposite direction
|
||||||
|
camera_bearing_rad = math.radians(camera_bearing)
|
||||||
|
|
||||||
|
camera_lat = current_lat + follow_distance_deg * math.cos(camera_bearing_rad)
|
||||||
|
camera_lon = current_lon + follow_distance_deg * math.sin(camera_bearing_rad)
|
||||||
|
|
||||||
|
# Calculate relative camera position
|
||||||
|
camera_eye_x = (camera_lon - current_lon) * 100
|
||||||
|
camera_eye_y = (camera_lat - current_lat) * 100
|
||||||
|
camera_eye_z = camera_height / 1000
|
||||||
|
|
||||||
|
# Navigation info overlay
|
||||||
|
total_distance = sum(df['distance'])
|
||||||
|
completed_distance = sum(completed_route['distance'])
|
||||||
|
progress_percent = (completed_distance / total_distance) * 100 if total_distance > 0 else 0
|
||||||
|
|
||||||
|
fig.update_layout(
|
||||||
|
title=dict(
|
||||||
|
text=f'Navigation • Speed: {current_speed:.1f} km/h • Progress: {progress_percent:.1f}% • {current_row["timestamp"].strftime("%H:%M:%S")}',
|
||||||
|
x=0.5,
|
||||||
|
font=dict(size=20, color='white', family="Arial Black")
|
||||||
|
),
|
||||||
|
scene=dict(
|
||||||
|
camera=dict(
|
||||||
|
eye=dict(x=camera_eye_x, y=camera_eye_y, z=camera_eye_z),
|
||||||
|
center=dict(x=0, y=0, z=0),
|
||||||
|
up=dict(x=0, y=0, z=1)
|
||||||
|
),
|
||||||
|
xaxis=dict(visible=False),
|
||||||
|
yaxis=dict(visible=False),
|
||||||
|
zaxis=dict(visible=False),
|
||||||
|
aspectmode='manual',
|
||||||
|
aspectratio=dict(x=1, y=1, z=0.3),
|
||||||
|
bgcolor='rgb(135,206,235)' # Sky blue
|
||||||
|
),
|
||||||
|
paper_bgcolor='black',
|
||||||
|
showlegend=False,
|
||||||
|
width=1920,
|
||||||
|
height=1080,
|
||||||
|
margin=dict(l=0, r=0, t=50, b=0)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save frame
|
||||||
|
frame_path = os.path.join(self.frames_folder, f"Navigation_{frame_num:04d}.png")
|
||||||
|
fig.write_image(frame_path, engine="kaleido")
|
||||||
|
return frame_path
|
||||||
|
|
||||||
|
def generate_satellite_terrain(self, lat_mesh, lon_mesh, center_lat, center_lon):
|
||||||
|
"""Generate satellite-view realistic terrain for entry scene"""
|
||||||
|
# Convert to local coordinates
|
||||||
|
lat_m = (lat_mesh - center_lat) * 111000
|
||||||
|
lon_m = (lon_mesh - center_lon) * 111000 * np.cos(np.radians(center_lat))
|
||||||
|
|
||||||
|
# Base elevation with realistic variation
|
||||||
|
base_height = 200 + 50 * np.sin(lat_m / 5000) * np.cos(lon_m / 3000)
|
||||||
|
|
||||||
|
# Mountain ranges
|
||||||
|
mountains = 800 * np.exp(-((lat_m - 2000)**2 + (lon_m - 1000)**2) / (3000**2))
|
||||||
|
mountains += 600 * np.exp(-((lat_m + 1500)**2 + (lon_m + 2000)**2) / (2500**2))
|
||||||
|
|
||||||
|
# Hills and valleys
|
||||||
|
hills = 200 * np.sin(lat_m / 1000) * np.cos(lon_m / 1200)
|
||||||
|
valleys = -100 * np.exp(-((lat_m)**2 + (lon_m)**2) / (2000**2))
|
||||||
|
|
||||||
|
terrain = base_height + mountains + hills + valleys
|
||||||
|
return np.maximum(terrain, 50)
|
||||||
|
|
||||||
|
def generate_detailed_terrain(self, lat_mesh, lon_mesh, center_lat, center_lon):
|
||||||
|
"""Generate high-detail terrain for navigation view"""
|
||||||
|
# Convert to local coordinates
|
||||||
|
lat_m = (lat_mesh - center_lat) * 111000
|
||||||
|
lon_m = (lon_mesh - center_lon) * 111000 * np.cos(np.radians(center_lat))
|
||||||
|
|
||||||
|
# Base terrain
|
||||||
|
base = 150 + 30 * np.sin(lat_m / 500) * np.cos(lon_m / 400)
|
||||||
|
|
||||||
|
# Local features
|
||||||
|
hills = 100 * np.exp(-((lat_m - 300)**2 + (lon_m - 200)**2) / (200**2))
|
||||||
|
ridges = 80 * np.exp(-((lat_m + 200)**2 + (lon_m - 400)**2) / (300**2))
|
||||||
|
|
||||||
|
# Fine detail
|
||||||
|
detail = 20 * np.sin(lat_m / 50) * np.cos(lon_m / 60)
|
||||||
|
|
||||||
|
terrain = base + hills + ridges + detail
|
||||||
|
return np.maximum(terrain, 30)
|
||||||
|
|
||||||
for i in range(len(df)):
|
for i in range(len(df)):
|
||||||
if i == 0:
|
if i == 0:
|
||||||
@@ -557,14 +907,16 @@ class Advanced3DGenerator:
|
|||||||
return frame_paths
|
return frame_paths
|
||||||
|
|
||||||
def create_video(self, frame_paths, output_video_path, progress_callback=None):
|
def create_video(self, frame_paths, output_video_path, progress_callback=None):
|
||||||
"""Create video from frames using MoviePy with optimized settings"""
|
"""Create video from frames using OpenCV for better compatibility"""
|
||||||
print("Creating Relive-style animation video...")
|
print("Creating navigation animation video...")
|
||||||
|
|
||||||
if not frame_paths:
|
if not frame_paths:
|
||||||
print("No frames to create video from")
|
print("No frames to create video from")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
import cv2
|
||||||
|
|
||||||
# Filter out None paths
|
# Filter out None paths
|
||||||
valid_frames = [f for f in frame_paths if f and os.path.exists(f)]
|
valid_frames = [f for f in frame_paths if f and os.path.exists(f)]
|
||||||
|
|
||||||
@@ -574,46 +926,67 @@ class Advanced3DGenerator:
|
|||||||
|
|
||||||
print(f"Creating video from {len(valid_frames)} frames at {self.fps} FPS...")
|
print(f"Creating video from {len(valid_frames)} frames at {self.fps} FPS...")
|
||||||
|
|
||||||
# Create video clip from images with optimal settings
|
# Update progress
|
||||||
clip = ImageSequenceClip(valid_frames, fps=self.fps)
|
if progress_callback:
|
||||||
|
progress_callback(30, "Reading frame dimensions...")
|
||||||
|
|
||||||
# Add smooth fade effects for professional look
|
# Read first frame to get dimensions
|
||||||
clip = clip.fadein(0.5).fadeout(0.5)
|
first_frame = cv2.imread(valid_frames[0])
|
||||||
|
if first_frame is None:
|
||||||
|
print("Error reading first frame")
|
||||||
|
return False
|
||||||
|
|
||||||
|
height, width, layers = first_frame.shape
|
||||||
|
|
||||||
# Update progress
|
# Update progress
|
||||||
if progress_callback:
|
if progress_callback:
|
||||||
progress_callback(50, "Encoding video with optimized settings...")
|
progress_callback(40, "Setting up video encoder...")
|
||||||
|
|
||||||
# Write video file with high quality settings
|
# Create video writer with OpenCV
|
||||||
clip.write_videofile(
|
fourcc = cv2.VideoWriter_fourcc(*'mp4v') # You can also try 'XVID'
|
||||||
output_video_path,
|
video_writer = cv2.VideoWriter(output_video_path, fourcc, self.fps, (width, height))
|
||||||
codec='libx264',
|
|
||||||
audio=False,
|
if not video_writer.isOpened():
|
||||||
temp_audiofile=None,
|
print("Error: Could not open video writer")
|
||||||
remove_temp=True,
|
return False
|
||||||
verbose=False,
|
|
||||||
logger=None,
|
# Update progress
|
||||||
bitrate="8000k", # High quality bitrate
|
if progress_callback:
|
||||||
ffmpeg_params=[
|
progress_callback(50, "Writing frames to video...")
|
||||||
"-preset", "medium", # Balance between speed and compression
|
|
||||||
"-crf", "18", # High quality (lower = better quality)
|
# Write frames to video
|
||||||
"-pix_fmt", "yuv420p" # Better compatibility
|
total_frames = len(valid_frames)
|
||||||
]
|
for i, frame_path in enumerate(valid_frames):
|
||||||
)
|
frame = cv2.imread(frame_path)
|
||||||
|
if frame is not None:
|
||||||
|
video_writer.write(frame)
|
||||||
|
|
||||||
|
# Update progress periodically
|
||||||
|
if progress_callback and i % 10 == 0:
|
||||||
|
progress_percent = 50 + (i / total_frames) * 40 # 50-90%
|
||||||
|
progress_callback(progress_percent, f"Writing frame {i+1}/{total_frames}...")
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
video_writer.release()
|
||||||
|
cv2.destroyAllWindows()
|
||||||
|
|
||||||
if progress_callback:
|
if progress_callback:
|
||||||
progress_callback(100, f"Video successfully created: {os.path.basename(output_video_path)}")
|
progress_callback(100, f"Video successfully created: {os.path.basename(output_video_path)}")
|
||||||
|
|
||||||
print(f"✅ Relive-style animation video saved to: {output_video_path}")
|
# Verify the video was created
|
||||||
print(f"📊 Video info: {len(valid_frames)} frames, {self.fps} FPS, {clip.duration:.1f}s duration")
|
if os.path.exists(output_video_path) and os.path.getsize(output_video_path) > 0:
|
||||||
|
print(f"✅ Navigation animation video saved to: {output_video_path}")
|
||||||
# Clean up clip
|
file_size = os.path.getsize(output_video_path) / (1024 * 1024) # MB
|
||||||
clip.close()
|
print(f"📊 Video info: {len(valid_frames)} frames, {self.fps} FPS, {file_size:.1f} MB")
|
||||||
|
return True
|
||||||
return True
|
else:
|
||||||
|
print("Error: Video file was not created properly")
|
||||||
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error creating video: {e}")
|
print(f"Error creating video: {e}")
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(-1, f"Error: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def cleanup_frames(self):
|
def cleanup_frames(self):
|
||||||
@@ -623,38 +996,7 @@ class Advanced3DGenerator:
|
|||||||
shutil.rmtree(self.frames_folder)
|
shutil.rmtree(self.frames_folder)
|
||||||
os.makedirs(self.frames_folder, exist_ok=True)
|
os.makedirs(self.frames_folder, exist_ok=True)
|
||||||
|
|
||||||
def generate_3d_animation(self, positions_file, output_video_path,
|
|
||||||
style='advanced', cleanup=True, progress_callback=None):
|
|
||||||
"""
|
|
||||||
Main method to generate 3D animation
|
|
||||||
|
|
||||||
Args:
|
|
||||||
positions_file: Path to JSON file with GPS positions
|
|
||||||
output_video_path: Path for output video
|
|
||||||
style: 'pydeck', 'plotly', or 'advanced'
|
|
||||||
cleanup: Whether to clean up temporary files
|
|
||||||
progress_callback: Callback function for progress updates
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Generate frames
|
|
||||||
frame_paths = self.generate_frames(positions_file, style, progress_callback)
|
|
||||||
|
|
||||||
if not frame_paths:
|
|
||||||
raise Exception("No frames generated")
|
|
||||||
|
|
||||||
# Create video
|
|
||||||
success = self.create_video(frame_paths, output_video_path, progress_callback)
|
|
||||||
|
|
||||||
if cleanup:
|
|
||||||
self.cleanup_frames()
|
|
||||||
|
|
||||||
return success
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error generating 3D animation: {e}")
|
|
||||||
if progress_callback:
|
|
||||||
progress_callback(-1, f"Error: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def create_google_earth_frame(self, df, current_index, frame_num):
|
def create_google_earth_frame(self, df, current_index, frame_num):
|
||||||
"""
|
"""
|
||||||
@@ -887,42 +1229,4 @@ class Advanced3DGenerator:
|
|||||||
|
|
||||||
return terrain
|
return terrain
|
||||||
|
|
||||||
def generate_advanced_3d_video(positions_file, output_folder, filename_prefix="advanced_3d",
|
|
||||||
style='advanced', progress_callback=None):
|
|
||||||
"""
|
|
||||||
Convenience function to generate advanced 3D video
|
|
||||||
"""
|
|
||||||
generator = Advanced3DGenerator(output_folder)
|
|
||||||
|
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
||||||
output_video_path = os.path.join(output_folder, f"{filename_prefix}_{timestamp}.mp4")
|
|
||||||
|
|
||||||
success = generator.generate_3d_animation(
|
|
||||||
positions_file,
|
|
||||||
output_video_path,
|
|
||||||
style=style,
|
|
||||||
progress_callback=progress_callback
|
|
||||||
)
|
|
||||||
|
|
||||||
return output_video_path if success else None
|
|
||||||
|
|
||||||
# Test function
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Test the advanced 3D generator
|
|
||||||
test_positions = "test_positions.json"
|
|
||||||
output_dir = "test_output"
|
|
||||||
|
|
||||||
def test_progress(progress, message):
|
|
||||||
print(f"Progress: {progress:.1f}% - {message}")
|
|
||||||
|
|
||||||
video_path = generate_advanced_3d_video(
|
|
||||||
test_positions,
|
|
||||||
output_dir,
|
|
||||||
style='advanced',
|
|
||||||
progress_callback=test_progress
|
|
||||||
)
|
|
||||||
|
|
||||||
if video_path:
|
|
||||||
print(f"Test video created: {video_path}")
|
|
||||||
else:
|
|
||||||
print("Test failed")
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,6 @@ selenium
|
|||||||
pillow
|
pillow
|
||||||
geopy
|
geopy
|
||||||
opencv-python
|
opencv-python
|
||||||
moviepy
|
|
||||||
requests
|
requests
|
||||||
numpy
|
numpy
|
||||||
matplotlib
|
matplotlib
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
gAAAAABobmx0PnGbcR3Hxn93Z2r3z0dqZpHYGfWhJC7ko6QSMHLY_qoGsEZLrlLjjGrdjVOqSNVfwCP6_pAQ5QWbDRs6RoyZFPIA-vLFYpU9tUVC6pHCSSxvQimS_Thdj5WMIBlpTOWa
|
gAAAAABob4-usKjps0vEVupB8FIJ3tKqoOeedzOUpt16NICbpi1ejKoSwqDvH7eIAPaZCOkfbPC6gjJGTo2yxt4BOPg1yzg_-Xanpl5iL1Y2mIRxWag-5cWhDNiqZo3bEZMqZ3M875O-
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 157 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 156 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 158 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 156 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 169 KiB |
@@ -1,20 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"start_time": "2025-07-08T04:51:13.000+00:00",
|
|
||||||
"end_time": "2025-07-08T13:20:50.000+00:00",
|
|
||||||
"duration_seconds": 30578,
|
|
||||||
"location": {
|
|
||||||
"latitude": 45.79908722222223,
|
|
||||||
"longitude": 24.085938333333335
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"start_time": "2025-07-08T13:33:15.000+00:00",
|
|
||||||
"end_time": "2025-07-08T13:35:59.000+00:00",
|
|
||||||
"duration_seconds": 164,
|
|
||||||
"location": {
|
|
||||||
"latitude": 45.794045000000004,
|
|
||||||
"longitude": 24.13890055555556
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -9,8 +9,7 @@ from kivy.properties import StringProperty, NumericProperty, AliasProperty
|
|||||||
from py_scripts.utils import (
|
from py_scripts.utils import (
|
||||||
process_preview_util, optimize_route_entries_util
|
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
|
||||||
from py_scripts.advanced_3d_generator import Advanced3DGenerator
|
|
||||||
from py_scripts.blender_animator import BlenderGPSAnimator
|
from py_scripts.blender_animator import BlenderGPSAnimator
|
||||||
from kivy.uix.popup import Popup
|
from kivy.uix.popup import Popup
|
||||||
from kivy.uix.button import Button
|
from kivy.uix.button import Button
|
||||||
@@ -51,8 +50,6 @@ class CreateAnimationScreen(Screen):
|
|||||||
count = 0
|
count = 0
|
||||||
self.ids.route_entries_label.text = f"Your route has {count} entries,"
|
self.ids.route_entries_label.text = f"Your route has {count} entries,"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def open_rename_popup(self):
|
def open_rename_popup(self):
|
||||||
from kivy.uix.popup import Popup
|
from kivy.uix.popup import Popup
|
||||||
from kivy.uix.boxlayout import BoxLayout
|
from kivy.uix.boxlayout import BoxLayout
|
||||||
@@ -131,7 +128,6 @@ class CreateAnimationScreen(Screen):
|
|||||||
on_save=lambda: self.on_pre_enter()
|
on_save=lambda: self.on_pre_enter()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def preview_route(self):
|
def preview_route(self):
|
||||||
# Show processing popup
|
# Show processing popup
|
||||||
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||||||
@@ -168,291 +164,16 @@ class CreateAnimationScreen(Screen):
|
|||||||
0.5
|
0.5
|
||||||
)
|
)
|
||||||
|
|
||||||
def open_pauses_popup(self):
|
def generate_google_earth_animation(self):
|
||||||
"""Navigate to the pause edit screen"""
|
"""Generate Google Earth-style flythrough animation using NavigationAnimationGenerator"""
|
||||||
pause_edit_screen = self.manager.get_screen("pause_edit")
|
|
||||||
pause_edit_screen.set_project_and_callback(self.project_name, self.on_pre_enter)
|
|
||||||
self.manager.current = "pause_edit"
|
|
||||||
|
|
||||||
def generate_3d_video(self):
|
|
||||||
"""Show video generation mode selection popup"""
|
|
||||||
self.show_video_generation_options()
|
|
||||||
|
|
||||||
def generate_3d_video_test_mode(self):
|
|
||||||
"""Generate a 3D video animation in 720p test mode for faster processing"""
|
|
||||||
# Show processing popup with test mode indication
|
|
||||||
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
|
||||||
label = Label(text="Preparing 720p test video generation...")
|
|
||||||
progress = ProgressBar(max=100, value=0)
|
|
||||||
layout.add_widget(label)
|
|
||||||
layout.add_widget(progress)
|
|
||||||
popup = Popup(
|
|
||||||
title="Generating 3D Video Animation (720p Test Mode)",
|
|
||||||
content=layout,
|
|
||||||
size_hint=(0.9, None),
|
|
||||||
size=(0, 200),
|
|
||||||
auto_dismiss=False
|
|
||||||
)
|
|
||||||
popup.open()
|
|
||||||
|
|
||||||
# Schedule the 3D video generation in test mode
|
|
||||||
Clock.schedule_once(
|
|
||||||
lambda dt: generate_3d_video_animation(
|
|
||||||
self.project_name,
|
|
||||||
RESOURCES_FOLDER,
|
|
||||||
label,
|
|
||||||
progress,
|
|
||||||
popup,
|
|
||||||
Clock,
|
|
||||||
test_mode=True # Enable test mode
|
|
||||||
),
|
|
||||||
0.5
|
|
||||||
)
|
|
||||||
|
|
||||||
def generate_3d_video_production_mode(self):
|
|
||||||
"""Generate a 3D video animation in 2K production mode for high quality"""
|
|
||||||
# Show processing popup with production mode indication
|
|
||||||
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
|
||||||
label = Label(text="Preparing 2K production video generation...")
|
|
||||||
progress = ProgressBar(max=100, value=0)
|
|
||||||
layout.add_widget(label)
|
|
||||||
layout.add_widget(progress)
|
|
||||||
popup = Popup(
|
|
||||||
title="Generating 3D Video Animation (2K Production Mode)",
|
|
||||||
content=layout,
|
|
||||||
size_hint=(0.9, None),
|
|
||||||
size=(0, 200),
|
|
||||||
auto_dismiss=False
|
|
||||||
)
|
|
||||||
popup.open()
|
|
||||||
|
|
||||||
# Schedule the 3D video generation in production mode
|
|
||||||
Clock.schedule_once(
|
|
||||||
lambda dt: generate_3d_video_animation(
|
|
||||||
self.project_name,
|
|
||||||
RESOURCES_FOLDER,
|
|
||||||
label,
|
|
||||||
progress,
|
|
||||||
popup,
|
|
||||||
Clock,
|
|
||||||
test_mode=False # Disable test mode for production
|
|
||||||
),
|
|
||||||
0.5
|
|
||||||
)
|
|
||||||
|
|
||||||
def show_video_generation_options(self):
|
|
||||||
"""Show popup with video generation mode options including new advanced animations"""
|
|
||||||
from kivy.uix.popup import Popup
|
|
||||||
from kivy.uix.boxlayout import BoxLayout
|
|
||||||
from kivy.uix.button import Button
|
|
||||||
from kivy.uix.label import Label
|
|
||||||
|
|
||||||
layout = BoxLayout(orientation='vertical', spacing=12, padding=15)
|
|
||||||
|
|
||||||
# Title
|
|
||||||
title_label = Label(
|
|
||||||
text="Choose Animation Style & Quality",
|
|
||||||
font_size=20,
|
|
||||||
size_hint_y=None,
|
|
||||||
height=40,
|
|
||||||
color=(1, 1, 1, 1)
|
|
||||||
)
|
|
||||||
layout.add_widget(title_label)
|
|
||||||
|
|
||||||
# Classic 3D Mode
|
|
||||||
classic_layout = BoxLayout(orientation='vertical', spacing=5)
|
|
||||||
classic_title = Label(
|
|
||||||
text="🏃♂️ Classic 3D (Original Pipeline)",
|
|
||||||
font_size=16,
|
|
||||||
size_hint_y=None,
|
|
||||||
height=30,
|
|
||||||
color=(0.2, 0.8, 0.2, 1)
|
|
||||||
)
|
|
||||||
classic_desc = Label(
|
|
||||||
text="• Traditional OpenCV/PIL approach\n• Fast generation\n• Good for simple tracks\n• Test (720p) or Production (2K)",
|
|
||||||
font_size=11,
|
|
||||||
size_hint_y=None,
|
|
||||||
height=70,
|
|
||||||
color=(0.9, 0.9, 0.9, 1),
|
|
||||||
halign="left",
|
|
||||||
valign="middle"
|
|
||||||
)
|
|
||||||
classic_desc.text_size = (None, None)
|
|
||||||
classic_layout.add_widget(classic_title)
|
|
||||||
classic_layout.add_widget(classic_desc)
|
|
||||||
layout.add_widget(classic_layout)
|
|
||||||
|
|
||||||
# Classic buttons
|
|
||||||
classic_btn_layout = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=45)
|
|
||||||
classic_test_btn = Button(
|
|
||||||
text="Classic 720p",
|
|
||||||
background_color=(0.2, 0.8, 0.2, 1),
|
|
||||||
font_size=12
|
|
||||||
)
|
|
||||||
classic_prod_btn = Button(
|
|
||||||
text="Classic 2K",
|
|
||||||
background_color=(0.3, 0.6, 0.3, 1),
|
|
||||||
font_size=12
|
|
||||||
)
|
|
||||||
classic_btn_layout.add_widget(classic_test_btn)
|
|
||||||
classic_btn_layout.add_widget(classic_prod_btn)
|
|
||||||
layout.add_widget(classic_btn_layout)
|
|
||||||
|
|
||||||
# Advanced Pydeck/Plotly Mode
|
|
||||||
advanced_layout = BoxLayout(orientation='vertical', spacing=5)
|
|
||||||
advanced_title = Label(
|
|
||||||
text="🚀 Advanced 3D (Pydeck + Plotly)",
|
|
||||||
font_size=16,
|
|
||||||
size_hint_y=None,
|
|
||||||
height=30,
|
|
||||||
color=(0.2, 0.6, 0.9, 1)
|
|
||||||
)
|
|
||||||
advanced_desc = Label(
|
|
||||||
text="• Professional geospatial visualization\n• Interactive 3D terrain\n• Advanced camera movements\n• High-quality animations",
|
|
||||||
font_size=11,
|
|
||||||
size_hint_y=None,
|
|
||||||
height=70,
|
|
||||||
color=(0.9, 0.9, 0.9, 1),
|
|
||||||
halign="left",
|
|
||||||
valign="middle"
|
|
||||||
)
|
|
||||||
advanced_desc.text_size = (None, None)
|
|
||||||
advanced_layout.add_widget(advanced_title)
|
|
||||||
advanced_layout.add_widget(advanced_desc)
|
|
||||||
layout.add_widget(advanced_layout)
|
|
||||||
|
|
||||||
# Advanced button
|
|
||||||
advanced_btn = Button(
|
|
||||||
text="Generate Advanced 3D Animation",
|
|
||||||
background_color=(0.2, 0.6, 0.9, 1),
|
|
||||||
size_hint_y=None,
|
|
||||||
height=45,
|
|
||||||
font_size=13
|
|
||||||
)
|
|
||||||
layout.add_widget(advanced_btn)
|
|
||||||
|
|
||||||
# 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="<EFBFBD> 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_advanced_3d_animation(self):
|
|
||||||
"""Generate advanced 3D animation using Pydeck and Plotly"""
|
|
||||||
# Show processing popup
|
# Show processing popup
|
||||||
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||||||
label = Label(text="Initializing advanced 3D animation...")
|
label = Label(text="Initializing Google Earth flythrough...")
|
||||||
progress = ProgressBar(max=100, value=0)
|
progress = ProgressBar(max=100, value=0)
|
||||||
layout.add_widget(label)
|
layout.add_widget(label)
|
||||||
layout.add_widget(progress)
|
layout.add_widget(progress)
|
||||||
popup = Popup(
|
popup = Popup(
|
||||||
title="Generating Advanced 3D Animation",
|
title="Generating Google Earth Flythrough",
|
||||||
content=layout,
|
content=layout,
|
||||||
size_hint=(0.9, None),
|
size_hint=(0.9, None),
|
||||||
size=(0, 200),
|
size=(0, 200),
|
||||||
@@ -460,7 +181,7 @@ class CreateAnimationScreen(Screen):
|
|||||||
)
|
)
|
||||||
popup.open()
|
popup.open()
|
||||||
|
|
||||||
def run_advanced_animation():
|
def run_google_earth_animation():
|
||||||
try:
|
try:
|
||||||
# Update status
|
# Update status
|
||||||
def update_status(progress_val, status_text):
|
def update_status(progress_val, status_text):
|
||||||
@@ -480,38 +201,38 @@ class CreateAnimationScreen(Screen):
|
|||||||
update_status(10, "Loading GPS data...")
|
update_status(10, "Loading GPS data...")
|
||||||
|
|
||||||
# Check dependencies first
|
# Check dependencies first
|
||||||
generator = Advanced3DGenerator(project_folder)
|
generator = NavigationAnimationGenerator(project_folder)
|
||||||
generator.check_dependencies()
|
generator.check_dependencies()
|
||||||
|
|
||||||
update_status(20, "Processing GPS coordinates...")
|
update_status(20, "Processing GPS coordinates...")
|
||||||
df = generator.load_gps_data(positions_path)
|
df = generator.load_gps_data(positions_path)
|
||||||
|
|
||||||
update_status(40, "Creating 3D visualization frames...")
|
update_status(40, "Creating Google Earth flythrough...")
|
||||||
output_video_path = os.path.join(project_folder, f"{self.project_name}_advanced_3d_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
|
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
|
# Progress callback for the generator
|
||||||
def generator_progress(progress, message):
|
def generator_progress(progress, message):
|
||||||
update_status(40 + (progress * 0.4), message) # Map 0-100% to 40-80%
|
update_status(40 + (progress * 0.5), message) # Map 0-100% to 40-90%
|
||||||
|
|
||||||
update_status(80, "Rendering video...")
|
update_status(90, "Creating flythrough video...")
|
||||||
success = generator.generate_3d_animation(
|
success = generator.generate_frames(positions_path, style='google_earth', progress_callback=generator_progress)
|
||||||
positions_path,
|
|
||||||
output_video_path,
|
|
||||||
style='advanced',
|
|
||||||
progress_callback=generator_progress
|
|
||||||
)
|
|
||||||
|
|
||||||
if success:
|
if success and len(success) > 0:
|
||||||
update_status(100, "Advanced 3D animation complete!")
|
update_status(95, "Rendering final video...")
|
||||||
output_path = output_video_path
|
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:
|
else:
|
||||||
raise Exception("Failed to generate video")
|
raise Exception("Failed to generate frames")
|
||||||
|
|
||||||
def show_success(dt):
|
def show_success(dt):
|
||||||
popup.dismiss()
|
popup.dismiss()
|
||||||
self.show_success_popup(
|
self.show_success_popup(
|
||||||
"Advanced 3D Animation Complete!",
|
"Google Earth Flythrough Complete!",
|
||||||
f"Your high-quality 3D animation has been saved to:\n{output_path}",
|
f"Your Google Earth-style flythrough has been saved to:\n{output_path}",
|
||||||
output_path
|
output_path
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -521,13 +242,13 @@ class CreateAnimationScreen(Screen):
|
|||||||
error_message = str(e)
|
error_message = str(e)
|
||||||
def show_error(dt):
|
def show_error(dt):
|
||||||
popup.dismiss()
|
popup.dismiss()
|
||||||
self.show_error_popup("Advanced Animation Error", error_message)
|
self.show_error_popup("Google Earth Animation Error", error_message)
|
||||||
|
|
||||||
Clock.schedule_once(show_error, 0)
|
Clock.schedule_once(show_error, 0)
|
||||||
|
|
||||||
# Schedule the animation generation
|
# Schedule the animation generation
|
||||||
Clock.schedule_once(lambda dt: run_advanced_animation(), 0.5)
|
Clock.schedule_once(lambda dt: run_google_earth_animation(), 0.5)
|
||||||
|
|
||||||
def generate_blender_animation(self):
|
def generate_blender_animation(self):
|
||||||
"""Generate cinema-quality animation using Blender"""
|
"""Generate cinema-quality animation using Blender"""
|
||||||
# Show processing popup
|
# Show processing popup
|
||||||
@@ -588,13 +309,13 @@ class CreateAnimationScreen(Screen):
|
|||||||
update_status(100, "Blender cinema animation complete!")
|
update_status(100, "Blender cinema animation complete!")
|
||||||
output_path = output_video_path
|
output_path = output_video_path
|
||||||
else:
|
else:
|
||||||
raise Exception("Failed to render Blender animation")
|
raise Exception("Failed to generate Blender animation")
|
||||||
|
|
||||||
def show_success(dt):
|
def show_success(dt):
|
||||||
popup.dismiss()
|
popup.dismiss()
|
||||||
self.show_success_popup(
|
self.show_success_popup(
|
||||||
"Blender Cinema Animation Complete!",
|
"Blender Cinema Animation Complete!",
|
||||||
f"Your cinema-quality animation has been rendered to:\n{output_path}",
|
f"Your cinema-quality animation has been saved to:\n{output_path}",
|
||||||
output_path
|
output_path
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -610,26 +331,34 @@ class CreateAnimationScreen(Screen):
|
|||||||
|
|
||||||
# Schedule the animation generation
|
# Schedule the animation generation
|
||||||
Clock.schedule_once(lambda dt: run_blender_animation(), 0.5)
|
Clock.schedule_once(lambda dt: run_blender_animation(), 0.5)
|
||||||
|
|
||||||
def generate_google_earth_animation(self):
|
def generate_progressive_3d_animation(self):
|
||||||
"""Generate Google Earth-style flythrough animation with terrain"""
|
"""Generate a progressive 3D animation that builds the trip point by point"""
|
||||||
# Show processing popup
|
# Show processing popup
|
||||||
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||||||
label = Label(text="Initializing Google Earth flythrough...")
|
label = Label(text="Initializing progressive 3D animation...")
|
||||||
progress = ProgressBar(max=100, value=0)
|
progress = ProgressBar(max=100, value=0)
|
||||||
layout.add_widget(label)
|
layout.add_widget(label)
|
||||||
layout.add_widget(progress)
|
layout.add_widget(progress)
|
||||||
popup = Popup(
|
popup = Popup(
|
||||||
title="Generating Google Earth Flythrough",
|
title="Generating Progressive 3D Animation",
|
||||||
content=layout,
|
content=layout,
|
||||||
size_hint=(0.9, None),
|
size_hint=(0.9, None),
|
||||||
size=(0, 200),
|
size=(0, 200),
|
||||||
auto_dismiss=False
|
auto_dismiss=False
|
||||||
)
|
)
|
||||||
popup.open()
|
popup.open()
|
||||||
|
|
||||||
def run_google_earth_animation():
|
def run_progressive_animation():
|
||||||
try:
|
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
|
# Update status
|
||||||
def update_status(progress_val, status_text):
|
def update_status(progress_val, status_text):
|
||||||
def _update(dt):
|
def _update(dt):
|
||||||
@@ -645,132 +374,360 @@ class CreateAnimationScreen(Screen):
|
|||||||
Clock.schedule_once(lambda dt: popup.dismiss(), 2)
|
Clock.schedule_once(lambda dt: popup.dismiss(), 2)
|
||||||
return
|
return
|
||||||
|
|
||||||
update_status(10, "Checking dependencies...")
|
update_status(10, "Loading GPS data...")
|
||||||
|
|
||||||
# Check dependencies first
|
# Load GPS data
|
||||||
generator = Advanced3DGenerator(project_folder)
|
with open(positions_path, 'r') as f:
|
||||||
generator.check_dependencies()
|
positions = json.load(f)
|
||||||
|
|
||||||
update_status(20, "Loading GPS data...")
|
if len(positions) < 2:
|
||||||
df = generator.load_gps_data(positions_path)
|
update_status(0, "Error: Need at least 2 GPS points")
|
||||||
|
Clock.schedule_once(lambda dt: popup.dismiss(), 2)
|
||||||
|
return
|
||||||
|
|
||||||
update_status(30, "Generating terrain and camera flythrough...")
|
update_status(20, "Processing GPS coordinates...")
|
||||||
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
|
# Extract coordinates and timestamps
|
||||||
def generator_progress(progress, message):
|
lats = [pos['latitude'] for pos in positions]
|
||||||
update_status(30 + (progress * 0.5), message) # Map 0-100% to 30-80%
|
lons = [pos['longitude'] for pos in positions]
|
||||||
|
alts = [pos.get('altitude', 0) for pos in positions]
|
||||||
|
timestamps = [pos.get('fixTime', '') for pos in positions]
|
||||||
|
|
||||||
update_status(80, "Creating flythrough video...")
|
# Convert to numpy arrays for easier manipulation
|
||||||
success = generator.generate_3d_animation(
|
lats = np.array(lats)
|
||||||
positions_path,
|
lons = np.array(lons)
|
||||||
output_video_path,
|
alts = np.array(alts)
|
||||||
style='google_earth',
|
|
||||||
progress_callback=generator_progress
|
|
||||||
)
|
|
||||||
|
|
||||||
if success:
|
# Normalize coordinates for better visualization
|
||||||
update_status(100, "Google Earth flythrough complete!")
|
lat_center = np.mean(lats)
|
||||||
output_path = output_video_path
|
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:
|
else:
|
||||||
raise Exception("Failed to generate flythrough video")
|
raise Exception("No frames were generated")
|
||||||
|
|
||||||
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:
|
except Exception as e:
|
||||||
error_message = str(e)
|
error_message = str(e)
|
||||||
|
print(f"DEBUG: Progressive animation error: {error_message}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
def show_error(dt):
|
def show_error(dt):
|
||||||
popup.dismiss()
|
popup.dismiss()
|
||||||
self.show_error_popup("Google Earth Animation Error", error_message)
|
self.show_error_popup("Progressive Animation Error", error_message)
|
||||||
|
|
||||||
Clock.schedule_once(show_error, 0)
|
Clock.schedule_once(show_error, 0)
|
||||||
|
|
||||||
# Schedule the animation generation
|
# Schedule the animation generation
|
||||||
Clock.schedule_once(lambda dt: run_google_earth_animation(), 0.5)
|
Clock.schedule_once(lambda dt: run_progressive_animation(), 0.5)
|
||||||
|
|
||||||
def show_success_popup(self, title, message, file_path=None):
|
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"""
|
"""Show success popup with option to open file location"""
|
||||||
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
layout = BoxLayout(orientation='vertical', spacing=10, padding=15)
|
||||||
|
|
||||||
|
# Success message
|
||||||
success_label = Label(
|
success_label = Label(
|
||||||
text=message,
|
text=message,
|
||||||
text_size=(400, None),
|
text_size=(None, None),
|
||||||
halign="center",
|
halign="center",
|
||||||
valign="middle"
|
valign="middle"
|
||||||
)
|
)
|
||||||
layout.add_widget(success_label)
|
layout.add_widget(success_label)
|
||||||
|
|
||||||
button_layout = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=50)
|
# Buttons
|
||||||
|
btn_layout = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=50)
|
||||||
|
|
||||||
if file_path:
|
open_folder_btn = Button(
|
||||||
open_btn = Button(text="Open Folder", background_color=(0.2, 0.8, 0.2, 1))
|
text="Open Folder",
|
||||||
open_btn.bind(on_press=lambda x: self.open_file_location(file_path))
|
background_color=(0.2, 0.6, 0.9, 1)
|
||||||
button_layout.add_widget(open_btn)
|
)
|
||||||
|
|
||||||
ok_btn = Button(text="OK", background_color=(0.2, 0.6, 0.9, 1))
|
ok_btn = Button(
|
||||||
button_layout.add_widget(ok_btn)
|
text="OK",
|
||||||
layout.add_widget(button_layout)
|
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(
|
popup = Popup(
|
||||||
title=title,
|
title=title,
|
||||||
content=layout,
|
content=layout,
|
||||||
size_hint=(0.8, None),
|
size_hint=(0.9, 0.6),
|
||||||
size=(0, 250),
|
|
||||||
auto_dismiss=False
|
auto_dismiss=False
|
||||||
)
|
)
|
||||||
|
|
||||||
ok_btn.bind(on_press=lambda x: popup.dismiss())
|
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()
|
popup.open()
|
||||||
|
|
||||||
def show_error_popup(self, title, message):
|
def show_error_popup(self, title, message):
|
||||||
"""Show error popup"""
|
"""Show error popup"""
|
||||||
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
layout = BoxLayout(orientation='vertical', spacing=10, padding=15)
|
||||||
|
|
||||||
error_label = Label(
|
error_label = Label(
|
||||||
text=f"Error: {message}",
|
text=f"Error: {message}",
|
||||||
text_size=(400, None),
|
text_size=(None, None),
|
||||||
halign="center",
|
halign="center",
|
||||||
valign="middle",
|
valign="middle"
|
||||||
color=(1, 0.3, 0.3, 1)
|
|
||||||
)
|
)
|
||||||
layout.add_widget(error_label)
|
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)
|
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)
|
layout.add_widget(ok_btn)
|
||||||
|
|
||||||
popup = Popup(
|
popup = Popup(
|
||||||
title=title,
|
title=title,
|
||||||
content=layout,
|
content=layout,
|
||||||
size_hint=(0.8, None),
|
size_hint=(0.8, 0.4),
|
||||||
size=(0, 200),
|
|
||||||
auto_dismiss=False
|
auto_dismiss=False
|
||||||
)
|
)
|
||||||
|
|
||||||
ok_btn.bind(on_press=lambda x: popup.dismiss())
|
ok_btn.bind(on_press=lambda x: popup.dismiss())
|
||||||
popup.open()
|
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}")
|
|
||||||
|
|
||||||
|
|||||||
633
screens/create_animation_screen.py.backup
Normal file
633
screens/create_animation_screen.py.backup
Normal 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}")
|
||||||
|
|
||||||
419
screens/create_animation_screen_clean.py
Normal file
419
screens/create_animation_screen_clean.py
Normal 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()
|
||||||
@@ -7,7 +7,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
sys.path.append('/home/pi/Desktop/traccar_animation')
|
sys.path.append('/home/pi/Desktop/traccar_animation')
|
||||||
|
|
||||||
from py_scripts.advanced_3d_generator import Advanced3DGenerator
|
from py_scripts.advanced_3d_generator import NavigationAnimationGenerator
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
def test_google_earth_animation():
|
def test_google_earth_animation():
|
||||||
@@ -39,7 +39,7 @@ def test_google_earth_animation():
|
|||||||
print(f"Testing Google Earth animation with project: {project_name}")
|
print(f"Testing Google Earth animation with project: {project_name}")
|
||||||
|
|
||||||
# Create generator
|
# Create generator
|
||||||
generator = Advanced3DGenerator(project_folder)
|
generator = NavigationAnimationGenerator(project_folder)
|
||||||
|
|
||||||
# Check dependencies
|
# Check dependencies
|
||||||
try:
|
try:
|
||||||
@@ -57,22 +57,36 @@ def test_google_earth_animation():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
print("Starting Google Earth flythrough generation...")
|
print("Starting Google Earth flythrough generation...")
|
||||||
success = generator.generate_3d_animation(
|
# Generate frames
|
||||||
|
frame_paths = generator.generate_frames(
|
||||||
positions_file,
|
positions_file,
|
||||||
output_video,
|
|
||||||
style='google_earth',
|
style='google_earth',
|
||||||
progress_callback=progress_callback
|
progress_callback=progress_callback
|
||||||
)
|
)
|
||||||
|
|
||||||
if success and os.path.exists(output_video):
|
if frame_paths and len(frame_paths) > 0:
|
||||||
print(f"✅ SUCCESS! Google Earth flythrough created: {output_video}")
|
print(f"✅ Generated {len(frame_paths)} frames")
|
||||||
|
|
||||||
# Get file size
|
# Create video
|
||||||
file_size = os.path.getsize(output_video) / (1024 * 1024) # MB
|
success = generator.create_video(
|
||||||
print(f"📹 Video size: {file_size:.1f} MB")
|
frame_paths,
|
||||||
|
output_video,
|
||||||
|
progress_callback=progress_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
if success and os.path.exists(output_video):
|
||||||
|
print(f"✅ SUCCESS! Google Earth flythrough created: {output_video}")
|
||||||
|
|
||||||
|
# Get file size
|
||||||
|
file_size = os.path.getsize(output_video) / (1024 * 1024) # MB
|
||||||
|
print(f"📹 Video size: {file_size:.1f} MB")
|
||||||
|
|
||||||
|
# Clean up frames
|
||||||
|
generator.cleanup_frames()
|
||||||
|
else:
|
||||||
|
print("❌ Failed to create video from frames")
|
||||||
else:
|
else:
|
||||||
print("❌ Failed to create video")
|
print("❌ Failed to generate frames")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Error during generation: {e}")
|
print(f"❌ Error during generation: {e}")
|
||||||
|
|||||||
69
test_progressive_3d.py
Normal file
69
test_progressive_3d.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for progressive 3D animation function
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
sys.path.append('/home/pi/Desktop/traccar_animation')
|
||||||
|
|
||||||
|
def test_progressive_3d_animation():
|
||||||
|
"""Test the progressive 3D animation dependencies"""
|
||||||
|
|
||||||
|
print("Testing progressive 3D animation dependencies...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test matplotlib with 3D
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use('Agg') # Non-interactive backend
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from mpl_toolkits.mplot3d import Axes3D
|
||||||
|
print("✅ Matplotlib with 3D support available")
|
||||||
|
|
||||||
|
# Test OpenCV (instead of MoviePy)
|
||||||
|
import cv2
|
||||||
|
print("✅ OpenCV available for video creation")
|
||||||
|
|
||||||
|
# Test numpy
|
||||||
|
import numpy as np
|
||||||
|
print("✅ NumPy available")
|
||||||
|
|
||||||
|
# Test basic 3D plot creation
|
||||||
|
fig = plt.figure(figsize=(8, 6))
|
||||||
|
ax = fig.add_subplot(111, projection='3d')
|
||||||
|
|
||||||
|
# Create simple test data
|
||||||
|
x = np.array([0, 1, 2, 3, 4])
|
||||||
|
y = np.array([0, 1, 0, 1, 0])
|
||||||
|
z = np.array([0, 0, 1, 1, 2])
|
||||||
|
|
||||||
|
ax.plot(x, y, z, 'b-', linewidth=2)
|
||||||
|
ax.scatter(x, y, z, c='red', s=50)
|
||||||
|
ax.set_xlabel('X')
|
||||||
|
ax.set_ylabel('Y')
|
||||||
|
ax.set_zlabel('Z')
|
||||||
|
ax.set_title('Test 3D Plot')
|
||||||
|
|
||||||
|
# Save test plot
|
||||||
|
test_path = '/tmp/test_3d_plot.png'
|
||||||
|
plt.savefig(test_path, dpi=100, bbox_inches='tight')
|
||||||
|
plt.close(fig)
|
||||||
|
|
||||||
|
if os.path.exists(test_path):
|
||||||
|
print("✅ 3D plot creation and saving works")
|
||||||
|
os.remove(test_path)
|
||||||
|
else:
|
||||||
|
print("❌ Failed to create 3D plot")
|
||||||
|
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"❌ Import error: {e}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("🎉 All dependencies for progressive 3D animation are working!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_progressive_3d_animation()
|
||||||
202
test_progressive_debug.py
Normal file
202
test_progressive_debug.py
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for progressive 3D animation with debugging
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
sys.path.append('/home/pi/Desktop/traccar_animation')
|
||||||
|
|
||||||
|
def test_progressive_animation_debug():
|
||||||
|
"""Test the progressive animation with a simple GPS dataset"""
|
||||||
|
|
||||||
|
print("Testing progressive 3D animation with debug output...")
|
||||||
|
|
||||||
|
# Find a project with GPS data
|
||||||
|
projects_folder = "/home/pi/Desktop/traccar_animation/resources/projects"
|
||||||
|
|
||||||
|
if not os.path.exists(projects_folder):
|
||||||
|
print("❌ Projects folder not found!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Look for projects
|
||||||
|
projects = [d for d in os.listdir(projects_folder) if os.path.isdir(os.path.join(projects_folder, d))]
|
||||||
|
|
||||||
|
if not projects:
|
||||||
|
print("❌ No projects found!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Use the first project found
|
||||||
|
project_name = projects[0]
|
||||||
|
project_folder = os.path.join(projects_folder, project_name)
|
||||||
|
positions_file = os.path.join(project_folder, "positions.json")
|
||||||
|
|
||||||
|
if not os.path.exists(positions_file):
|
||||||
|
print(f"❌ No positions.json found in project {project_name}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"✅ Testing with project: {project_name}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Import the animation generation code
|
||||||
|
import json
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use('Agg')
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from mpl_toolkits.mplot3d import Axes3D
|
||||||
|
import numpy as np
|
||||||
|
import cv2
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Load GPS data
|
||||||
|
with open(positions_file, 'r') as f:
|
||||||
|
positions = json.load(f)
|
||||||
|
|
||||||
|
print(f"✅ Loaded {len(positions)} GPS points")
|
||||||
|
|
||||||
|
if len(positions) < 2:
|
||||||
|
print("❌ Need at least 2 GPS points")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Test creating just 3 frames
|
||||||
|
test_frames_folder = os.path.join(project_folder, "test_frames")
|
||||||
|
os.makedirs(test_frames_folder, exist_ok=True)
|
||||||
|
|
||||||
|
# Extract coordinates
|
||||||
|
lats = np.array([pos['latitude'] for pos in positions[:10]]) # Just first 10 points
|
||||||
|
lons = np.array([pos['longitude'] for pos in positions[:10]])
|
||||||
|
alts = np.array([pos.get('altitude', 0) for pos in positions[:10]])
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
print(f"✅ Processed coordinates: x={x.min():.1f} to {x.max():.1f}, y={y.min():.1f} to {y.max():.1f}")
|
||||||
|
|
||||||
|
frame_files = []
|
||||||
|
|
||||||
|
# Create 3 test frames
|
||||||
|
for frame_idx in range(3):
|
||||||
|
end_point = (frame_idx + 1) * 3
|
||||||
|
end_point = min(end_point, len(x))
|
||||||
|
|
||||||
|
print(f"Creating frame {frame_idx + 1}, showing {end_point} points...")
|
||||||
|
|
||||||
|
# Create 3D plot
|
||||||
|
fig = plt.figure(figsize=(10, 8), dpi=100)
|
||||||
|
ax = fig.add_subplot(111, projection='3d')
|
||||||
|
|
||||||
|
# Plot route up to current point
|
||||||
|
if end_point > 1:
|
||||||
|
ax.plot(x[:end_point], y[:end_point], z[:end_point], 'b-', linewidth=2)
|
||||||
|
ax.scatter(x[:end_point], y[:end_point], z[:end_point], c='blue', s=20)
|
||||||
|
|
||||||
|
# Current position
|
||||||
|
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')
|
||||||
|
|
||||||
|
# Remaining route
|
||||||
|
if end_point < len(x):
|
||||||
|
ax.plot(x[end_point:], y[end_point:], z[end_point:],
|
||||||
|
'lightgray', linewidth=1, alpha=0.3)
|
||||||
|
|
||||||
|
ax.set_xlabel('East-West (m)')
|
||||||
|
ax.set_ylabel('North-South (m)')
|
||||||
|
ax.set_zlabel('Elevation (m)')
|
||||||
|
ax.set_title(f'Test Frame {frame_idx + 1} - Point {end_point}/{len(x)}')
|
||||||
|
|
||||||
|
# Set view
|
||||||
|
margin = max(np.ptp(x), np.ptp(y)) * 0.1 if np.ptp(x) > 0 else 100
|
||||||
|
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)
|
||||||
|
ax.view_init(elev=20, azim=45)
|
||||||
|
ax.grid(True)
|
||||||
|
|
||||||
|
# Save frame
|
||||||
|
frame_path = os.path.join(test_frames_folder, f"test_frame_{frame_idx:03d}.png")
|
||||||
|
try:
|
||||||
|
plt.savefig(frame_path, dpi=100, bbox_inches='tight',
|
||||||
|
facecolor='white', edgecolor='none', format='png')
|
||||||
|
plt.close(fig)
|
||||||
|
|
||||||
|
# Check frame
|
||||||
|
if os.path.exists(frame_path):
|
||||||
|
file_size = os.path.getsize(frame_path)
|
||||||
|
print(f"✅ Frame {frame_idx + 1} saved: {file_size} bytes")
|
||||||
|
|
||||||
|
# Test OpenCV reading
|
||||||
|
test_img = cv2.imread(frame_path)
|
||||||
|
if test_img is not None:
|
||||||
|
h, w, c = test_img.shape
|
||||||
|
print(f"✅ Frame {frame_idx + 1} readable by OpenCV: {w}x{h}")
|
||||||
|
frame_files.append(frame_path)
|
||||||
|
else:
|
||||||
|
print(f"❌ Frame {frame_idx + 1} not readable by OpenCV")
|
||||||
|
else:
|
||||||
|
print(f"❌ Frame {frame_idx + 1} not created")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error creating frame {frame_idx + 1}: {e}")
|
||||||
|
plt.close(fig)
|
||||||
|
|
||||||
|
print(f"Created {len(frame_files)} valid frames")
|
||||||
|
|
||||||
|
# Test video creation
|
||||||
|
if frame_files:
|
||||||
|
output_video = os.path.join(project_folder, f"test_progressive_{datetime.now().strftime('%H%M%S')}.mp4")
|
||||||
|
|
||||||
|
# Read first frame for dimensions
|
||||||
|
first_frame = cv2.imread(frame_files[0])
|
||||||
|
height, width, layers = first_frame.shape
|
||||||
|
|
||||||
|
print(f"Video dimensions: {width}x{height}")
|
||||||
|
|
||||||
|
# Create video
|
||||||
|
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
||||||
|
video_writer = cv2.VideoWriter(output_video, fourcc, 2.0, (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)
|
||||||
|
print(f"✅ Added frame {i+1} to video")
|
||||||
|
else:
|
||||||
|
print(f"❌ Could not read frame {i+1}")
|
||||||
|
|
||||||
|
video_writer.release()
|
||||||
|
|
||||||
|
if os.path.exists(output_video):
|
||||||
|
file_size = os.path.getsize(output_video)
|
||||||
|
print(f"✅ Video created: {output_video} ({file_size} bytes)")
|
||||||
|
else:
|
||||||
|
print("❌ Video file not created")
|
||||||
|
else:
|
||||||
|
print("❌ Could not open video writer")
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
for frame_file in frame_files:
|
||||||
|
try:
|
||||||
|
os.remove(frame_file)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
os.rmdir(test_frames_folder)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_progressive_animation_debug()
|
||||||
126
test_video_creation.py
Normal file
126
test_video_creation.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for video creation functionality
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
sys.path.append('/home/pi/Desktop/traccar_animation')
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use('Agg')
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
def test_video_creation():
|
||||||
|
"""Test video creation with sample frames"""
|
||||||
|
|
||||||
|
print("Testing video creation functionality...")
|
||||||
|
|
||||||
|
# Create test directory
|
||||||
|
test_dir = "/tmp/video_test"
|
||||||
|
os.makedirs(test_dir, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create sample frames
|
||||||
|
frame_files = []
|
||||||
|
for i in range(10):
|
||||||
|
# Create a simple test plot
|
||||||
|
fig, ax = plt.subplots(figsize=(8, 6))
|
||||||
|
|
||||||
|
# Simple animation - moving dot
|
||||||
|
x = np.linspace(0, 10, 100)
|
||||||
|
y = np.sin(x + i * 0.5)
|
||||||
|
|
||||||
|
ax.plot(x, y, 'b-', linewidth=2)
|
||||||
|
ax.scatter([i], [np.sin(i * 0.5)], c='red', s=100)
|
||||||
|
ax.set_xlim(0, 10)
|
||||||
|
ax.set_ylim(-2, 2)
|
||||||
|
ax.set_title(f'Test Frame {i+1}/10')
|
||||||
|
ax.grid(True)
|
||||||
|
|
||||||
|
# Save frame
|
||||||
|
frame_path = os.path.join(test_dir, f"frame_{i:03d}.png")
|
||||||
|
plt.savefig(frame_path, dpi=100, bbox_inches='tight')
|
||||||
|
plt.close(fig)
|
||||||
|
|
||||||
|
frame_files.append(frame_path)
|
||||||
|
print(f"Created frame {i+1}/10")
|
||||||
|
|
||||||
|
print(f"Created {len(frame_files)} test frames")
|
||||||
|
|
||||||
|
# Test video creation with different codecs
|
||||||
|
codecs_to_test = [
|
||||||
|
('mp4v', '.mp4'),
|
||||||
|
('XVID', '.avi'),
|
||||||
|
('MJPG', '.avi')
|
||||||
|
]
|
||||||
|
|
||||||
|
for codec, ext in codecs_to_test:
|
||||||
|
try:
|
||||||
|
output_path = os.path.join(test_dir, f"test_video_{codec}{ext}")
|
||||||
|
|
||||||
|
# Read first frame for dimensions
|
||||||
|
first_frame = cv2.imread(frame_files[0])
|
||||||
|
if first_frame is None:
|
||||||
|
print(f"❌ Could not read first frame")
|
||||||
|
continue
|
||||||
|
|
||||||
|
height, width, layers = first_frame.shape
|
||||||
|
print(f"Frame dimensions: {width}x{height}")
|
||||||
|
|
||||||
|
# Create video writer
|
||||||
|
fourcc = cv2.VideoWriter_fourcc(*codec)
|
||||||
|
video_writer = cv2.VideoWriter(output_path, fourcc, 5.0, (width, height))
|
||||||
|
|
||||||
|
if not video_writer.isOpened():
|
||||||
|
print(f"❌ Could not open video writer with {codec}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Write frames
|
||||||
|
frames_written = 0
|
||||||
|
for frame_file in frame_files:
|
||||||
|
frame = cv2.imread(frame_file)
|
||||||
|
if frame is not None:
|
||||||
|
video_writer.write(frame)
|
||||||
|
frames_written += 1
|
||||||
|
|
||||||
|
video_writer.release()
|
||||||
|
|
||||||
|
# Check result
|
||||||
|
if os.path.exists(output_path):
|
||||||
|
file_size = os.path.getsize(output_path)
|
||||||
|
if file_size > 1024: # At least 1KB
|
||||||
|
print(f"✅ {codec} video created: {output_path} ({file_size} bytes, {frames_written} frames)")
|
||||||
|
else:
|
||||||
|
print(f"❌ {codec} video too small: {file_size} bytes")
|
||||||
|
else:
|
||||||
|
print(f"❌ {codec} video not created")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error with {codec}: {e}")
|
||||||
|
|
||||||
|
# Check OpenCV version and capabilities
|
||||||
|
print(f"\nOpenCV version: {cv2.__version__}")
|
||||||
|
print(f"OpenCV build info available: {hasattr(cv2, 'getBuildInformation')}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Clean up
|
||||||
|
for frame_file in frame_files:
|
||||||
|
try:
|
||||||
|
os.remove(frame_file)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
os.rmdir(test_dir)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_video_creation()
|
||||||
79
traccar.kv
79
traccar.kv
@@ -722,7 +722,7 @@
|
|||||||
height: 202
|
height: 202
|
||||||
size_hint_x: 1
|
size_hint_x: 1
|
||||||
|
|
||||||
# 3D Video Animation frame
|
# Progressive 3D Animation frame
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
orientation: "horizontal"
|
orientation: "horizontal"
|
||||||
size_hint_y: None
|
size_hint_y: None
|
||||||
@@ -737,7 +737,7 @@
|
|||||||
size: self.size
|
size: self.size
|
||||||
|
|
||||||
Label:
|
Label:
|
||||||
text: "Generate 3D video animation\nsimilar to Relive style"
|
text: "Generate progressive 3D animation\nBuilds trip point by point"
|
||||||
font_size: 16
|
font_size: 16
|
||||||
color: 1, 1, 1, 1
|
color: 1, 1, 1, 1
|
||||||
size_hint_x: 0.7
|
size_hint_x: 0.7
|
||||||
@@ -746,15 +746,84 @@
|
|||||||
text_size: self.size
|
text_size: self.size
|
||||||
|
|
||||||
Button:
|
Button:
|
||||||
text: "Generate\n3D Video"
|
text: "Generate\n3D Trip"
|
||||||
size_hint_x: 0.3
|
size_hint_x: 0.3
|
||||||
font_size: 14
|
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
|
color: 1, 1, 1, 1
|
||||||
halign: "center"
|
halign: "center"
|
||||||
valign: "middle"
|
valign: "middle"
|
||||||
text_size: self.size
|
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:
|
Widget:
|
||||||
|
|||||||
Reference in New Issue
Block a user