Compare commits
5 Commits
4fa7ed2a48
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7df9de12ce | |||
| 9f8c1c27dc | |||
| 1d0dc05a7b | |||
| 911143dfc5 | |||
| 29fd68f732 |
37
.gitignore
vendored
37
.gitignore
vendored
@@ -1,2 +1,39 @@
|
|||||||
# Ignore the virtual environment folder
|
# Ignore the virtual environment folder
|
||||||
track/
|
track/
|
||||||
|
|
||||||
|
# Ignore Python cache files
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
|
||||||
|
# Ignore project data and generated files
|
||||||
|
resources/projects/
|
||||||
|
resources/trip_archive/
|
||||||
|
resources/credentials.enc
|
||||||
|
resources/key.key
|
||||||
|
resources/server_settings.enc
|
||||||
|
|
||||||
|
# Ignore generated videos and frames
|
||||||
|
*.mp4
|
||||||
|
*.avi
|
||||||
|
*.mov
|
||||||
|
*.webm
|
||||||
|
cinema_frames/
|
||||||
|
progressive_frames/
|
||||||
|
|
||||||
|
# Ignore test files and temporary files
|
||||||
|
test_*.py
|
||||||
|
*.tmp
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Ignore IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Ignore OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
# Project Cleanup Summary
|
|
||||||
|
|
||||||
## What Was Cleaned Up
|
|
||||||
|
|
||||||
### Moved to `junk_files/`
|
|
||||||
- Documentation files (*.md) that were cluttering the root directory
|
|
||||||
- `3D_VIDEO_DOCUMENTATION.md`
|
|
||||||
- `PAUSE_EDIT_IMPROVEMENTS.md`
|
|
||||||
- `PROJECT_MODERNIZATION_SUMMARY.md`
|
|
||||||
- `TEST_MODE_DOCUMENTATION.md`
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
- All `__pycache__` directories and compiled Python bytecode files
|
|
||||||
- Duplicate and test files that were no longer needed
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Fixed typo in requirements.txt (`reqirements.txt` was corrected to `requirements.txt`)
|
|
||||||
- Ensured proper import structure (app uses `py_scripts.video_3d_generator` correctly)
|
|
||||||
|
|
||||||
## Current Clean Structure
|
|
||||||
```
|
|
||||||
traccar_animation/
|
|
||||||
├── .git/ # Git repository files
|
|
||||||
├── .gitignore # Git ignore rules
|
|
||||||
├── config.py # Application configuration
|
|
||||||
├── main.py # Main application entry point
|
|
||||||
├── traccar.kv # Kivy UI layout file
|
|
||||||
├── requirements.txt # Python dependencies (fixed)
|
|
||||||
├── py_scripts/ # Python modules
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── utils.py
|
|
||||||
│ ├── video_3d_generator.py
|
|
||||||
│ └── webview.py
|
|
||||||
├── screens/ # Kivy screen modules
|
|
||||||
├── resources/ # Application resources
|
|
||||||
├── track/ # Virtual environment
|
|
||||||
└── junk_files/ # Non-essential files moved here
|
|
||||||
```
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
- ✅ Utils module imports correctly
|
|
||||||
- ✅ Video 3D generator module imports correctly
|
|
||||||
- ✅ No duplicate files remain
|
|
||||||
- ✅ All dependencies properly listed in requirements.txt
|
|
||||||
- ✅ Clean project structure maintained
|
|
||||||
Binary file not shown.
@@ -1,41 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Complete video generation from existing frames
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
import glob
|
|
||||||
from moviepy import ImageSequenceClip
|
|
||||||
|
|
||||||
def create_video_from_frames():
|
|
||||||
frames_folder = "/home/pi/Desktop/traccar_animation/resources/projects/day 2/frames"
|
|
||||||
output_path = "/home/pi/Desktop/traccar_animation/resources/projects/day 2/advanced_3d_animation.mp4"
|
|
||||||
|
|
||||||
# Get all frame files
|
|
||||||
frame_files = glob.glob(os.path.join(frames_folder, "frame_*.png"))
|
|
||||||
frame_files.sort() # Ensure correct order
|
|
||||||
|
|
||||||
if not frame_files:
|
|
||||||
print("No frames found!")
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"Found {len(frame_files)} frames")
|
|
||||||
print("Creating video...")
|
|
||||||
|
|
||||||
# Create video clip
|
|
||||||
clip = ImageSequenceClip(frame_files, fps=30)
|
|
||||||
|
|
||||||
# Write video file
|
|
||||||
clip.write_videofile(
|
|
||||||
output_path,
|
|
||||||
codec='libx264',
|
|
||||||
bitrate='8000k',
|
|
||||||
audio=False,
|
|
||||||
temp_audiofile=None,
|
|
||||||
remove_temp=True
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"Video created successfully: {output_path}")
|
|
||||||
return output_path
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
create_video_from_frames()
|
|
||||||
@@ -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.
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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")
|
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ class BlenderGPSAnimator:
|
|||||||
if BLENDER_AVAILABLE:
|
if BLENDER_AVAILABLE:
|
||||||
self.setup_blender_scene()
|
self.setup_blender_scene()
|
||||||
else:
|
else:
|
||||||
raise ImportError("Blender (bpy) is not available. Please install Blender with Python API access.")
|
# Don't raise error here, let the caller handle the check
|
||||||
|
pass
|
||||||
|
|
||||||
def check_dependencies(self):
|
def check_dependencies(self):
|
||||||
"""Check if Blender dependencies are available"""
|
"""Check if Blender dependencies are available"""
|
||||||
|
|||||||
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 +0,0 @@
|
|||||||
gAAAAABobmx0PnGbcR3Hxn93Z2r3z0dqZpHYGfWhJC7ko6QSMHLY_qoGsEZLrlLjjGrdjVOqSNVfwCP6_pAQ5QWbDRs6RoyZFPIA-vLFYpU9tUVC6pHCSSxvQimS_Thdj5WMIBlpTOWa
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
wetp_PNG9CC5432-W9H3rUbaqIurwldZxHlOgori5kY=
|
|
||||||
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
@@ -1 +0,0 @@
|
|||||||
gAAAAABobK2fcNGeWyfPJzYnOl_HWl8TdQfRDb5teUXH9Kpjmme0TUVA3Dy7wm2MuMEGsPBTWBm8XfaX8daIwu6iDV6o8G07XZ_A0RoMqx3xWiYUbX63ovYy8qITIpMqbt0dayYigDSPmdr_8pcqko6ik-ctfdg4SkGH1gRXb5yuacnzezLr3KcHMh833PkbTO6WiUYPCwaivEMTVHUxL5YORiLRGu4E3lS_WDPo7kv53khtUI9b7vWJOOUFXcelM2vF3iHI3EkXCWrO2Qpm22nC44b-yCnZvYzx7g-WHZDNfG6CA1KXbcyhouxR4b7502iofpEAN5sizLFuyOWIOBdVphblIkRd1qdq6fVmt0IMeoaMpNPNuDKJqMDLuAU05wXDWbGXei6YU6rs6YJgpGOfNdv8A_sKKJBrh5QVE2kZ2GE0Ysqpnw2Yfj_jsMBpdh-bBs6UDwcI
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
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()
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
#!/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()
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test script for the improved Relive-style GPS animation
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# Add the project directory to the path
|
|
||||||
sys.path.append('/home/pi/Desktop/traccar_animation')
|
|
||||||
|
|
||||||
from py_scripts.advanced_3d_generator import Advanced3DGenerator
|
|
||||||
|
|
||||||
def test_relive_animation():
|
|
||||||
"""Test the new Relive-style animation"""
|
|
||||||
|
|
||||||
# Find a project with GPS data
|
|
||||||
resources_folder = "/home/pi/Desktop/traccar_animation/resources"
|
|
||||||
projects_folder = os.path.join(resources_folder, "projects")
|
|
||||||
|
|
||||||
if not os.path.exists(projects_folder):
|
|
||||||
print("No projects folder found")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Look for projects with positions.json
|
|
||||||
for project_name in os.listdir(projects_folder):
|
|
||||||
project_path = os.path.join(projects_folder, project_name)
|
|
||||||
positions_file = os.path.join(project_path, "positions.json")
|
|
||||||
|
|
||||||
if os.path.exists(positions_file):
|
|
||||||
print(f"🎬 Testing Relive-style animation with project: {project_name}")
|
|
||||||
|
|
||||||
# Check if positions file has data
|
|
||||||
try:
|
|
||||||
with open(positions_file, 'r') as f:
|
|
||||||
positions = json.load(f)
|
|
||||||
|
|
||||||
if len(positions) < 5:
|
|
||||||
print(f"❌ Project {project_name} has only {len(positions)} GPS points - skipping")
|
|
||||||
continue
|
|
||||||
|
|
||||||
print(f"📍 Found {len(positions)} GPS points")
|
|
||||||
|
|
||||||
# Create generator
|
|
||||||
generator = Advanced3DGenerator(project_path)
|
|
||||||
|
|
||||||
# Progress callback
|
|
||||||
def progress_callback(progress, message):
|
|
||||||
print(f"Progress: {progress:.1f}% - {message}")
|
|
||||||
|
|
||||||
# Generate animation
|
|
||||||
output_video = os.path.join(project_path, f"relive_animation_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
|
|
||||||
|
|
||||||
print(f"🚀 Starting Relive-style animation generation...")
|
|
||||||
success = generator.generate_3d_animation(
|
|
||||||
positions_file,
|
|
||||||
output_video,
|
|
||||||
style='advanced',
|
|
||||||
progress_callback=progress_callback
|
|
||||||
)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
print(f"✅ SUCCESS! Relive-style animation created: {output_video}")
|
|
||||||
print(f"📁 You can find your video at: {output_video}")
|
|
||||||
else:
|
|
||||||
print("❌ Failed to generate animation")
|
|
||||||
|
|
||||||
return # Exit after first successful project
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Error testing project {project_name}: {e}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
print("❌ No suitable projects found for testing")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print("🎬 Testing Improved Relive-Style GPS Animation")
|
|
||||||
print("=" * 50)
|
|
||||||
test_relive_animation()
|
|
||||||
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