Compare commits
7 Commits
507f526433
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7df9de12ce | |||
| 9f8c1c27dc | |||
| 1d0dc05a7b | |||
| 911143dfc5 | |||
| 29fd68f732 | |||
| 4fa7ed2a48 | |||
| 35d3bb8442 |
39
.gitignore
vendored
39
.gitignore
vendored
@@ -1,2 +1,39 @@
|
||||
# Ignore the virtual environment folder
|
||||
track/
|
||||
track/
|
||||
|
||||
# Ignore Python cache files
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
|
||||
# Ignore project data and generated files
|
||||
resources/projects/
|
||||
resources/trip_archive/
|
||||
resources/credentials.enc
|
||||
resources/key.key
|
||||
resources/server_settings.enc
|
||||
|
||||
# Ignore generated videos and frames
|
||||
*.mp4
|
||||
*.avi
|
||||
*.mov
|
||||
*.webm
|
||||
cinema_frames/
|
||||
progressive_frames/
|
||||
|
||||
# Ignore test files and temporary files
|
||||
test_*.py
|
||||
*.tmp
|
||||
*.log
|
||||
|
||||
# Ignore IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Ignore OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
@@ -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,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.
1232
py_scripts/advanced_3d_generator.py
Normal file
1232
py_scripts/advanced_3d_generator.py
Normal file
File diff suppressed because it is too large
Load Diff
333
py_scripts/blender_animator.py
Normal file
333
py_scripts/blender_animator.py
Normal file
@@ -0,0 +1,333 @@
|
||||
import json
|
||||
import numpy as np
|
||||
import os
|
||||
from datetime import datetime
|
||||
import math
|
||||
|
||||
# Blender dependencies with fallback handling
|
||||
try:
|
||||
import bpy
|
||||
import bmesh
|
||||
from mathutils import Vector, Euler
|
||||
BLENDER_AVAILABLE = True
|
||||
except ImportError:
|
||||
BLENDER_AVAILABLE = False
|
||||
print("Warning: Blender (bpy) not available. This module requires Blender to be installed with Python API access.")
|
||||
|
||||
class BlenderGPSAnimator:
|
||||
"""
|
||||
Advanced GPS track animation using Blender for high-quality 3D rendering
|
||||
"""
|
||||
|
||||
def __init__(self, output_folder):
|
||||
self.output_folder = output_folder
|
||||
if BLENDER_AVAILABLE:
|
||||
self.setup_blender_scene()
|
||||
else:
|
||||
# Don't raise error here, let the caller handle the check
|
||||
pass
|
||||
|
||||
def check_dependencies(self):
|
||||
"""Check if Blender dependencies are available"""
|
||||
if not BLENDER_AVAILABLE:
|
||||
raise ImportError("Blender (bpy) is not available. Please install Blender with Python API access.")
|
||||
return True
|
||||
|
||||
def setup_blender_scene(self):
|
||||
"""Setup Blender scene for GPS animation"""
|
||||
# Clear existing mesh objects
|
||||
bpy.ops.object.select_all(action='SELECT')
|
||||
bpy.ops.object.delete(use_global=False)
|
||||
|
||||
# Add camera
|
||||
bpy.ops.object.camera_add(location=(0, 0, 10))
|
||||
self.camera = bpy.context.object
|
||||
|
||||
# Add sun light
|
||||
bpy.ops.object.light_add(type='SUN', location=(0, 0, 20))
|
||||
light = bpy.context.object
|
||||
light.data.energy = 5
|
||||
|
||||
# Setup world environment
|
||||
world = bpy.context.scene.world
|
||||
world.use_nodes = True
|
||||
env_texture = world.node_tree.nodes.new('ShaderNodeTexEnvironment')
|
||||
world.node_tree.links.new(env_texture.outputs[0], world.node_tree.nodes['Background'].inputs[0])
|
||||
|
||||
# Set render settings
|
||||
scene = bpy.context.scene
|
||||
scene.render.engine = 'CYCLES'
|
||||
scene.render.resolution_x = 1920
|
||||
scene.render.resolution_y = 1080
|
||||
scene.render.fps = 30
|
||||
scene.cycles.samples = 64
|
||||
|
||||
def load_gps_data(self, positions_file):
|
||||
"""Load GPS data from JSON file"""
|
||||
with open(positions_file, 'r') as f:
|
||||
positions = json.load(f)
|
||||
|
||||
# Convert to numpy array for easier processing
|
||||
coords = []
|
||||
times = []
|
||||
speeds = []
|
||||
|
||||
for pos in positions:
|
||||
coords.append([pos['longitude'], pos['latitude'], pos.get('altitude', 0)])
|
||||
times.append(pos['fixTime'])
|
||||
speeds.append(pos.get('speed', 0) * 1.852) # Convert to km/h
|
||||
|
||||
return np.array(coords), times, speeds
|
||||
|
||||
def create_terrain_mesh(self, coords):
|
||||
"""Create a simple terrain mesh based on GPS bounds"""
|
||||
# Calculate bounds
|
||||
min_lon, min_lat = coords[:, :2].min(axis=0)
|
||||
max_lon, max_lat = coords[:, :2].max(axis=0)
|
||||
|
||||
# Expand bounds slightly
|
||||
padding = 0.001
|
||||
min_lon -= padding
|
||||
min_lat -= padding
|
||||
max_lon += padding
|
||||
max_lat += padding
|
||||
|
||||
# Create terrain mesh
|
||||
bpy.ops.mesh.primitive_plane_add(size=2, location=(0, 0, 0))
|
||||
terrain = bpy.context.object
|
||||
terrain.name = "Terrain"
|
||||
|
||||
# Scale terrain to match GPS bounds
|
||||
lon_range = max_lon - min_lon
|
||||
lat_range = max_lat - min_lat
|
||||
scale_factor = max(lon_range, lat_range) * 100000 # Convert to reasonable scale
|
||||
|
||||
terrain.scale = (scale_factor, scale_factor, 1)
|
||||
|
||||
# Apply material
|
||||
mat = bpy.data.materials.new(name="TerrainMaterial")
|
||||
mat.use_nodes = True
|
||||
mat.node_tree.nodes.clear()
|
||||
|
||||
# Add principled BSDF
|
||||
bsdf = mat.node_tree.nodes.new(type='ShaderNodeBsdfPrincipled')
|
||||
bsdf.inputs['Base Color'].default_value = (0.2, 0.5, 0.2, 1.0) # Green
|
||||
bsdf.inputs['Roughness'].default_value = 0.8
|
||||
|
||||
material_output = mat.node_tree.nodes.new(type='ShaderNodeOutputMaterial')
|
||||
mat.node_tree.links.new(bsdf.outputs['BSDF'], material_output.inputs['Surface'])
|
||||
|
||||
terrain.data.materials.append(mat)
|
||||
|
||||
return terrain
|
||||
|
||||
def create_gps_track_mesh(self, coords):
|
||||
"""Create a 3D mesh for the GPS track"""
|
||||
# Normalize coordinates to Blender scale
|
||||
coords_normalized = self.normalize_coordinates(coords)
|
||||
|
||||
# Create curve from GPS points
|
||||
curve_data = bpy.data.curves.new('GPSTrack', type='CURVE')
|
||||
curve_data.dimensions = '3D'
|
||||
curve_data.bevel_depth = 0.02
|
||||
curve_data.bevel_resolution = 4
|
||||
|
||||
# Create spline
|
||||
spline = curve_data.splines.new('BEZIER')
|
||||
spline.bezier_points.add(len(coords_normalized) - 1)
|
||||
|
||||
for i, coord in enumerate(coords_normalized):
|
||||
point = spline.bezier_points[i]
|
||||
point.co = coord
|
||||
point.handle_left_type = 'AUTO'
|
||||
point.handle_right_type = 'AUTO'
|
||||
|
||||
# Create object from curve
|
||||
track_obj = bpy.data.objects.new('GPSTrack', curve_data)
|
||||
bpy.context.collection.objects.link(track_obj)
|
||||
|
||||
# Apply material
|
||||
mat = bpy.data.materials.new(name="TrackMaterial")
|
||||
mat.use_nodes = True
|
||||
bsdf = mat.node_tree.nodes["Principled BSDF"]
|
||||
bsdf.inputs['Base Color'].default_value = (1.0, 0.0, 0.0, 1.0) # Red
|
||||
bsdf.inputs['Emission'].default_value = (1.0, 0.2, 0.2, 1.0)
|
||||
bsdf.inputs['Emission Strength'].default_value = 2.0
|
||||
|
||||
track_obj.data.materials.append(mat)
|
||||
|
||||
return track_obj
|
||||
|
||||
def create_vehicle_model(self):
|
||||
"""Create a simple vehicle model"""
|
||||
# Create a simple car shape using cubes
|
||||
bpy.ops.mesh.primitive_cube_add(size=0.1, location=(0, 0, 0.05))
|
||||
vehicle = bpy.context.object
|
||||
vehicle.name = "Vehicle"
|
||||
vehicle.scale = (2, 1, 0.5)
|
||||
|
||||
# Apply material
|
||||
mat = bpy.data.materials.new(name="VehicleMaterial")
|
||||
mat.use_nodes = True
|
||||
bsdf = mat.node_tree.nodes["Principled BSDF"]
|
||||
bsdf.inputs['Base Color'].default_value = (0.0, 0.0, 1.0, 1.0) # Blue
|
||||
bsdf.inputs['Metallic'].default_value = 0.5
|
||||
bsdf.inputs['Roughness'].default_value = 0.2
|
||||
|
||||
vehicle.data.materials.append(mat)
|
||||
|
||||
return vehicle
|
||||
|
||||
def normalize_coordinates(self, coords):
|
||||
"""Normalize GPS coordinates to Blender scale"""
|
||||
# Center coordinates
|
||||
center = coords.mean(axis=0)
|
||||
coords_centered = coords - center
|
||||
|
||||
# Scale to reasonable size for Blender
|
||||
scale_factor = 100
|
||||
coords_scaled = coords_centered * scale_factor
|
||||
|
||||
# Convert to Vector objects
|
||||
return [Vector((x, y, z)) for x, y, z in coords_scaled]
|
||||
|
||||
def animate_vehicle(self, vehicle, coords, times, speeds):
|
||||
"""Create animation keyframes for vehicle movement"""
|
||||
coords_normalized = self.normalize_coordinates(coords)
|
||||
|
||||
scene = bpy.context.scene
|
||||
scene.frame_start = 1
|
||||
scene.frame_end = len(coords_normalized) * 2 # 2 frames per GPS point
|
||||
|
||||
for i, (coord, speed) in enumerate(zip(coords_normalized, speeds)):
|
||||
frame = i * 2 + 1
|
||||
|
||||
# Set location
|
||||
vehicle.location = coord
|
||||
vehicle.keyframe_insert(data_path="location", frame=frame)
|
||||
|
||||
# Calculate rotation based on direction
|
||||
if i < len(coords_normalized) - 1:
|
||||
next_coord = coords_normalized[i + 1]
|
||||
direction = next_coord - coord
|
||||
if direction.length > 0:
|
||||
direction.normalize()
|
||||
# Calculate rotation angle
|
||||
angle = math.atan2(direction.y, direction.x)
|
||||
vehicle.rotation_euler = Euler((0, 0, angle), 'XYZ')
|
||||
vehicle.keyframe_insert(data_path="rotation_euler", frame=frame)
|
||||
|
||||
# Set interpolation mode
|
||||
if vehicle.animation_data:
|
||||
for fcurve in vehicle.animation_data.action.fcurves:
|
||||
for keyframe in fcurve.keyframe_points:
|
||||
keyframe.interpolation = 'BEZIER'
|
||||
|
||||
def animate_camera(self, coords):
|
||||
"""Create smooth camera animation following the vehicle"""
|
||||
coords_normalized = self.normalize_coordinates(coords)
|
||||
|
||||
# Create camera path
|
||||
for i, coord in enumerate(coords_normalized):
|
||||
frame = i * 2 + 1
|
||||
|
||||
# Position camera above and behind the vehicle
|
||||
offset = Vector((0, -2, 3))
|
||||
cam_location = coord + offset
|
||||
|
||||
self.camera.location = cam_location
|
||||
self.camera.keyframe_insert(data_path="location", frame=frame)
|
||||
|
||||
# Look at the vehicle
|
||||
direction = coord - cam_location
|
||||
if direction.length > 0:
|
||||
rot_quat = direction.to_track_quat('-Z', 'Y')
|
||||
self.camera.rotation_euler = rot_quat.to_euler()
|
||||
self.camera.keyframe_insert(data_path="rotation_euler", frame=frame)
|
||||
|
||||
def add_particles_effects(self, vehicle):
|
||||
"""Add particle effects for enhanced visuals"""
|
||||
# Add dust particles
|
||||
bpy.context.view_layer.objects.active = vehicle
|
||||
bpy.ops.object.modifier_add(type='PARTICLE_SYSTEM')
|
||||
|
||||
particles = vehicle.modifiers["ParticleSystem"].particle_system
|
||||
particles.settings.count = 100
|
||||
particles.settings.lifetime = 30
|
||||
particles.settings.emit_from = 'FACE'
|
||||
particles.settings.physics_type = 'NEWTON'
|
||||
particles.settings.effector_weights.gravity = 0.1
|
||||
|
||||
# Set material for particles
|
||||
particles.settings.material = 1
|
||||
|
||||
def render_animation(self, output_path, progress_callback=None):
|
||||
"""Render the animation to video"""
|
||||
scene = bpy.context.scene
|
||||
|
||||
# Set output settings
|
||||
scene.render.filepath = output_path
|
||||
scene.render.image_settings.file_format = 'FFMPEG'
|
||||
scene.render.ffmpeg.format = 'MPEG4'
|
||||
scene.render.ffmpeg.codec = 'H264'
|
||||
|
||||
# Render animation
|
||||
total_frames = scene.frame_end - scene.frame_start + 1
|
||||
|
||||
for frame in range(scene.frame_start, scene.frame_end + 1):
|
||||
scene.frame_set(frame)
|
||||
|
||||
# Render frame
|
||||
frame_path = f"{output_path}_{frame:04d}.png"
|
||||
scene.render.filepath = frame_path
|
||||
bpy.ops.render.render(write_still=True)
|
||||
|
||||
# Update progress
|
||||
if progress_callback:
|
||||
progress = ((frame - scene.frame_start) / total_frames) * 100
|
||||
progress_callback(progress, f"Rendering frame {frame}/{scene.frame_end}")
|
||||
|
||||
def create_gps_animation(self, positions_file, output_path, progress_callback=None):
|
||||
"""Main method to create GPS animation in Blender"""
|
||||
try:
|
||||
# Load GPS data
|
||||
coords, times, speeds = self.load_gps_data(positions_file)
|
||||
|
||||
# Create scene elements
|
||||
terrain = self.create_terrain_mesh(coords)
|
||||
track = self.create_gps_track_mesh(coords)
|
||||
vehicle = self.create_vehicle_model()
|
||||
|
||||
# Create animations
|
||||
self.animate_vehicle(vehicle, coords, times, speeds)
|
||||
self.animate_camera(coords)
|
||||
|
||||
# Add effects
|
||||
self.add_particles_effects(vehicle)
|
||||
|
||||
# Render animation
|
||||
self.render_animation(output_path, progress_callback)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creating Blender animation: {e}")
|
||||
if progress_callback:
|
||||
progress_callback(-1, f"Error: {e}")
|
||||
return False
|
||||
|
||||
def generate_blender_animation(positions_file, output_folder, progress_callback=None):
|
||||
"""
|
||||
Convenience function to generate Blender animation
|
||||
"""
|
||||
animator = BlenderGPSAnimator(output_folder)
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
output_path = os.path.join(output_folder, f"blender_animation_{timestamp}")
|
||||
|
||||
success = animator.create_gps_animation(positions_file, output_path, progress_callback)
|
||||
|
||||
return f"{output_path}.mp4" if success else None
|
||||
|
||||
# Note: This script should be run from within Blender's Python environment
|
||||
# or with Blender as a Python module (bpy)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,10 +6,15 @@ selenium
|
||||
pillow
|
||||
geopy
|
||||
opencv-python
|
||||
moviepy
|
||||
requests
|
||||
numpy
|
||||
matplotlib
|
||||
scipy
|
||||
imageio
|
||||
ffmpeg-python
|
||||
ffmpeg-python
|
||||
pydeck
|
||||
plotly
|
||||
dash
|
||||
pandas
|
||||
geopandas
|
||||
bpy
|
||||
@@ -1 +0,0 @@
|
||||
gAAAAABobfofcr10BIPwspryfc740kIyIDl3sH0B0Jb598Zc9boEPMP01OyKqPXI1Dcfrqu6KGUI0useWSTQanKWBjCLNY-jQZmGKvbRRWL03bVhFl0i_5qUwgmLNHMSSXZi5U9oXFo7
|
||||
@@ -1 +0,0 @@
|
||||
wetp_PNG9CC5432-W9H3rUbaqIurwldZxHlOgori5kY=
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB |
@@ -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.
@@ -3,12 +3,14 @@ from kivy.uix.screenmanager import Screen
|
||||
import os
|
||||
import json
|
||||
import math
|
||||
from datetime import datetime
|
||||
from kivy.clock import Clock
|
||||
from kivy.properties import StringProperty, NumericProperty, AliasProperty
|
||||
from py_scripts.utils import (
|
||||
process_preview_util, optimize_route_entries_util
|
||||
)
|
||||
from py_scripts.video_3d_generator import generate_3d_video_animation
|
||||
from py_scripts.advanced_3d_generator import NavigationAnimationGenerator
|
||||
# BlenderGPSAnimator imported conditionally when needed
|
||||
from kivy.uix.popup import Popup
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.label import Label
|
||||
@@ -48,8 +50,6 @@ class CreateAnimationScreen(Screen):
|
||||
count = 0
|
||||
self.ids.route_entries_label.text = f"Your route has {count} entries,"
|
||||
|
||||
|
||||
|
||||
def open_rename_popup(self):
|
||||
from kivy.uix.popup import Popup
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
@@ -128,7 +128,6 @@ class CreateAnimationScreen(Screen):
|
||||
on_save=lambda: self.on_pre_enter()
|
||||
)
|
||||
|
||||
|
||||
def preview_route(self):
|
||||
# Show processing popup
|
||||
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||||
@@ -165,191 +164,778 @@ class CreateAnimationScreen(Screen):
|
||||
0.5
|
||||
)
|
||||
|
||||
def generate_google_earth_animation(self):
|
||||
"""Generate Google Earth-style flythrough animation using NavigationAnimationGenerator"""
|
||||
# Show processing popup
|
||||
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||||
label = Label(text="Initializing Google Earth flythrough...")
|
||||
progress = ProgressBar(max=100, value=0)
|
||||
layout.add_widget(label)
|
||||
layout.add_widget(progress)
|
||||
popup = Popup(
|
||||
title="Generating Google Earth Flythrough",
|
||||
content=layout,
|
||||
size_hint=(0.9, None),
|
||||
size=(0, 200),
|
||||
auto_dismiss=False
|
||||
)
|
||||
popup.open()
|
||||
|
||||
def run_google_earth_animation():
|
||||
try:
|
||||
# Update status
|
||||
def update_status(progress_val, status_text):
|
||||
def _update(dt):
|
||||
progress.value = progress_val
|
||||
label.text = status_text
|
||||
Clock.schedule_once(_update, 0)
|
||||
|
||||
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
|
||||
positions_path = os.path.join(project_folder, "positions.json")
|
||||
|
||||
if not os.path.exists(positions_path):
|
||||
update_status(0, "Error: No GPS data found")
|
||||
Clock.schedule_once(lambda dt: popup.dismiss(), 2)
|
||||
return
|
||||
|
||||
update_status(10, "Loading GPS data...")
|
||||
|
||||
# Check dependencies first
|
||||
generator = NavigationAnimationGenerator(project_folder)
|
||||
generator.check_dependencies()
|
||||
|
||||
update_status(20, "Processing GPS coordinates...")
|
||||
df = generator.load_gps_data(positions_path)
|
||||
|
||||
update_status(40, "Creating Google Earth flythrough...")
|
||||
output_video_path = os.path.join(project_folder, f"{self.project_name}_google_earth_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
|
||||
|
||||
# Progress callback for the generator
|
||||
def generator_progress(progress, message):
|
||||
update_status(40 + (progress * 0.5), message) # Map 0-100% to 40-90%
|
||||
|
||||
update_status(90, "Creating flythrough video...")
|
||||
success = generator.generate_frames(positions_path, style='google_earth', progress_callback=generator_progress)
|
||||
|
||||
if success and len(success) > 0:
|
||||
update_status(95, "Rendering final video...")
|
||||
video_success = generator.create_video(success, output_video_path, generator_progress)
|
||||
if video_success:
|
||||
update_status(100, "Google Earth flythrough complete!")
|
||||
output_path = output_video_path
|
||||
else:
|
||||
raise Exception("Failed to create video from frames")
|
||||
else:
|
||||
raise Exception("Failed to generate frames")
|
||||
|
||||
def show_success(dt):
|
||||
popup.dismiss()
|
||||
self.show_success_popup(
|
||||
"Google Earth Flythrough Complete!",
|
||||
f"Your Google Earth-style flythrough has been saved to:\n{output_path}",
|
||||
output_path
|
||||
)
|
||||
|
||||
Clock.schedule_once(show_success, 1)
|
||||
|
||||
except Exception as e:
|
||||
error_message = str(e)
|
||||
def show_error(dt):
|
||||
popup.dismiss()
|
||||
self.show_error_popup("Google Earth Animation Error", error_message)
|
||||
|
||||
Clock.schedule_once(show_error, 0)
|
||||
|
||||
# Schedule the animation generation
|
||||
Clock.schedule_once(lambda dt: run_google_earth_animation(), 0.5)
|
||||
|
||||
def generate_blender_animation(self):
|
||||
"""Generate cinema-quality animation using Blender (or fallback to advanced 3D)"""
|
||||
# Show processing popup
|
||||
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||||
label = Label(text="Initializing cinema rendering pipeline...")
|
||||
progress = ProgressBar(max=100, value=0)
|
||||
layout.add_widget(label)
|
||||
layout.add_widget(progress)
|
||||
popup = Popup(
|
||||
title="Generating Cinema Animation",
|
||||
content=layout,
|
||||
size_hint=(0.9, None),
|
||||
size=(0, 200),
|
||||
auto_dismiss=False
|
||||
)
|
||||
popup.open()
|
||||
|
||||
def run_blender_animation():
|
||||
try:
|
||||
# Update status
|
||||
def update_status(progress_val, status_text):
|
||||
def _update(dt):
|
||||
progress.value = progress_val
|
||||
label.text = status_text
|
||||
Clock.schedule_once(_update, 0)
|
||||
|
||||
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
|
||||
positions_path = os.path.join(project_folder, "positions.json")
|
||||
|
||||
if not os.path.exists(positions_path):
|
||||
update_status(0, "Error: No GPS data found")
|
||||
Clock.schedule_once(lambda dt: popup.dismiss(), 2)
|
||||
return
|
||||
|
||||
# Check if Blender is available
|
||||
try:
|
||||
from py_scripts.blender_animator import BLENDER_AVAILABLE, BlenderGPSAnimator
|
||||
if BLENDER_AVAILABLE:
|
||||
update_status(10, "Loading GPS data into Blender...")
|
||||
|
||||
# Use Blender for rendering
|
||||
animator = BlenderGPSAnimator(project_folder)
|
||||
animator.check_dependencies()
|
||||
|
||||
update_status(25, "Processing GPS coordinates...")
|
||||
gps_data = animator.load_gps_data(positions_path)
|
||||
|
||||
output_video_path = os.path.join(project_folder, f"{self.project_name}_blender_cinema_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
|
||||
|
||||
# Progress callback for the animator
|
||||
def animator_progress(progress, message):
|
||||
update_status(25 + (progress * 0.6), message) # Map 0-100% to 25-85%
|
||||
|
||||
update_status(85, "Rendering cinema-quality video...")
|
||||
success = animator.create_gps_animation(
|
||||
positions_path,
|
||||
output_video_path,
|
||||
progress_callback=animator_progress
|
||||
)
|
||||
|
||||
if success:
|
||||
update_status(100, "Blender cinema animation complete!")
|
||||
output_path = output_video_path
|
||||
else:
|
||||
raise Exception("Failed to generate Blender animation")
|
||||
|
||||
else:
|
||||
raise ImportError("Blender not available")
|
||||
|
||||
except ImportError:
|
||||
# Fallback to advanced 3D animation with cinema-quality settings
|
||||
update_status(10, "Blender not available - using advanced 3D cinema mode...")
|
||||
|
||||
# Import here to avoid startup delays
|
||||
import matplotlib
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
from mpl_toolkits.mplot3d import Axes3D
|
||||
import numpy as np
|
||||
import cv2
|
||||
|
||||
# Load GPS data
|
||||
with open(positions_path, 'r') as f:
|
||||
positions = json.load(f)
|
||||
|
||||
if len(positions) < 2:
|
||||
update_status(0, "Error: Need at least 2 GPS points")
|
||||
Clock.schedule_once(lambda dt: popup.dismiss(), 2)
|
||||
return
|
||||
|
||||
update_status(20, "Processing GPS coordinates for cinema rendering...")
|
||||
|
||||
# Extract coordinates
|
||||
lats = np.array([pos['latitude'] for pos in positions])
|
||||
lons = np.array([pos['longitude'] for pos in positions])
|
||||
alts = np.array([pos.get('altitude', 0) for pos in positions])
|
||||
timestamps = [pos.get('fixTime', '') for pos in positions]
|
||||
|
||||
# Convert to relative coordinates
|
||||
lat_center = np.mean(lats)
|
||||
lon_center = np.mean(lons)
|
||||
alt_min = np.min(alts)
|
||||
|
||||
x = (lons - lon_center) * 111320 * np.cos(np.radians(lat_center))
|
||||
y = (lats - lat_center) * 110540
|
||||
z = alts - alt_min
|
||||
|
||||
update_status(30, "Creating cinema-quality frames...")
|
||||
|
||||
# Cinema settings - higher quality
|
||||
frames_folder = os.path.join(project_folder, "cinema_frames")
|
||||
os.makedirs(frames_folder, exist_ok=True)
|
||||
|
||||
fps = 24 # Cinema standard
|
||||
total_frames = min(len(positions), 200) # Limit for reasonable processing time
|
||||
points_per_frame = max(1, len(positions) // total_frames)
|
||||
|
||||
frame_files = []
|
||||
|
||||
# Generate cinema-quality frames
|
||||
for frame_idx in range(total_frames):
|
||||
current_progress = 30 + (frame_idx / total_frames) * 50
|
||||
update_status(current_progress, f"Rendering cinema frame {frame_idx + 1}/{total_frames}...")
|
||||
|
||||
end_point = min((frame_idx + 1) * points_per_frame, len(positions))
|
||||
|
||||
# Create high-quality 3D plot
|
||||
plt.style.use('dark_background') # Cinema-style dark theme
|
||||
fig = plt.figure(figsize=(16, 12), dpi=150) # Higher resolution
|
||||
ax = fig.add_subplot(111, projection='3d')
|
||||
|
||||
# Plot route with cinema styling
|
||||
if end_point > 1:
|
||||
# Gradient effect for completed route
|
||||
colors = np.linspace(0, 1, end_point)
|
||||
ax.scatter(x[:end_point], y[:end_point], z[:end_point],
|
||||
c=colors, cmap='plasma', s=30, alpha=0.8)
|
||||
ax.plot(x[:end_point], y[:end_point], z[:end_point],
|
||||
color='cyan', linewidth=3, alpha=0.9)
|
||||
|
||||
# Current position with glow effect
|
||||
if end_point > 0:
|
||||
current_idx = end_point - 1
|
||||
# Multiple layers for glow effect
|
||||
for size, alpha in [(200, 0.3), (150, 0.5), (100, 0.8)]:
|
||||
ax.scatter(x[current_idx], y[current_idx], z[current_idx],
|
||||
c='yellow', s=size, alpha=alpha, marker='o')
|
||||
|
||||
# Trail effect
|
||||
trail_start = max(0, current_idx - 10)
|
||||
if current_idx > trail_start:
|
||||
trail_alpha = np.linspace(0.3, 1.0, current_idx - trail_start + 1)
|
||||
for i, alpha in enumerate(trail_alpha):
|
||||
idx = trail_start + i
|
||||
ax.scatter(x[idx], y[idx], z[idx],
|
||||
c='orange', s=60, alpha=alpha)
|
||||
|
||||
# Remaining route preview
|
||||
if end_point < len(positions):
|
||||
ax.plot(x[end_point:], y[end_point:], z[end_point:],
|
||||
color='gray', linewidth=1, alpha=0.4, linestyle='--')
|
||||
|
||||
# Cinema-style labels and styling
|
||||
ax.set_xlabel('East-West (m)', color='white', fontsize=14)
|
||||
ax.set_ylabel('North-South (m)', color='white', fontsize=14)
|
||||
ax.set_zlabel('Elevation (m)', color='white', fontsize=14)
|
||||
|
||||
# Progress and time info
|
||||
progress_percent = (end_point / len(positions)) * 100
|
||||
timestamp_str = timestamps[end_point-1] if end_point > 0 else "Start"
|
||||
ax.set_title(f'CINEMA GPS JOURNEY\nProgress: {progress_percent:.1f}% • Point {end_point}/{len(positions)} • {timestamp_str}',
|
||||
color='white', fontsize=16, pad=20, weight='bold')
|
||||
|
||||
# Consistent view with cinematic angle
|
||||
margin = max(np.ptp(x), np.ptp(y)) * 0.15
|
||||
ax.set_xlim(np.min(x) - margin, np.max(x) + margin)
|
||||
ax.set_ylim(np.min(y) - margin, np.max(y) + margin)
|
||||
ax.set_zlim(np.min(z) - 20, np.max(z) + 20)
|
||||
|
||||
# Dynamic camera movement for cinematic effect
|
||||
azim = 45 + (frame_idx * 0.5) % 360 # Slowly rotating view
|
||||
ax.view_init(elev=25, azim=azim)
|
||||
|
||||
# Cinema-style grid
|
||||
ax.grid(True, alpha=0.2, color='white')
|
||||
ax.xaxis.pane.fill = False
|
||||
ax.yaxis.pane.fill = False
|
||||
ax.zaxis.pane.fill = False
|
||||
|
||||
# Make pane edges more subtle
|
||||
ax.xaxis.pane.set_edgecolor('gray')
|
||||
ax.yaxis.pane.set_edgecolor('gray')
|
||||
ax.zaxis.pane.set_edgecolor('gray')
|
||||
ax.xaxis.pane.set_alpha(0.1)
|
||||
ax.yaxis.pane.set_alpha(0.1)
|
||||
ax.zaxis.pane.set_alpha(0.1)
|
||||
|
||||
# Save high-quality frame
|
||||
frame_path = os.path.join(frames_folder, f"cinema_frame_{frame_idx:04d}.png")
|
||||
try:
|
||||
plt.savefig(frame_path, dpi=150, bbox_inches='tight',
|
||||
facecolor='black', edgecolor='none', format='png')
|
||||
plt.close(fig)
|
||||
|
||||
if os.path.exists(frame_path) and os.path.getsize(frame_path) > 1024:
|
||||
test_frame = cv2.imread(frame_path)
|
||||
if test_frame is not None:
|
||||
frame_files.append(frame_path)
|
||||
if frame_idx == 0:
|
||||
h, w, c = test_frame.shape
|
||||
update_status(current_progress, f"Cinema quality: {w}x{h} at {fps} FPS")
|
||||
except Exception as frame_error:
|
||||
update_status(current_progress, f"Error creating frame {frame_idx}: {str(frame_error)}")
|
||||
plt.close(fig)
|
||||
continue
|
||||
|
||||
plt.style.use('default') # Reset style
|
||||
|
||||
# Create cinema video
|
||||
if not frame_files:
|
||||
raise Exception("No valid cinema frames were generated")
|
||||
|
||||
update_status(80, f"Creating cinema video from {len(frame_files)} frames...")
|
||||
|
||||
output_video_path = os.path.join(project_folder, f"{self.project_name}_cinema_3d_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
|
||||
|
||||
# Cinema video creation with higher quality
|
||||
first_frame = cv2.imread(frame_files[0])
|
||||
height, width, layers = first_frame.shape
|
||||
|
||||
# Try to create high-quality video
|
||||
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
||||
video_writer = cv2.VideoWriter(output_video_path, fourcc, fps, (width, height))
|
||||
|
||||
if video_writer.isOpened():
|
||||
for i, frame_file in enumerate(frame_files):
|
||||
frame = cv2.imread(frame_file)
|
||||
if frame is not None:
|
||||
video_writer.write(frame)
|
||||
|
||||
if i % 10 == 0:
|
||||
progress = 80 + (i / len(frame_files)) * 8
|
||||
update_status(progress, f"Encoding cinema frame {i+1}/{len(frame_files)}")
|
||||
|
||||
video_writer.release()
|
||||
|
||||
if os.path.exists(output_video_path) and os.path.getsize(output_video_path) > 1024:
|
||||
update_status(90, "Cinema video created successfully")
|
||||
output_path = output_video_path
|
||||
else:
|
||||
raise Exception("Cinema video creation failed")
|
||||
else:
|
||||
raise Exception("Could not initialize cinema video writer")
|
||||
|
||||
# Clean up frames
|
||||
for frame_file in frame_files:
|
||||
try:
|
||||
os.remove(frame_file)
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
os.rmdir(frames_folder)
|
||||
except:
|
||||
pass
|
||||
|
||||
update_status(100, "Cinema animation complete!")
|
||||
|
||||
def show_success(dt):
|
||||
popup.dismiss()
|
||||
self.show_success_popup(
|
||||
"Cinema Animation Complete!",
|
||||
f"Your cinema-quality animation has been saved to:\n{output_path}\n\nNote: Blender was not available, so advanced 3D cinema mode was used instead.",
|
||||
output_path
|
||||
)
|
||||
|
||||
Clock.schedule_once(show_success, 1)
|
||||
|
||||
except Exception as e:
|
||||
error_message = str(e)
|
||||
print(f"DEBUG: Cinema animation error: {error_message}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
def show_error(dt):
|
||||
popup.dismiss()
|
||||
self.show_error_popup("Cinema Animation Error", error_message)
|
||||
|
||||
Clock.schedule_once(show_error, 0)
|
||||
|
||||
# Schedule the animation generation
|
||||
Clock.schedule_once(lambda dt: run_blender_animation(), 0.5)
|
||||
|
||||
def generate_progressive_3d_animation(self):
|
||||
"""Generate a progressive 3D animation that builds the trip point by point"""
|
||||
# Show processing popup
|
||||
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||||
label = Label(text="Initializing progressive 3D animation...")
|
||||
progress = ProgressBar(max=100, value=0)
|
||||
layout.add_widget(label)
|
||||
layout.add_widget(progress)
|
||||
popup = Popup(
|
||||
title="Generating Progressive 3D Animation",
|
||||
content=layout,
|
||||
size_hint=(0.9, None),
|
||||
size=(0, 200),
|
||||
auto_dismiss=False
|
||||
)
|
||||
popup.open()
|
||||
|
||||
def run_progressive_animation():
|
||||
try:
|
||||
# Import here to avoid startup delays
|
||||
import matplotlib
|
||||
matplotlib.use('Agg') # Use non-interactive backend
|
||||
import matplotlib.pyplot as plt
|
||||
from mpl_toolkits.mplot3d import Axes3D
|
||||
import numpy as np
|
||||
import cv2 # Use OpenCV instead of MoviePy
|
||||
|
||||
# Update status
|
||||
def update_status(progress_val, status_text):
|
||||
def _update(dt):
|
||||
progress.value = progress_val
|
||||
label.text = status_text
|
||||
Clock.schedule_once(_update, 0)
|
||||
|
||||
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
|
||||
positions_path = os.path.join(project_folder, "positions.json")
|
||||
|
||||
if not os.path.exists(positions_path):
|
||||
update_status(0, "Error: No GPS data found")
|
||||
Clock.schedule_once(lambda dt: popup.dismiss(), 2)
|
||||
return
|
||||
|
||||
update_status(10, "Loading GPS data...")
|
||||
|
||||
# Load GPS data
|
||||
with open(positions_path, 'r') as f:
|
||||
positions = json.load(f)
|
||||
|
||||
if len(positions) < 2:
|
||||
update_status(0, "Error: Need at least 2 GPS points")
|
||||
Clock.schedule_once(lambda dt: popup.dismiss(), 2)
|
||||
return
|
||||
|
||||
update_status(20, "Processing GPS coordinates...")
|
||||
|
||||
# Extract coordinates and timestamps
|
||||
lats = [pos['latitude'] for pos in positions]
|
||||
lons = [pos['longitude'] for pos in positions]
|
||||
alts = [pos.get('altitude', 0) for pos in positions]
|
||||
timestamps = [pos.get('fixTime', '') for pos in positions]
|
||||
|
||||
# Convert to numpy arrays for easier manipulation
|
||||
lats = np.array(lats)
|
||||
lons = np.array(lons)
|
||||
alts = np.array(alts)
|
||||
|
||||
# Normalize coordinates for better visualization
|
||||
lat_center = np.mean(lats)
|
||||
lon_center = np.mean(lons)
|
||||
alt_min = np.min(alts)
|
||||
|
||||
# Convert to relative coordinates (in meters approximately)
|
||||
x = (lons - lon_center) * 111320 * np.cos(np.radians(lat_center)) # longitude to meters
|
||||
y = (lats - lat_center) * 110540 # latitude to meters
|
||||
z = alts - alt_min # relative altitude
|
||||
|
||||
update_status(30, "Creating animation frames...")
|
||||
|
||||
# Create frames folder
|
||||
frames_folder = os.path.join(project_folder, "progressive_frames")
|
||||
os.makedirs(frames_folder, exist_ok=True)
|
||||
|
||||
# Animation settings
|
||||
fps = 10 # frames per second
|
||||
points_per_frame = max(1, len(positions) // 100) # Show multiple points per frame for long routes
|
||||
total_frames = len(positions) // points_per_frame
|
||||
|
||||
frame_files = []
|
||||
|
||||
# Generate frames
|
||||
for frame_idx in range(total_frames):
|
||||
current_progress = 30 + (frame_idx / total_frames) * 50
|
||||
update_status(current_progress, f"Creating frame {frame_idx + 1}/{total_frames}...")
|
||||
|
||||
# Points to show in this frame
|
||||
end_point = min((frame_idx + 1) * points_per_frame, len(positions))
|
||||
|
||||
# Create 3D plot
|
||||
fig = plt.figure(figsize=(12, 9), dpi=100)
|
||||
ax = fig.add_subplot(111, projection='3d')
|
||||
|
||||
# Plot the route progressively
|
||||
if end_point > 1:
|
||||
# Plot completed route in blue
|
||||
ax.plot(x[:end_point], y[:end_point], z[:end_point],
|
||||
'b-', linewidth=2, alpha=0.7, label='Route')
|
||||
|
||||
# Plot points as small dots
|
||||
ax.scatter(x[:end_point], y[:end_point], z[:end_point],
|
||||
c='blue', s=20, alpha=0.6)
|
||||
|
||||
# Highlight current position in red
|
||||
if end_point > 0:
|
||||
current_idx = end_point - 1
|
||||
ax.scatter(x[current_idx], y[current_idx], z[current_idx],
|
||||
c='red', s=100, marker='o', label='Current Position')
|
||||
|
||||
# Add a small trail behind current position
|
||||
trail_start = max(0, current_idx - 5)
|
||||
if current_idx > trail_start:
|
||||
ax.plot(x[trail_start:current_idx+1],
|
||||
y[trail_start:current_idx+1],
|
||||
z[trail_start:current_idx+1],
|
||||
'r-', linewidth=4, alpha=0.8)
|
||||
|
||||
# Plot remaining route in light gray (preview)
|
||||
if end_point < len(positions):
|
||||
ax.plot(x[end_point:], y[end_point:], z[end_point:],
|
||||
'lightgray', linewidth=1, alpha=0.3, label='Remaining Route')
|
||||
|
||||
# Set labels and title
|
||||
ax.set_xlabel('East-West (meters)')
|
||||
ax.set_ylabel('North-South (meters)')
|
||||
ax.set_zlabel('Elevation (meters)')
|
||||
|
||||
# Add progress info
|
||||
progress_percent = (end_point / len(positions)) * 100
|
||||
timestamp_str = timestamps[end_point-1] if end_point > 0 else "Start"
|
||||
ax.set_title(f'GPS Trip Animation\nProgress: {progress_percent:.1f}% - Point {end_point}/{len(positions)}\nTime: {timestamp_str}',
|
||||
fontsize=14, pad=20)
|
||||
|
||||
# Set consistent view limits for all frames
|
||||
margin = max(np.ptp(x), np.ptp(y)) * 0.1
|
||||
ax.set_xlim(np.min(x) - margin, np.max(x) + margin)
|
||||
ax.set_ylim(np.min(y) - margin, np.max(y) + margin)
|
||||
ax.set_zlim(np.min(z) - 10, np.max(z) + 10)
|
||||
|
||||
# Set viewing angle for better 3D perspective
|
||||
ax.view_init(elev=20, azim=45)
|
||||
|
||||
# Add legend
|
||||
ax.legend(loc='upper right')
|
||||
|
||||
# Add grid
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
# Save frame with comprehensive error handling
|
||||
frame_path = os.path.join(frames_folder, f"frame_{frame_idx:04d}.png")
|
||||
try:
|
||||
plt.savefig(frame_path, dpi=100, bbox_inches='tight',
|
||||
facecolor='white', edgecolor='none',
|
||||
format='png', optimize=False)
|
||||
plt.close(fig)
|
||||
|
||||
# Verify frame was saved properly and is readable by OpenCV
|
||||
if os.path.exists(frame_path) and os.path.getsize(frame_path) > 1024:
|
||||
# Test if OpenCV can read the frame
|
||||
test_frame = cv2.imread(frame_path)
|
||||
if test_frame is not None:
|
||||
frame_files.append(frame_path)
|
||||
if frame_idx == 0: # Log first frame details
|
||||
h, w, c = test_frame.shape
|
||||
update_status(current_progress, f"First frame: {w}x{h}, size: {os.path.getsize(frame_path)} bytes")
|
||||
else:
|
||||
update_status(current_progress, f"Warning: Frame {frame_idx} not readable by OpenCV")
|
||||
try:
|
||||
os.remove(frame_path)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
update_status(current_progress, f"Warning: Frame {frame_idx} too small or missing")
|
||||
|
||||
except Exception as e:
|
||||
update_status(current_progress, f"Error saving frame {frame_idx}: {str(e)}")
|
||||
try:
|
||||
plt.close(fig)
|
||||
except:
|
||||
pass
|
||||
continue
|
||||
|
||||
# Validate frames before creating video
|
||||
if not frame_files:
|
||||
raise Exception("No valid frames were generated")
|
||||
|
||||
update_status(80, f"Creating video from {len(frame_files)} frames...")
|
||||
|
||||
# Create video using OpenCV with better error handling
|
||||
output_video_path = os.path.join(project_folder,
|
||||
f"{self.project_name}_progressive_3d_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
|
||||
|
||||
if frame_files:
|
||||
try:
|
||||
# Read first frame to get dimensions
|
||||
first_frame = cv2.imread(frame_files[0])
|
||||
if first_frame is None:
|
||||
raise Exception(f"Could not read first frame: {frame_files[0]}")
|
||||
|
||||
height, width, layers = first_frame.shape
|
||||
update_status(82, f"Video dimensions: {width}x{height}")
|
||||
|
||||
# Try different codecs for better compatibility
|
||||
codecs_to_try = [
|
||||
('mp4v', '.mp4'),
|
||||
('XVID', '.avi'),
|
||||
('MJPG', '.avi')
|
||||
]
|
||||
|
||||
video_created = False
|
||||
|
||||
for codec, ext in codecs_to_try:
|
||||
try:
|
||||
# Update output path for different codecs
|
||||
if ext != '.mp4':
|
||||
test_output_path = output_video_path.replace('.mp4', ext)
|
||||
else:
|
||||
test_output_path = output_video_path
|
||||
|
||||
update_status(84, f"Trying codec {codec}...")
|
||||
|
||||
# Create video writer
|
||||
fourcc = cv2.VideoWriter_fourcc(*codec)
|
||||
video_writer = cv2.VideoWriter(test_output_path, fourcc, fps, (width, height))
|
||||
|
||||
if not video_writer.isOpened():
|
||||
update_status(85, f"Failed to open video writer with {codec}")
|
||||
continue
|
||||
|
||||
# Add frames to video
|
||||
frames_written = 0
|
||||
for i, frame_file in enumerate(frame_files):
|
||||
frame = cv2.imread(frame_file)
|
||||
if frame is not None:
|
||||
# Ensure frame dimensions match
|
||||
if frame.shape[:2] != (height, width):
|
||||
frame = cv2.resize(frame, (width, height))
|
||||
video_writer.write(frame)
|
||||
frames_written += 1
|
||||
|
||||
if i % 10 == 0: # Update progress every 10 frames
|
||||
progress = 85 + (i / len(frame_files)) * 3
|
||||
update_status(progress, f"Writing frame {i+1}/{len(frame_files)} with {codec}")
|
||||
|
||||
video_writer.release()
|
||||
|
||||
# Check if video file was created and has reasonable size
|
||||
if os.path.exists(test_output_path) and os.path.getsize(test_output_path) > 1024:
|
||||
output_video_path = test_output_path
|
||||
video_created = True
|
||||
update_status(88, f"Video created successfully with {codec} ({frames_written} frames)")
|
||||
break
|
||||
else:
|
||||
update_status(86, f"Video file not created or too small with {codec}")
|
||||
|
||||
except Exception as codec_error:
|
||||
update_status(87, f"Error with {codec}: {str(codec_error)}")
|
||||
continue
|
||||
|
||||
if not video_created:
|
||||
raise Exception("Failed to create video with any codec")
|
||||
|
||||
except Exception as video_error:
|
||||
raise Exception(f"Video creation failed: {str(video_error)}")
|
||||
|
||||
update_status(90, "Cleaning up temporary files...")
|
||||
|
||||
# Clean up frame files
|
||||
for frame_file in frame_files:
|
||||
try:
|
||||
os.remove(frame_file)
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
os.rmdir(frames_folder)
|
||||
except:
|
||||
pass
|
||||
|
||||
update_status(100, "Progressive 3D animation complete!")
|
||||
|
||||
def show_success(dt):
|
||||
popup.dismiss()
|
||||
self.show_success_popup(
|
||||
"Progressive 3D Animation Complete!",
|
||||
f"Your progressive 3D animation has been saved to:\n{output_video_path}\n\nThe animation shows {len(positions)} GPS points building the route progressively from start to finish.",
|
||||
output_video_path
|
||||
)
|
||||
|
||||
Clock.schedule_once(show_success, 1)
|
||||
else:
|
||||
raise Exception("No frames were generated")
|
||||
|
||||
except Exception as e:
|
||||
error_message = str(e)
|
||||
print(f"DEBUG: Progressive animation error: {error_message}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
def show_error(dt):
|
||||
popup.dismiss()
|
||||
self.show_error_popup("Progressive Animation Error", error_message)
|
||||
|
||||
Clock.schedule_once(show_error, 0)
|
||||
|
||||
# Schedule the animation generation
|
||||
Clock.schedule_once(lambda dt: run_progressive_animation(), 0.5)
|
||||
|
||||
def open_pauses_popup(self):
|
||||
"""Navigate to the pause edit screen"""
|
||||
pause_edit_screen = self.manager.get_screen("pause_edit")
|
||||
pause_edit_screen.set_project_and_callback(self.project_name, self.on_pre_enter)
|
||||
self.manager.current = "pause_edit"
|
||||
|
||||
def generate_3d_video(self):
|
||||
"""Show video generation mode selection popup"""
|
||||
self.show_video_generation_options()
|
||||
|
||||
def generate_3d_video_test_mode(self):
|
||||
"""Generate a 3D video animation in 720p test mode for faster processing"""
|
||||
# Show processing popup with test mode indication
|
||||
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||||
label = Label(text="Preparing 720p test video generation...")
|
||||
progress = ProgressBar(max=100, value=0)
|
||||
layout.add_widget(label)
|
||||
layout.add_widget(progress)
|
||||
popup = Popup(
|
||||
title="Generating 3D Video Animation (720p Test Mode)",
|
||||
content=layout,
|
||||
size_hint=(0.9, None),
|
||||
size=(0, 200),
|
||||
auto_dismiss=False
|
||||
)
|
||||
popup.open()
|
||||
def show_success_popup(self, title, message, file_path):
|
||||
"""Show success popup with option to open file location"""
|
||||
layout = BoxLayout(orientation='vertical', spacing=10, padding=15)
|
||||
|
||||
# Schedule the 3D video generation in test mode
|
||||
Clock.schedule_once(
|
||||
lambda dt: generate_3d_video_animation(
|
||||
self.project_name,
|
||||
RESOURCES_FOLDER,
|
||||
label,
|
||||
progress,
|
||||
popup,
|
||||
Clock,
|
||||
test_mode=True # Enable test mode
|
||||
),
|
||||
0.5
|
||||
)
|
||||
|
||||
def generate_3d_video_production_mode(self):
|
||||
"""Generate a 3D video animation in 2K production mode for high quality"""
|
||||
# Show processing popup with production mode indication
|
||||
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||||
label = Label(text="Preparing 2K production video generation...")
|
||||
progress = ProgressBar(max=100, value=0)
|
||||
layout.add_widget(label)
|
||||
layout.add_widget(progress)
|
||||
popup = Popup(
|
||||
title="Generating 3D Video Animation (2K Production Mode)",
|
||||
content=layout,
|
||||
size_hint=(0.9, None),
|
||||
size=(0, 200),
|
||||
auto_dismiss=False
|
||||
)
|
||||
popup.open()
|
||||
|
||||
# Schedule the 3D video generation in production mode
|
||||
Clock.schedule_once(
|
||||
lambda dt: generate_3d_video_animation(
|
||||
self.project_name,
|
||||
RESOURCES_FOLDER,
|
||||
label,
|
||||
progress,
|
||||
popup,
|
||||
Clock,
|
||||
test_mode=False # Disable test mode for production
|
||||
),
|
||||
0.5
|
||||
)
|
||||
|
||||
def show_video_generation_options(self):
|
||||
"""Show popup with video generation mode options"""
|
||||
from kivy.uix.popup import Popup
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.label import Label
|
||||
|
||||
layout = BoxLayout(orientation='vertical', spacing=15, padding=15)
|
||||
|
||||
# Title
|
||||
title_label = Label(
|
||||
text="Choose Video Generation Mode",
|
||||
font_size=18,
|
||||
size_hint_y=None,
|
||||
height=40,
|
||||
color=(1, 1, 1, 1)
|
||||
)
|
||||
layout.add_widget(title_label)
|
||||
|
||||
# Test mode description
|
||||
test_layout = BoxLayout(orientation='vertical', spacing=5)
|
||||
test_title = Label(
|
||||
text="🏃♂️ 720p Test Mode (Fast)",
|
||||
font_size=16,
|
||||
size_hint_y=None,
|
||||
height=30,
|
||||
color=(0.2, 0.8, 0.2, 1)
|
||||
)
|
||||
test_desc = Label(
|
||||
text="• Resolution: 1280x720\n• Frame rate: 30 FPS\n• ~3x faster generation\n• Perfect for quick previews",
|
||||
font_size=12,
|
||||
size_hint_y=None,
|
||||
height=80,
|
||||
color=(0.9, 0.9, 0.9, 1),
|
||||
halign="left",
|
||||
# Success message
|
||||
success_label = Label(
|
||||
text=message,
|
||||
text_size=(None, None),
|
||||
halign="center",
|
||||
valign="middle"
|
||||
)
|
||||
test_desc.text_size = (None, None)
|
||||
test_layout.add_widget(test_title)
|
||||
test_layout.add_widget(test_desc)
|
||||
layout.add_widget(test_layout)
|
||||
layout.add_widget(success_label)
|
||||
|
||||
# Test mode button
|
||||
test_btn = Button(
|
||||
text="Generate 720p Test Video",
|
||||
background_color=(0.2, 0.8, 0.2, 1),
|
||||
size_hint_y=None,
|
||||
height=50,
|
||||
font_size=14
|
||||
)
|
||||
layout.add_widget(test_btn)
|
||||
# Buttons
|
||||
btn_layout = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=50)
|
||||
|
||||
# Production mode description
|
||||
prod_layout = BoxLayout(orientation='vertical', spacing=5)
|
||||
prod_title = Label(
|
||||
text="🎯 2K Production Mode (High Quality)",
|
||||
font_size=16,
|
||||
size_hint_y=None,
|
||||
height=30,
|
||||
color=(0.8, 0.2, 0.2, 1)
|
||||
open_folder_btn = Button(
|
||||
text="Open Folder",
|
||||
background_color=(0.2, 0.6, 0.9, 1)
|
||||
)
|
||||
prod_desc = Label(
|
||||
text="• Resolution: 2560x1440\n• Frame rate: 60 FPS\n• Cinema-quality results\n• Ultra-detailed visuals",
|
||||
font_size=12,
|
||||
size_hint_y=None,
|
||||
height=80,
|
||||
color=(0.9, 0.9, 0.9, 1),
|
||||
halign="left",
|
||||
valign="middle"
|
||||
)
|
||||
prod_desc.text_size = (None, None)
|
||||
prod_layout.add_widget(prod_title)
|
||||
prod_layout.add_widget(prod_desc)
|
||||
layout.add_widget(prod_layout)
|
||||
|
||||
# Production mode button
|
||||
prod_btn = Button(
|
||||
text="Generate 2K Production Video",
|
||||
background_color=(0.8, 0.2, 0.2, 1),
|
||||
size_hint_y=None,
|
||||
height=50,
|
||||
font_size=14
|
||||
ok_btn = Button(
|
||||
text="OK",
|
||||
background_color=(0.3, 0.7, 0.3, 1)
|
||||
)
|
||||
layout.add_widget(prod_btn)
|
||||
|
||||
# Cancel button
|
||||
cancel_btn = Button(
|
||||
text="Cancel",
|
||||
background_color=(0.5, 0.5, 0.5, 1),
|
||||
size_hint_y=None,
|
||||
height=40,
|
||||
font_size=12
|
||||
)
|
||||
layout.add_widget(cancel_btn)
|
||||
|
||||
btn_layout.add_widget(open_folder_btn)
|
||||
btn_layout.add_widget(ok_btn)
|
||||
layout.add_widget(btn_layout)
|
||||
|
||||
popup = Popup(
|
||||
title="Select Video Generation Mode",
|
||||
title=title,
|
||||
content=layout,
|
||||
size_hint=(0.9, 0.8),
|
||||
size_hint=(0.9, 0.6),
|
||||
auto_dismiss=False
|
||||
)
|
||||
|
||||
def start_test_mode(instance):
|
||||
|
||||
def open_folder(instance):
|
||||
folder_path = os.path.dirname(file_path)
|
||||
os.system(f'xdg-open "{folder_path}"') # Linux
|
||||
popup.dismiss()
|
||||
self.generate_3d_video_test_mode()
|
||||
|
||||
def start_production_mode(instance):
|
||||
|
||||
def close_popup(instance):
|
||||
popup.dismiss()
|
||||
self.generate_3d_video_production_mode()
|
||||
|
||||
test_btn.bind(on_press=start_test_mode)
|
||||
prod_btn.bind(on_press=start_production_mode)
|
||||
cancel_btn.bind(on_press=lambda x: popup.dismiss())
|
||||
|
||||
open_folder_btn.bind(on_press=open_folder)
|
||||
ok_btn.bind(on_press=close_popup)
|
||||
|
||||
popup.open()
|
||||
|
||||
def show_error_popup(self, title, message):
|
||||
"""Show error popup"""
|
||||
layout = BoxLayout(orientation='vertical', spacing=10, padding=15)
|
||||
|
||||
error_label = Label(
|
||||
text=f"Error: {message}",
|
||||
text_size=(None, None),
|
||||
halign="center",
|
||||
valign="middle"
|
||||
)
|
||||
layout.add_widget(error_label)
|
||||
|
||||
ok_btn = Button(
|
||||
text="OK",
|
||||
background_color=(0.8, 0.3, 0.3, 1),
|
||||
size_hint_y=None,
|
||||
height=50
|
||||
)
|
||||
layout.add_widget(ok_btn)
|
||||
|
||||
popup = Popup(
|
||||
title=title,
|
||||
content=layout,
|
||||
size_hint=(0.8, 0.4),
|
||||
auto_dismiss=False
|
||||
)
|
||||
|
||||
ok_btn.bind(on_press=lambda x: popup.dismiss())
|
||||
popup.open()
|
||||
|
||||
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()
|
||||
79
traccar.kv
79
traccar.kv
@@ -722,7 +722,7 @@
|
||||
height: 202
|
||||
size_hint_x: 1
|
||||
|
||||
# 3D Video Animation frame
|
||||
# Progressive 3D Animation frame
|
||||
BoxLayout:
|
||||
orientation: "horizontal"
|
||||
size_hint_y: None
|
||||
@@ -737,7 +737,7 @@
|
||||
size: self.size
|
||||
|
||||
Label:
|
||||
text: "Generate 3D video animation\nsimilar to Relive style"
|
||||
text: "Generate progressive 3D animation\nBuilds trip point by point"
|
||||
font_size: 16
|
||||
color: 1, 1, 1, 1
|
||||
size_hint_x: 0.7
|
||||
@@ -746,15 +746,84 @@
|
||||
text_size: self.size
|
||||
|
||||
Button:
|
||||
text: "Generate\n3D Video"
|
||||
text: "Generate\n3D Trip"
|
||||
size_hint_x: 0.3
|
||||
font_size: 14
|
||||
background_color: 0.8, 0.2, 0.4, 1
|
||||
background_color: 0.2, 0.8, 0.4, 1
|
||||
color: 1, 1, 1, 1
|
||||
halign: "center"
|
||||
valign: "middle"
|
||||
text_size: self.size
|
||||
on_press: root.generate_3d_video()
|
||||
on_press: root.generate_progressive_3d_animation()
|
||||
|
||||
# Google Earth Animation frame
|
||||
BoxLayout:
|
||||
orientation: "horizontal"
|
||||
size_hint_y: None
|
||||
height: 60
|
||||
padding: [10, 10, 10, 10]
|
||||
spacing: 10
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: 0.15, 0.15, 0.18, 1
|
||||
Rectangle:
|
||||
pos: self.pos
|
||||
size: self.size
|
||||
|
||||
Label:
|
||||
text: "Generate Google Earth flythrough\nCinematic aerial view"
|
||||
font_size: 16
|
||||
color: 1, 1, 1, 1
|
||||
size_hint_x: 0.7
|
||||
halign: "left"
|
||||
valign: "middle"
|
||||
text_size: self.size
|
||||
|
||||
Button:
|
||||
text: "Generate\nFlythrough"
|
||||
size_hint_x: 0.3
|
||||
font_size: 14
|
||||
background_color: 0.1, 0.8, 0.1, 1
|
||||
color: 1, 1, 1, 1
|
||||
halign: "center"
|
||||
valign: "middle"
|
||||
text_size: self.size
|
||||
on_press: root.generate_google_earth_animation()
|
||||
|
||||
# Blender Animation frame
|
||||
BoxLayout:
|
||||
orientation: "horizontal"
|
||||
size_hint_y: None
|
||||
height: 60
|
||||
padding: [10, 10, 10, 10]
|
||||
spacing: 10
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: 0.15, 0.15, 0.18, 1
|
||||
Rectangle:
|
||||
pos: self.pos
|
||||
size: self.size
|
||||
|
||||
Label:
|
||||
text: "Generate cinema-quality animation\nProfessional 3D rendering"
|
||||
font_size: 16
|
||||
color: 1, 1, 1, 1
|
||||
size_hint_x: 0.7
|
||||
halign: "left"
|
||||
valign: "middle"
|
||||
text_size: self.size
|
||||
|
||||
Button:
|
||||
text: "Generate\nCinema"
|
||||
size_hint_x: 0.3
|
||||
font_size: 14
|
||||
background_color: 0.9, 0.6, 0.2, 1
|
||||
color: 1, 1, 1, 1
|
||||
halign: "center"
|
||||
valign: "middle"
|
||||
text_size: self.size
|
||||
on_press: root.generate_blender_animation()
|
||||
|
||||
|
||||
|
||||
Widget:
|
||||
|
||||
Reference in New Issue
Block a user