Compare commits
10 Commits
a38e2b1fe9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7df9de12ce | |||
| 9f8c1c27dc | |||
| 1d0dc05a7b | |||
| 911143dfc5 | |||
| 29fd68f732 | |||
| 4fa7ed2a48 | |||
| 35d3bb8442 | |||
| 507f526433 | |||
| a565cd67e1 | |||
| 2532bf6219 |
37
.gitignore
vendored
37
.gitignore
vendored
@@ -1,2 +1,39 @@
|
|||||||
# Ignore the virtual environment folder
|
# Ignore the virtual environment folder
|
||||||
track/
|
track/
|
||||||
|
|
||||||
|
# Ignore Python cache files
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
|
||||||
|
# Ignore project data and generated files
|
||||||
|
resources/projects/
|
||||||
|
resources/trip_archive/
|
||||||
|
resources/credentials.enc
|
||||||
|
resources/key.key
|
||||||
|
resources/server_settings.enc
|
||||||
|
|
||||||
|
# Ignore generated videos and frames
|
||||||
|
*.mp4
|
||||||
|
*.avi
|
||||||
|
*.mov
|
||||||
|
*.webm
|
||||||
|
cinema_frames/
|
||||||
|
progressive_frames/
|
||||||
|
|
||||||
|
# Ignore test files and temporary files
|
||||||
|
test_*.py
|
||||||
|
*.tmp
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Ignore IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Ignore OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,29 +1,66 @@
|
|||||||
# 3D Video Animation Feature
|
# Professional Google Earth-Style 3D Video Animation
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
The 3D Video Animation feature generates Relive-style video animations from GPS route data. This creates engaging, cinematic videos that visualize your journey in a 3D perspective.
|
The Professional Google Earth-Style 3D Video Animation feature generates cinematic, high-quality video animations from GPS route data with realistic space entry sequences. This system creates authentic Google Earth-style visuals with professional terrain rendering, atmospheric effects, and spectacular space-to-Earth transitions.
|
||||||
|
|
||||||
## Features
|
## Major Visual Enhancements
|
||||||
|
|
||||||
### Visual Elements
|
### Realistic Google Earth Visuals
|
||||||
- **3D Isometric View**: Perspective projection that simulates 3D depth
|
- **Authentic Earth Sphere Rendering**: Realistic planetary view from space with proper curvature
|
||||||
- **Sky Gradient Background**: Blue gradient background that mimics sky
|
- **Professional Terrain Textures**: Multi-layer terrain with forests, mountains, plains, deserts, and water bodies
|
||||||
- **Animated Route Trail**: Color-coded path from blue (start) to red (end)
|
- **Geographic Feature Simulation**: Coastlines, rivers, and landmasses with fractal-like detail
|
||||||
- **Pulsing Position Marker**: Animated current position indicator
|
- **Atmospheric Scattering**: Realistic atmospheric effects and color gradients
|
||||||
- **Grid Overlay**: 3D grid effect for depth perception
|
- **Cloud Layer Rendering**: Dynamic cloud formations with proper shadows
|
||||||
- **Real-time Data Display**: Speed, timestamp, and progress information
|
|
||||||
|
|
||||||
### Technical Specifications
|
### Enhanced Space Entry Sequence
|
||||||
|
- **Spectacular Space View**: Authentic space background with star fields and Earth sphere
|
||||||
|
- **Realistic Atmospheric Entry**: Progressive transition through atmospheric layers
|
||||||
|
- **Earth's Terminator Line**: Day/night boundary visible at high altitudes
|
||||||
|
- **Professional UI**: Google Earth-style information panels and progress indicators
|
||||||
|
- **Cinematic Descent**: Smooth altitude progression from 50km to route level
|
||||||
|
|
||||||
|
### Advanced Terrain System
|
||||||
|
- **Multi-Octave Terrain Generation**: Realistic landscape using multiple noise layers
|
||||||
|
- **Geographic Coordinate Influence**: Terrain varies based on actual GPS coordinates
|
||||||
|
- **Atmospheric Perspective**: Distance-based color shifts and haze effects
|
||||||
|
- **Cloud Shadow Mapping**: Realistic shadow patterns on terrain
|
||||||
|
- **Enhanced Color Palette**: Professional color schemes for different terrain types
|
||||||
|
|
||||||
|
### Professional UI Elements
|
||||||
|
- **Information Panel**: Speed, bearing, altitude, time, and progress with gradients
|
||||||
|
- **360° Compass**: Full compass with cardinal directions and dynamic needle
|
||||||
|
- **Gradient Progress Bar**: Color-transitioning progress indicator
|
||||||
|
- **Enhanced Typography**: Better text rendering with shadows and effects
|
||||||
|
- **Atmospheric Vignette**: Subtle edge darkening for cinematic feel
|
||||||
|
|
||||||
|
## Technical Specifications
|
||||||
- **Resolution**: 1920x1080 (Full HD)
|
- **Resolution**: 1920x1080 (Full HD)
|
||||||
- **Frame Rate**: 30 FPS
|
- **Frame Rate**: 30 FPS (smooth motion)
|
||||||
- **Format**: MP4 video
|
- **Format**: MP4 video (universal compatibility)
|
||||||
- **Compression**: MP4V codec for broad compatibility
|
- **Compression**: MP4V codec optimized for quality
|
||||||
|
- **Visual Quality**: Professional Google Earth-style rendering
|
||||||
|
- **Space Entry**: 3-second descent from 50km altitude with realistic visuals
|
||||||
|
- **Camera Height**: 1000-3000m (dynamic aerial perspective)
|
||||||
|
- **View Distance**: 3000m ahead (enhanced for aerial views)
|
||||||
|
- **Field of View**: 75° (optimized for aerial perspective)
|
||||||
|
- **Tilt Angle**: 65-73° (dynamic for terrain following)
|
||||||
|
- **Terrain Detail**: Multi-layer realistic terrain with 6+ terrain types
|
||||||
|
- **Color Depth**: Professional color palette with atmospheric effects
|
||||||
|
- **Entry Altitude Range**: 50km → 2km (space to aerial transition)
|
||||||
|
|
||||||
### Animation Effects
|
## Advanced Animation Features
|
||||||
- **Shadow Effects**: Route lines and markers have 3D shadows
|
- **Space Entry Sequence**: Spectacular 3-second descent from space to route
|
||||||
- **Elevation Simulation**: Simulated terrain elevation using sine waves
|
- **Earth Curvature Rendering**: Realistic planetary curvature at high altitudes
|
||||||
- **Smooth Transitions**: Interpolated movement between GPS points
|
- **Atmospheric Transition**: Smooth space-to-atmosphere visual effects
|
||||||
- **Progress Indicators**: Visual progress through the route
|
- **Enhanced Aerial Perspective**: Optimized 1000-3000m camera height range
|
||||||
|
- **3D Shadow Effects**: Multi-layer shadows for depth
|
||||||
|
- **Elevation Dynamics**: Real-time terrain elevation calculation
|
||||||
|
- **Smooth Interpolation**: Advanced movement interpolation
|
||||||
|
- **Depth Culling**: Performance optimization through view frustum culling
|
||||||
|
- **Route Highlighting**: Progressive route visibility during space descent
|
||||||
|
- **Progressive Rendering**: Back-to-front rendering for proper transparency
|
||||||
|
- **Atmospheric Effects**: Distance-based fog and atmospheric perspective
|
||||||
|
- **Dynamic Lighting**: Simulated lighting based on elevation and distance
|
||||||
|
|
||||||
## Required Libraries
|
## Required Libraries
|
||||||
|
|
||||||
@@ -46,33 +83,55 @@ The 3D Video Animation feature generates Relive-style video animations from GPS
|
|||||||
4. **Wait** for processing (can take several minutes)
|
4. **Wait** for processing (can take several minutes)
|
||||||
5. **View** the generated video in the project folder
|
5. **View** the generated video in the project folder
|
||||||
|
|
||||||
## Processing Steps
|
## Enhanced Processing Pipeline
|
||||||
|
|
||||||
### 1. Data Loading (10%)
|
### 1. Route Analysis & Camera Planning (10-20%)
|
||||||
- Loads GPS positions from `positions.json`
|
- Advanced GPS data analysis and validation
|
||||||
- Validates minimum route length (10+ points)
|
- Dynamic camera path calculation
|
||||||
- Calculates route boundaries and center point
|
- Elevation profile generation
|
||||||
|
- Viewport optimization for route coverage
|
||||||
|
|
||||||
### 2. Route Analysis (20%)
|
### 2. 3D Scene Setup (20-30%)
|
||||||
- Determines optimal viewport and scaling
|
- Camera position and target calculation
|
||||||
- Calculates center coordinates for camera position
|
- 3D coordinate system establishment
|
||||||
- Sets up coordinate transformation matrices
|
- Terrain mesh generation
|
||||||
|
- Lighting and atmosphere setup
|
||||||
|
|
||||||
### 3. Frame Generation (30-70%)
|
### 3. Enhanced Frame Generation (30-75%)
|
||||||
- Creates individual frames for each GPS point
|
- Dynamic camera positioning for each frame
|
||||||
- Applies 3D perspective transformation
|
- 3D-to-2D perspective projection
|
||||||
- Renders route trail with color progression
|
- Depth-sorted object rendering
|
||||||
- Adds animated markers and text overlays
|
- Advanced route visualization with gradients
|
||||||
|
- Multi-layer UI element composition
|
||||||
|
- Atmospheric effect application
|
||||||
|
|
||||||
### 4. Video Compilation (75-90%)
|
### 4. Video Assembly & Optimization (75-90%)
|
||||||
- Combines frames into MP4 video
|
- Frame sequence compilation
|
||||||
- Applies compression and optimization
|
- Advanced compression with quality optimization
|
||||||
- Adds metadata and timing information
|
- Metadata embedding
|
||||||
|
- Audio track preparation (future enhancement)
|
||||||
|
|
||||||
### 5. Finalization (90-100%)
|
### 5. Post-Processing & Output (90-100%)
|
||||||
- Saves video to project folder
|
- Final quality optimization
|
||||||
- Cleans up temporary files
|
- File system integration
|
||||||
- Shows completion notification
|
- Temporary file cleanup
|
||||||
|
- User notification and result display
|
||||||
|
|
||||||
|
## Technical Architecture
|
||||||
|
|
||||||
|
### Enhanced Rendering Pipeline
|
||||||
|
```
|
||||||
|
GPS Data → Camera Path Planning → 3D Scene Setup →
|
||||||
|
Dynamic Projection → Depth Sorting → Visual Effects →
|
||||||
|
UI Overlay → Atmospheric Effects → Frame Export
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced 3D Mathematics
|
||||||
|
- **Haversine Distance Calculation**: Precise GPS distance computation
|
||||||
|
- **Bearing Calculation**: Accurate directional vectors
|
||||||
|
- **3D Perspective Projection**: Field-of-view based projection
|
||||||
|
- **Matrix Transformations**: Rotation and translation matrices
|
||||||
|
- **Depth Buffer Simulation**: Z-order sorting for realistic rendering
|
||||||
|
|
||||||
## File Output
|
## File Output
|
||||||
|
|
||||||
@@ -154,4 +213,30 @@ Metadata Addition → File Output
|
|||||||
- **Automatic Naming**: Prevents file name conflicts
|
- **Automatic Naming**: Prevents file name conflicts
|
||||||
- **Folder Opening**: Direct access to output location
|
- **Folder Opening**: Direct access to output location
|
||||||
|
|
||||||
|
## Space Entry Sequence Details
|
||||||
|
|
||||||
|
### Visual Journey
|
||||||
|
1. **Space View (0-1 seconds)**: Starts from 50km altitude with black space background and Earth curvature
|
||||||
|
2. **Atmospheric Entry (1-2 seconds)**: Gradual transition showing atmospheric layers and blue sky emergence
|
||||||
|
3. **Route Approach (2-3 seconds)**: Descent to 2km altitude with route becoming visible and highlighted
|
||||||
|
4. **Transition Bridge (3-3.5 seconds)**: Smooth bridge frame announcing route start
|
||||||
|
5. **Aerial Following (3.5+ seconds)**: Seamless transition to dynamic camera following at optimal aerial height
|
||||||
|
|
||||||
|
### Technical Implementation
|
||||||
|
- **Altitude Range**: 50,000m → 2,000m → 1,000-3,000m (dynamic)
|
||||||
|
- **Descent Curve**: Cubic ease-out for natural deceleration
|
||||||
|
- **Camera Transition**: Smooth movement from center overview to route start
|
||||||
|
- **Transition Bridge**: Dedicated frame for smooth space-to-route handoff
|
||||||
|
- **Visual Effects**: Earth curvature, atmospheric glow, space-to-sky gradient
|
||||||
|
- **Route Visibility**: Progressive highlighting during descent approach
|
||||||
|
- **Error Handling**: Robust fallback frames ensure generation continues
|
||||||
|
- **Variable Safety**: Protected against undefined position markers
|
||||||
|
|
||||||
|
### Enhanced Aerial Perspective
|
||||||
|
- **Optimal Height Range**: 1000-3000 meters for perfect aerial views
|
||||||
|
- **Dynamic Variation**: Camera height varies smoothly for cinematic effect
|
||||||
|
- **Wide Field of View**: 75° FOV for comprehensive aerial perspective
|
||||||
|
- **Enhanced View Distance**: 3000m ahead for better route anticipation
|
||||||
|
- **Improved Tilt Angle**: 65-73° for optimal aerial viewing angle
|
||||||
|
|
||||||
This feature transforms GPS tracking data into professional-quality video animations suitable for sharing, presentations, or personal memories.
|
This feature transforms GPS tracking data into professional-quality video animations suitable for sharing, presentations, or personal memories.
|
||||||
|
|||||||
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)
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
"""
|
|
||||||
3D Video Animation Generator
|
|
||||||
Creates Relive-style 3D video animations from GPS route data
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import math
|
|
||||||
import requests
|
|
||||||
import cv2
|
|
||||||
import numpy as np
|
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
|
||||||
import tempfile
|
|
||||||
import shutil
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
def generate_3d_video_animation(project_name, resources_folder, label_widget, progress_widget, popup_widget, clock_module):
|
|
||||||
"""
|
|
||||||
Generate a 3D video animation similar to Relive
|
|
||||||
|
|
||||||
Args:
|
|
||||||
project_name: Name of the project
|
|
||||||
resources_folder: Path to resources folder
|
|
||||||
label_widget: Kivy label for status updates
|
|
||||||
progress_widget: Kivy progress bar
|
|
||||||
popup_widget: Kivy popup to dismiss when done
|
|
||||||
clock_module: Kivy Clock module for scheduling
|
|
||||||
"""
|
|
||||||
|
|
||||||
def update_progress(progress_val, status_text):
|
|
||||||
"""Update UI from background thread"""
|
|
||||||
def _update(dt):
|
|
||||||
progress_widget.value = progress_val
|
|
||||||
label_widget.text = status_text
|
|
||||||
clock_module.schedule_once(_update, 0)
|
|
||||||
|
|
||||||
def finish_generation(success, message, output_path=None):
|
|
||||||
"""Finish the generation process"""
|
|
||||||
def _finish(dt):
|
|
||||||
if popup_widget:
|
|
||||||
popup_widget.dismiss()
|
|
||||||
|
|
||||||
# Show result popup
|
|
||||||
from kivy.uix.popup import Popup
|
|
||||||
from kivy.uix.boxlayout import BoxLayout
|
|
||||||
from kivy.uix.button import Button
|
|
||||||
from kivy.uix.label import Label
|
|
||||||
|
|
||||||
result_layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
result_label = Label(
|
|
||||||
text=f"3D Video Generated Successfully!\n\nSaved to:\n{output_path}",
|
|
||||||
color=(0, 1, 0, 1),
|
|
||||||
halign="center"
|
|
||||||
)
|
|
||||||
open_btn = Button(
|
|
||||||
text="Open Video Folder",
|
|
||||||
size_hint_y=None,
|
|
||||||
height=40,
|
|
||||||
background_color=(0.2, 0.7, 0.2, 1)
|
|
||||||
)
|
|
||||||
open_btn.bind(on_press=lambda x: (os.system(f"xdg-open '{os.path.dirname(output_path)}'"), result_popup.dismiss()))
|
|
||||||
result_layout.add_widget(result_label)
|
|
||||||
result_layout.add_widget(open_btn)
|
|
||||||
else:
|
|
||||||
result_label = Label(
|
|
||||||
text=f"Generation Failed:\n{message}",
|
|
||||||
color=(1, 0, 0, 1),
|
|
||||||
halign="center"
|
|
||||||
)
|
|
||||||
result_layout.add_widget(result_label)
|
|
||||||
|
|
||||||
close_btn = Button(
|
|
||||||
text="Close",
|
|
||||||
size_hint_y=None,
|
|
||||||
height=40,
|
|
||||||
background_color=(0.3, 0.3, 0.3, 1)
|
|
||||||
)
|
|
||||||
|
|
||||||
result_layout.add_widget(close_btn)
|
|
||||||
|
|
||||||
result_popup = Popup(
|
|
||||||
title="3D Video Generation Result",
|
|
||||||
content=result_layout,
|
|
||||||
size_hint=(0.9, 0.6),
|
|
||||||
auto_dismiss=False
|
|
||||||
)
|
|
||||||
|
|
||||||
close_btn.bind(on_press=lambda x: result_popup.dismiss())
|
|
||||||
result_popup.open()
|
|
||||||
|
|
||||||
clock_module.schedule_once(_finish, 0)
|
|
||||||
|
|
||||||
def run_generation():
|
|
||||||
"""Main generation function"""
|
|
||||||
try:
|
|
||||||
# Step 1: Load route data
|
|
||||||
update_progress(10, "Loading route data...")
|
|
||||||
project_folder = os.path.join(resources_folder, "projects", project_name)
|
|
||||||
positions_path = os.path.join(project_folder, "positions.json")
|
|
||||||
|
|
||||||
if not os.path.exists(positions_path):
|
|
||||||
finish_generation(False, "No route data found!")
|
|
||||||
return
|
|
||||||
|
|
||||||
with open(positions_path, "r") as f:
|
|
||||||
positions = json.load(f)
|
|
||||||
|
|
||||||
if len(positions) < 10:
|
|
||||||
finish_generation(False, "Route too short for 3D animation (minimum 10 points)")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Step 2: Calculate route bounds and center
|
|
||||||
update_progress(20, "Calculating route boundaries...")
|
|
||||||
lats = [pos['latitude'] for pos in positions]
|
|
||||||
lons = [pos['longitude'] for pos in positions]
|
|
||||||
|
|
||||||
center_lat = sum(lats) / len(lats)
|
|
||||||
center_lon = sum(lons) / len(lons)
|
|
||||||
|
|
||||||
min_lat, max_lat = min(lats), max(lats)
|
|
||||||
min_lon, max_lon = min(lons), max(lons)
|
|
||||||
|
|
||||||
# Step 3: Generate frames
|
|
||||||
update_progress(30, "Generating 3D frames...")
|
|
||||||
|
|
||||||
# Create temporary directory for frames
|
|
||||||
temp_dir = tempfile.mkdtemp()
|
|
||||||
frames_dir = os.path.join(temp_dir, "frames")
|
|
||||||
os.makedirs(frames_dir)
|
|
||||||
|
|
||||||
# Video settings
|
|
||||||
width, height = 1920, 1080
|
|
||||||
fps = 30
|
|
||||||
total_frames = len(positions) * 2 # 2 frames per position for smooth animation
|
|
||||||
|
|
||||||
# Generate frames
|
|
||||||
for i, pos in enumerate(positions):
|
|
||||||
progress = 30 + (i / len(positions)) * 40
|
|
||||||
update_progress(progress, f"Generating frame {i+1}/{len(positions)}...")
|
|
||||||
|
|
||||||
frame = create_3d_frame(
|
|
||||||
pos, positions, i, center_lat, center_lon,
|
|
||||||
min_lat, max_lat, min_lon, max_lon,
|
|
||||||
width, height
|
|
||||||
)
|
|
||||||
|
|
||||||
# Save frame
|
|
||||||
frame_path = os.path.join(frames_dir, f"frame_{i:06d}.png")
|
|
||||||
cv2.imwrite(frame_path, frame)
|
|
||||||
|
|
||||||
# Step 4: Create video
|
|
||||||
update_progress(75, "Compiling video...")
|
|
||||||
|
|
||||||
# Output path
|
|
||||||
output_filename = f"{project_name}_3d_animation_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4"
|
|
||||||
output_path = os.path.join(project_folder, output_filename)
|
|
||||||
|
|
||||||
# Create video writer
|
|
||||||
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
|
||||||
video_writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
|
|
||||||
|
|
||||||
# Add frames to video
|
|
||||||
frame_files = sorted([f for f in os.listdir(frames_dir) if f.endswith('.png')])
|
|
||||||
for frame_file in frame_files:
|
|
||||||
frame_path = os.path.join(frames_dir, frame_file)
|
|
||||||
frame = cv2.imread(frame_path)
|
|
||||||
video_writer.write(frame)
|
|
||||||
|
|
||||||
video_writer.release()
|
|
||||||
|
|
||||||
# Step 5: Add audio (optional)
|
|
||||||
update_progress(90, "Adding finishing touches...")
|
|
||||||
|
|
||||||
# Clean up
|
|
||||||
shutil.rmtree(temp_dir)
|
|
||||||
|
|
||||||
update_progress(100, "3D Video generated successfully!")
|
|
||||||
finish_generation(True, "Success!", output_path)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
finish_generation(False, str(e))
|
|
||||||
|
|
||||||
# Start generation in background
|
|
||||||
import threading
|
|
||||||
thread = threading.Thread(target=run_generation)
|
|
||||||
thread.daemon = True
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
def create_3d_frame(current_pos, all_positions, frame_index, center_lat, center_lon,
|
|
||||||
min_lat, max_lat, min_lon, max_lon, width, height):
|
|
||||||
"""
|
|
||||||
Create a single 3D-style frame
|
|
||||||
"""
|
|
||||||
# Create canvas
|
|
||||||
frame = np.zeros((height, width, 3), dtype=np.uint8)
|
|
||||||
|
|
||||||
# Background gradient (sky effect)
|
|
||||||
for y in range(height):
|
|
||||||
color_intensity = int(255 * (1 - y / height))
|
|
||||||
sky_color = (min(255, color_intensity + 50), min(255, color_intensity + 100), 255)
|
|
||||||
frame[y, :] = sky_color
|
|
||||||
|
|
||||||
# Calculate perspective transformation
|
|
||||||
# Simple isometric-style projection
|
|
||||||
scale_x = width * 0.6 / (max_lon - min_lon) if max_lon != min_lon else 1000
|
|
||||||
scale_y = height * 0.6 / (max_lat - min_lat) if max_lat != min_lat else 1000
|
|
||||||
|
|
||||||
# Draw route path with 3D effect
|
|
||||||
route_points = []
|
|
||||||
for i, pos in enumerate(all_positions[:frame_index + 1]):
|
|
||||||
# Convert GPS to screen coordinates
|
|
||||||
x = int((pos['longitude'] - min_lon) * scale_x + width * 0.2)
|
|
||||||
y = int(height * 0.8 - (pos['latitude'] - min_lat) * scale_y)
|
|
||||||
|
|
||||||
# Add 3D effect (elevation simulation)
|
|
||||||
elevation_offset = int(20 * math.sin(i * 0.1)) # Simulated elevation
|
|
||||||
y -= elevation_offset
|
|
||||||
|
|
||||||
route_points.append((x, y))
|
|
||||||
|
|
||||||
# Draw route trail with gradient
|
|
||||||
if len(route_points) > 1:
|
|
||||||
for i in range(1, len(route_points)):
|
|
||||||
# Color gradient from blue to red
|
|
||||||
progress = i / len(route_points)
|
|
||||||
color_r = int(255 * progress)
|
|
||||||
color_b = int(255 * (1 - progress))
|
|
||||||
color = (color_b, 100, color_r)
|
|
||||||
|
|
||||||
# Draw thick line with 3D shadow effect
|
|
||||||
pt1, pt2 = route_points[i-1], route_points[i]
|
|
||||||
|
|
||||||
# Shadow
|
|
||||||
cv2.line(frame, (pt1[0]+2, pt1[1]+2), (pt2[0]+2, pt2[1]+2), (50, 50, 50), 8)
|
|
||||||
# Main line
|
|
||||||
cv2.line(frame, pt1, pt2, color, 6)
|
|
||||||
|
|
||||||
# Draw current position marker
|
|
||||||
if route_points:
|
|
||||||
current_point = route_points[-1]
|
|
||||||
# Pulsing effect
|
|
||||||
pulse_size = int(15 + 10 * math.sin(frame_index * 0.3))
|
|
||||||
|
|
||||||
# Shadow
|
|
||||||
cv2.circle(frame, (current_point[0]+3, current_point[1]+3), pulse_size, (0, 0, 0), -1)
|
|
||||||
# Main marker
|
|
||||||
cv2.circle(frame, current_point, pulse_size, (0, 255, 255), -1)
|
|
||||||
cv2.circle(frame, current_point, pulse_size-3, (255, 255, 255), 2)
|
|
||||||
|
|
||||||
# Add grid effect for 3D feel
|
|
||||||
grid_spacing = 50
|
|
||||||
for x in range(0, width, grid_spacing):
|
|
||||||
cv2.line(frame, (x, 0), (x, height), (100, 100, 100), 1)
|
|
||||||
for y in range(0, height, grid_spacing):
|
|
||||||
cv2.line(frame, (0, y), (width, y), (100, 100, 100), 1)
|
|
||||||
|
|
||||||
# Add text overlay
|
|
||||||
try:
|
|
||||||
# Position info
|
|
||||||
speed = current_pos.get('speed', 0) if current_pos else 0
|
|
||||||
timestamp = current_pos.get('deviceTime', '') if current_pos else ''
|
|
||||||
|
|
||||||
text_y = 50
|
|
||||||
cv2.putText(frame, f"Speed: {speed:.1f} km/h", (50, text_y),
|
|
||||||
cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
|
|
||||||
|
|
||||||
text_y += 40
|
|
||||||
if timestamp:
|
|
||||||
cv2.putText(frame, f"Time: {timestamp[:16]}", (50, text_y),
|
|
||||||
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2)
|
|
||||||
|
|
||||||
text_y += 40
|
|
||||||
cv2.putText(frame, f"Point: {frame_index + 1}/{len(all_positions)}", (50, text_y),
|
|
||||||
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2)
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
pass # Skip text if font issues
|
|
||||||
|
|
||||||
return frame
|
|
||||||
|
|
||||||
def get_elevation_data(lat, lon):
|
|
||||||
"""
|
|
||||||
Get elevation data for a coordinate (optional enhancement)
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Using a free elevation API
|
|
||||||
url = f"https://api.open-elevation.com/api/v1/lookup?locations={lat},{lon}"
|
|
||||||
response = requests.get(url, timeout=5)
|
|
||||||
if response.status_code == 200:
|
|
||||||
data = response.json()
|
|
||||||
return data['results'][0]['elevation']
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return 0 # Default elevation
|
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
kivy
|
kivy
|
||||||
cryptography
|
cryptography
|
||||||
kiwy-garden
|
kivy-garden
|
||||||
folium
|
folium
|
||||||
selenium
|
selenium
|
||||||
pillow
|
pillow
|
||||||
geopy
|
geopy
|
||||||
opencv-python
|
opencv-python
|
||||||
moviepy
|
|
||||||
requests
|
requests
|
||||||
numpy
|
numpy
|
||||||
matplotlib
|
matplotlib
|
||||||
scipy
|
scipy
|
||||||
|
imageio
|
||||||
|
ffmpeg-python
|
||||||
|
pydeck
|
||||||
|
plotly
|
||||||
|
dash
|
||||||
|
pandas
|
||||||
|
geopandas
|
||||||
|
bpy
|
||||||
@@ -1 +0,0 @@
|
|||||||
gAAAAABoa47dY_7ed4KPuQv7x1BWyfC8-MEwtoIo0u5lhW2Qp1BwdtL9Biry5xG0BhOGE7MgaO7-kSKJuZDiOxVzSXenDEeT0Bq7dW5GvIK8o_7Z5CN0gyXog_bBCV3FZvQ-b_s9fCkn
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
wetp_PNG9CC5432-W9H3rUbaqIurwldZxHlOgori5kY=
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
gAAAAABoY8pRV-Q85rU5krZOR_0dyq0MEBWpw35Mxz6scGhReSBw4yDI7f_-v1qmIaiEwaq0jXlNtA9T12JTY1rH4XJL6CGXTvhyChXeSAjx2xtuVtPzgrMtQZZwqdjbiy2izWUMCH71nNRNVTPHmgnQ-U0do_zxQyXuXV9gD6XI_BSS51d5B67Hg06iQzbgbqB7SJoPBfu-QGigBiAxmoF_snkfx10rnJoySx59kmI6w0ZV4lAwd_BCH1H58ylHtZWvin14Oruhu_0RWLtUipqHplYmgXskvXvtMFxOBg-1dpVq3zqZ_nW425xTWLGw4ElIGgXPYXO4cgPiDrMTTTi6y4Ymyt193r4jhVeU5A-UswEdhdEEJ4sEOV57UHdjSdPNVj8Ce3ZKAXPJ1DWQhpLCKpoLu4unQTp3V89wxZ63PcbrqglnFwtFNFmjVAQ97Q5qSZH6-VvA
|
|
||||||
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.
Binary file not shown.
@@ -3,12 +3,14 @@ from kivy.uix.screenmanager import Screen
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
|
from datetime import datetime
|
||||||
from kivy.clock import Clock
|
from kivy.clock import Clock
|
||||||
from kivy.properties import StringProperty, NumericProperty, AliasProperty
|
from kivy.properties import StringProperty, NumericProperty, AliasProperty
|
||||||
from py_scripts.utils import (
|
from py_scripts.utils import (
|
||||||
process_preview_util, optimize_route_entries_util
|
process_preview_util, optimize_route_entries_util
|
||||||
)
|
)
|
||||||
from py_scripts.video_3d_generator import generate_3d_video_animation
|
from py_scripts.advanced_3d_generator import NavigationAnimationGenerator
|
||||||
|
# BlenderGPSAnimator imported conditionally when needed
|
||||||
from kivy.uix.popup import Popup
|
from kivy.uix.popup import Popup
|
||||||
from kivy.uix.button import Button
|
from kivy.uix.button import Button
|
||||||
from kivy.uix.label import Label
|
from kivy.uix.label import Label
|
||||||
@@ -48,8 +50,6 @@ class CreateAnimationScreen(Screen):
|
|||||||
count = 0
|
count = 0
|
||||||
self.ids.route_entries_label.text = f"Your route has {count} entries,"
|
self.ids.route_entries_label.text = f"Your route has {count} entries,"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def open_rename_popup(self):
|
def open_rename_popup(self):
|
||||||
from kivy.uix.popup import Popup
|
from kivy.uix.popup import Popup
|
||||||
from kivy.uix.boxlayout import BoxLayout
|
from kivy.uix.boxlayout import BoxLayout
|
||||||
@@ -128,7 +128,6 @@ class CreateAnimationScreen(Screen):
|
|||||||
on_save=lambda: self.on_pre_enter()
|
on_save=lambda: self.on_pre_enter()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def preview_route(self):
|
def preview_route(self):
|
||||||
# Show processing popup
|
# Show processing popup
|
||||||
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||||||
@@ -165,22 +164,16 @@ class CreateAnimationScreen(Screen):
|
|||||||
0.5
|
0.5
|
||||||
)
|
)
|
||||||
|
|
||||||
def open_pauses_popup(self):
|
def generate_google_earth_animation(self):
|
||||||
"""Navigate to the pause edit screen"""
|
"""Generate Google Earth-style flythrough animation using NavigationAnimationGenerator"""
|
||||||
pause_edit_screen = self.manager.get_screen("pause_edit")
|
|
||||||
pause_edit_screen.set_project_and_callback(self.project_name, self.on_pre_enter)
|
|
||||||
self.manager.current = "pause_edit"
|
|
||||||
|
|
||||||
def generate_3d_video(self):
|
|
||||||
"""Generate a 3D video animation similar to Relive"""
|
|
||||||
# Show processing popup
|
# Show processing popup
|
||||||
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||||||
label = Label(text="Preparing 3D video generation...")
|
label = Label(text="Initializing Google Earth flythrough...")
|
||||||
progress = ProgressBar(max=100, value=0)
|
progress = ProgressBar(max=100, value=0)
|
||||||
layout.add_widget(label)
|
layout.add_widget(label)
|
||||||
layout.add_widget(progress)
|
layout.add_widget(progress)
|
||||||
popup = Popup(
|
popup = Popup(
|
||||||
title="Generating 3D Video Animation",
|
title="Generating Google Earth Flythrough",
|
||||||
content=layout,
|
content=layout,
|
||||||
size_hint=(0.9, None),
|
size_hint=(0.9, None),
|
||||||
size=(0, 200),
|
size=(0, 200),
|
||||||
@@ -188,16 +181,761 @@ class CreateAnimationScreen(Screen):
|
|||||||
)
|
)
|
||||||
popup.open()
|
popup.open()
|
||||||
|
|
||||||
# Schedule the 3D video generation
|
def run_google_earth_animation():
|
||||||
Clock.schedule_once(
|
try:
|
||||||
lambda dt: generate_3d_video_animation(
|
# Update status
|
||||||
self.project_name,
|
def update_status(progress_val, status_text):
|
||||||
RESOURCES_FOLDER,
|
def _update(dt):
|
||||||
label,
|
progress.value = progress_val
|
||||||
progress,
|
label.text = status_text
|
||||||
popup,
|
Clock.schedule_once(_update, 0)
|
||||||
Clock
|
|
||||||
),
|
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
|
||||||
0.5
|
positions_path = os.path.join(project_folder, "positions.json")
|
||||||
|
|
||||||
|
if not os.path.exists(positions_path):
|
||||||
|
update_status(0, "Error: No GPS data found")
|
||||||
|
Clock.schedule_once(lambda dt: popup.dismiss(), 2)
|
||||||
|
return
|
||||||
|
|
||||||
|
update_status(10, "Loading GPS data...")
|
||||||
|
|
||||||
|
# Check dependencies first
|
||||||
|
generator = NavigationAnimationGenerator(project_folder)
|
||||||
|
generator.check_dependencies()
|
||||||
|
|
||||||
|
update_status(20, "Processing GPS coordinates...")
|
||||||
|
df = generator.load_gps_data(positions_path)
|
||||||
|
|
||||||
|
update_status(40, "Creating Google Earth flythrough...")
|
||||||
|
output_video_path = os.path.join(project_folder, f"{self.project_name}_google_earth_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
|
||||||
|
|
||||||
|
# Progress callback for the generator
|
||||||
|
def generator_progress(progress, message):
|
||||||
|
update_status(40 + (progress * 0.5), message) # Map 0-100% to 40-90%
|
||||||
|
|
||||||
|
update_status(90, "Creating flythrough video...")
|
||||||
|
success = generator.generate_frames(positions_path, style='google_earth', progress_callback=generator_progress)
|
||||||
|
|
||||||
|
if success and len(success) > 0:
|
||||||
|
update_status(95, "Rendering final video...")
|
||||||
|
video_success = generator.create_video(success, output_video_path, generator_progress)
|
||||||
|
if video_success:
|
||||||
|
update_status(100, "Google Earth flythrough complete!")
|
||||||
|
output_path = output_video_path
|
||||||
|
else:
|
||||||
|
raise Exception("Failed to create video from frames")
|
||||||
|
else:
|
||||||
|
raise Exception("Failed to generate frames")
|
||||||
|
|
||||||
|
def show_success(dt):
|
||||||
|
popup.dismiss()
|
||||||
|
self.show_success_popup(
|
||||||
|
"Google Earth Flythrough Complete!",
|
||||||
|
f"Your Google Earth-style flythrough has been saved to:\n{output_path}",
|
||||||
|
output_path
|
||||||
|
)
|
||||||
|
|
||||||
|
Clock.schedule_once(show_success, 1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_message = str(e)
|
||||||
|
def show_error(dt):
|
||||||
|
popup.dismiss()
|
||||||
|
self.show_error_popup("Google Earth Animation Error", error_message)
|
||||||
|
|
||||||
|
Clock.schedule_once(show_error, 0)
|
||||||
|
|
||||||
|
# Schedule the animation generation
|
||||||
|
Clock.schedule_once(lambda dt: run_google_earth_animation(), 0.5)
|
||||||
|
|
||||||
|
def generate_blender_animation(self):
|
||||||
|
"""Generate cinema-quality animation using Blender (or fallback to advanced 3D)"""
|
||||||
|
# Show processing popup
|
||||||
|
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||||||
|
label = Label(text="Initializing cinema rendering pipeline...")
|
||||||
|
progress = ProgressBar(max=100, value=0)
|
||||||
|
layout.add_widget(label)
|
||||||
|
layout.add_widget(progress)
|
||||||
|
popup = Popup(
|
||||||
|
title="Generating Cinema Animation",
|
||||||
|
content=layout,
|
||||||
|
size_hint=(0.9, None),
|
||||||
|
size=(0, 200),
|
||||||
|
auto_dismiss=False
|
||||||
|
)
|
||||||
|
popup.open()
|
||||||
|
|
||||||
|
def run_blender_animation():
|
||||||
|
try:
|
||||||
|
# Update status
|
||||||
|
def update_status(progress_val, status_text):
|
||||||
|
def _update(dt):
|
||||||
|
progress.value = progress_val
|
||||||
|
label.text = status_text
|
||||||
|
Clock.schedule_once(_update, 0)
|
||||||
|
|
||||||
|
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
|
||||||
|
positions_path = os.path.join(project_folder, "positions.json")
|
||||||
|
|
||||||
|
if not os.path.exists(positions_path):
|
||||||
|
update_status(0, "Error: No GPS data found")
|
||||||
|
Clock.schedule_once(lambda dt: popup.dismiss(), 2)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if Blender is available
|
||||||
|
try:
|
||||||
|
from py_scripts.blender_animator import BLENDER_AVAILABLE, BlenderGPSAnimator
|
||||||
|
if BLENDER_AVAILABLE:
|
||||||
|
update_status(10, "Loading GPS data into Blender...")
|
||||||
|
|
||||||
|
# Use Blender for rendering
|
||||||
|
animator = BlenderGPSAnimator(project_folder)
|
||||||
|
animator.check_dependencies()
|
||||||
|
|
||||||
|
update_status(25, "Processing GPS coordinates...")
|
||||||
|
gps_data = animator.load_gps_data(positions_path)
|
||||||
|
|
||||||
|
output_video_path = os.path.join(project_folder, f"{self.project_name}_blender_cinema_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
|
||||||
|
|
||||||
|
# Progress callback for the animator
|
||||||
|
def animator_progress(progress, message):
|
||||||
|
update_status(25 + (progress * 0.6), message) # Map 0-100% to 25-85%
|
||||||
|
|
||||||
|
update_status(85, "Rendering cinema-quality video...")
|
||||||
|
success = animator.create_gps_animation(
|
||||||
|
positions_path,
|
||||||
|
output_video_path,
|
||||||
|
progress_callback=animator_progress
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
update_status(100, "Blender cinema animation complete!")
|
||||||
|
output_path = output_video_path
|
||||||
|
else:
|
||||||
|
raise Exception("Failed to generate Blender animation")
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ImportError("Blender not available")
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
# Fallback to advanced 3D animation with cinema-quality settings
|
||||||
|
update_status(10, "Blender not available - using advanced 3D cinema mode...")
|
||||||
|
|
||||||
|
# Import here to avoid startup delays
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use('Agg')
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from mpl_toolkits.mplot3d import Axes3D
|
||||||
|
import numpy as np
|
||||||
|
import cv2
|
||||||
|
|
||||||
|
# Load GPS data
|
||||||
|
with open(positions_path, 'r') as f:
|
||||||
|
positions = json.load(f)
|
||||||
|
|
||||||
|
if len(positions) < 2:
|
||||||
|
update_status(0, "Error: Need at least 2 GPS points")
|
||||||
|
Clock.schedule_once(lambda dt: popup.dismiss(), 2)
|
||||||
|
return
|
||||||
|
|
||||||
|
update_status(20, "Processing GPS coordinates for cinema rendering...")
|
||||||
|
|
||||||
|
# Extract coordinates
|
||||||
|
lats = np.array([pos['latitude'] for pos in positions])
|
||||||
|
lons = np.array([pos['longitude'] for pos in positions])
|
||||||
|
alts = np.array([pos.get('altitude', 0) for pos in positions])
|
||||||
|
timestamps = [pos.get('fixTime', '') for pos in positions]
|
||||||
|
|
||||||
|
# Convert to relative coordinates
|
||||||
|
lat_center = np.mean(lats)
|
||||||
|
lon_center = np.mean(lons)
|
||||||
|
alt_min = np.min(alts)
|
||||||
|
|
||||||
|
x = (lons - lon_center) * 111320 * np.cos(np.radians(lat_center))
|
||||||
|
y = (lats - lat_center) * 110540
|
||||||
|
z = alts - alt_min
|
||||||
|
|
||||||
|
update_status(30, "Creating cinema-quality frames...")
|
||||||
|
|
||||||
|
# Cinema settings - higher quality
|
||||||
|
frames_folder = os.path.join(project_folder, "cinema_frames")
|
||||||
|
os.makedirs(frames_folder, exist_ok=True)
|
||||||
|
|
||||||
|
fps = 24 # Cinema standard
|
||||||
|
total_frames = min(len(positions), 200) # Limit for reasonable processing time
|
||||||
|
points_per_frame = max(1, len(positions) // total_frames)
|
||||||
|
|
||||||
|
frame_files = []
|
||||||
|
|
||||||
|
# Generate cinema-quality frames
|
||||||
|
for frame_idx in range(total_frames):
|
||||||
|
current_progress = 30 + (frame_idx / total_frames) * 50
|
||||||
|
update_status(current_progress, f"Rendering cinema frame {frame_idx + 1}/{total_frames}...")
|
||||||
|
|
||||||
|
end_point = min((frame_idx + 1) * points_per_frame, len(positions))
|
||||||
|
|
||||||
|
# Create high-quality 3D plot
|
||||||
|
plt.style.use('dark_background') # Cinema-style dark theme
|
||||||
|
fig = plt.figure(figsize=(16, 12), dpi=150) # Higher resolution
|
||||||
|
ax = fig.add_subplot(111, projection='3d')
|
||||||
|
|
||||||
|
# Plot route with cinema styling
|
||||||
|
if end_point > 1:
|
||||||
|
# Gradient effect for completed route
|
||||||
|
colors = np.linspace(0, 1, end_point)
|
||||||
|
ax.scatter(x[:end_point], y[:end_point], z[:end_point],
|
||||||
|
c=colors, cmap='plasma', s=30, alpha=0.8)
|
||||||
|
ax.plot(x[:end_point], y[:end_point], z[:end_point],
|
||||||
|
color='cyan', linewidth=3, alpha=0.9)
|
||||||
|
|
||||||
|
# Current position with glow effect
|
||||||
|
if end_point > 0:
|
||||||
|
current_idx = end_point - 1
|
||||||
|
# Multiple layers for glow effect
|
||||||
|
for size, alpha in [(200, 0.3), (150, 0.5), (100, 0.8)]:
|
||||||
|
ax.scatter(x[current_idx], y[current_idx], z[current_idx],
|
||||||
|
c='yellow', s=size, alpha=alpha, marker='o')
|
||||||
|
|
||||||
|
# Trail effect
|
||||||
|
trail_start = max(0, current_idx - 10)
|
||||||
|
if current_idx > trail_start:
|
||||||
|
trail_alpha = np.linspace(0.3, 1.0, current_idx - trail_start + 1)
|
||||||
|
for i, alpha in enumerate(trail_alpha):
|
||||||
|
idx = trail_start + i
|
||||||
|
ax.scatter(x[idx], y[idx], z[idx],
|
||||||
|
c='orange', s=60, alpha=alpha)
|
||||||
|
|
||||||
|
# Remaining route preview
|
||||||
|
if end_point < len(positions):
|
||||||
|
ax.plot(x[end_point:], y[end_point:], z[end_point:],
|
||||||
|
color='gray', linewidth=1, alpha=0.4, linestyle='--')
|
||||||
|
|
||||||
|
# Cinema-style labels and styling
|
||||||
|
ax.set_xlabel('East-West (m)', color='white', fontsize=14)
|
||||||
|
ax.set_ylabel('North-South (m)', color='white', fontsize=14)
|
||||||
|
ax.set_zlabel('Elevation (m)', color='white', fontsize=14)
|
||||||
|
|
||||||
|
# Progress and time info
|
||||||
|
progress_percent = (end_point / len(positions)) * 100
|
||||||
|
timestamp_str = timestamps[end_point-1] if end_point > 0 else "Start"
|
||||||
|
ax.set_title(f'CINEMA GPS JOURNEY\nProgress: {progress_percent:.1f}% • Point {end_point}/{len(positions)} • {timestamp_str}',
|
||||||
|
color='white', fontsize=16, pad=20, weight='bold')
|
||||||
|
|
||||||
|
# Consistent view with cinematic angle
|
||||||
|
margin = max(np.ptp(x), np.ptp(y)) * 0.15
|
||||||
|
ax.set_xlim(np.min(x) - margin, np.max(x) + margin)
|
||||||
|
ax.set_ylim(np.min(y) - margin, np.max(y) + margin)
|
||||||
|
ax.set_zlim(np.min(z) - 20, np.max(z) + 20)
|
||||||
|
|
||||||
|
# Dynamic camera movement for cinematic effect
|
||||||
|
azim = 45 + (frame_idx * 0.5) % 360 # Slowly rotating view
|
||||||
|
ax.view_init(elev=25, azim=azim)
|
||||||
|
|
||||||
|
# Cinema-style grid
|
||||||
|
ax.grid(True, alpha=0.2, color='white')
|
||||||
|
ax.xaxis.pane.fill = False
|
||||||
|
ax.yaxis.pane.fill = False
|
||||||
|
ax.zaxis.pane.fill = False
|
||||||
|
|
||||||
|
# Make pane edges more subtle
|
||||||
|
ax.xaxis.pane.set_edgecolor('gray')
|
||||||
|
ax.yaxis.pane.set_edgecolor('gray')
|
||||||
|
ax.zaxis.pane.set_edgecolor('gray')
|
||||||
|
ax.xaxis.pane.set_alpha(0.1)
|
||||||
|
ax.yaxis.pane.set_alpha(0.1)
|
||||||
|
ax.zaxis.pane.set_alpha(0.1)
|
||||||
|
|
||||||
|
# Save high-quality frame
|
||||||
|
frame_path = os.path.join(frames_folder, f"cinema_frame_{frame_idx:04d}.png")
|
||||||
|
try:
|
||||||
|
plt.savefig(frame_path, dpi=150, bbox_inches='tight',
|
||||||
|
facecolor='black', edgecolor='none', format='png')
|
||||||
|
plt.close(fig)
|
||||||
|
|
||||||
|
if os.path.exists(frame_path) and os.path.getsize(frame_path) > 1024:
|
||||||
|
test_frame = cv2.imread(frame_path)
|
||||||
|
if test_frame is not None:
|
||||||
|
frame_files.append(frame_path)
|
||||||
|
if frame_idx == 0:
|
||||||
|
h, w, c = test_frame.shape
|
||||||
|
update_status(current_progress, f"Cinema quality: {w}x{h} at {fps} FPS")
|
||||||
|
except Exception as frame_error:
|
||||||
|
update_status(current_progress, f"Error creating frame {frame_idx}: {str(frame_error)}")
|
||||||
|
plt.close(fig)
|
||||||
|
continue
|
||||||
|
|
||||||
|
plt.style.use('default') # Reset style
|
||||||
|
|
||||||
|
# Create cinema video
|
||||||
|
if not frame_files:
|
||||||
|
raise Exception("No valid cinema frames were generated")
|
||||||
|
|
||||||
|
update_status(80, f"Creating cinema video from {len(frame_files)} frames...")
|
||||||
|
|
||||||
|
output_video_path = os.path.join(project_folder, f"{self.project_name}_cinema_3d_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
|
||||||
|
|
||||||
|
# Cinema video creation with higher quality
|
||||||
|
first_frame = cv2.imread(frame_files[0])
|
||||||
|
height, width, layers = first_frame.shape
|
||||||
|
|
||||||
|
# Try to create high-quality video
|
||||||
|
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
||||||
|
video_writer = cv2.VideoWriter(output_video_path, fourcc, fps, (width, height))
|
||||||
|
|
||||||
|
if video_writer.isOpened():
|
||||||
|
for i, frame_file in enumerate(frame_files):
|
||||||
|
frame = cv2.imread(frame_file)
|
||||||
|
if frame is not None:
|
||||||
|
video_writer.write(frame)
|
||||||
|
|
||||||
|
if i % 10 == 0:
|
||||||
|
progress = 80 + (i / len(frame_files)) * 8
|
||||||
|
update_status(progress, f"Encoding cinema frame {i+1}/{len(frame_files)}")
|
||||||
|
|
||||||
|
video_writer.release()
|
||||||
|
|
||||||
|
if os.path.exists(output_video_path) and os.path.getsize(output_video_path) > 1024:
|
||||||
|
update_status(90, "Cinema video created successfully")
|
||||||
|
output_path = output_video_path
|
||||||
|
else:
|
||||||
|
raise Exception("Cinema video creation failed")
|
||||||
|
else:
|
||||||
|
raise Exception("Could not initialize cinema video writer")
|
||||||
|
|
||||||
|
# Clean up frames
|
||||||
|
for frame_file in frame_files:
|
||||||
|
try:
|
||||||
|
os.remove(frame_file)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
os.rmdir(frames_folder)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
update_status(100, "Cinema animation complete!")
|
||||||
|
|
||||||
|
def show_success(dt):
|
||||||
|
popup.dismiss()
|
||||||
|
self.show_success_popup(
|
||||||
|
"Cinema Animation Complete!",
|
||||||
|
f"Your cinema-quality animation has been saved to:\n{output_path}\n\nNote: Blender was not available, so advanced 3D cinema mode was used instead.",
|
||||||
|
output_path
|
||||||
|
)
|
||||||
|
|
||||||
|
Clock.schedule_once(show_success, 1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_message = str(e)
|
||||||
|
print(f"DEBUG: Cinema animation error: {error_message}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
def show_error(dt):
|
||||||
|
popup.dismiss()
|
||||||
|
self.show_error_popup("Cinema Animation Error", error_message)
|
||||||
|
|
||||||
|
Clock.schedule_once(show_error, 0)
|
||||||
|
|
||||||
|
# Schedule the animation generation
|
||||||
|
Clock.schedule_once(lambda dt: run_blender_animation(), 0.5)
|
||||||
|
|
||||||
|
def generate_progressive_3d_animation(self):
|
||||||
|
"""Generate a progressive 3D animation that builds the trip point by point"""
|
||||||
|
# Show processing popup
|
||||||
|
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||||||
|
label = Label(text="Initializing progressive 3D animation...")
|
||||||
|
progress = ProgressBar(max=100, value=0)
|
||||||
|
layout.add_widget(label)
|
||||||
|
layout.add_widget(progress)
|
||||||
|
popup = Popup(
|
||||||
|
title="Generating Progressive 3D Animation",
|
||||||
|
content=layout,
|
||||||
|
size_hint=(0.9, None),
|
||||||
|
size=(0, 200),
|
||||||
|
auto_dismiss=False
|
||||||
|
)
|
||||||
|
popup.open()
|
||||||
|
|
||||||
|
def run_progressive_animation():
|
||||||
|
try:
|
||||||
|
# Import here to avoid startup delays
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use('Agg') # Use non-interactive backend
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from mpl_toolkits.mplot3d import Axes3D
|
||||||
|
import numpy as np
|
||||||
|
import cv2 # Use OpenCV instead of MoviePy
|
||||||
|
|
||||||
|
# Update status
|
||||||
|
def update_status(progress_val, status_text):
|
||||||
|
def _update(dt):
|
||||||
|
progress.value = progress_val
|
||||||
|
label.text = status_text
|
||||||
|
Clock.schedule_once(_update, 0)
|
||||||
|
|
||||||
|
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
|
||||||
|
positions_path = os.path.join(project_folder, "positions.json")
|
||||||
|
|
||||||
|
if not os.path.exists(positions_path):
|
||||||
|
update_status(0, "Error: No GPS data found")
|
||||||
|
Clock.schedule_once(lambda dt: popup.dismiss(), 2)
|
||||||
|
return
|
||||||
|
|
||||||
|
update_status(10, "Loading GPS data...")
|
||||||
|
|
||||||
|
# Load GPS data
|
||||||
|
with open(positions_path, 'r') as f:
|
||||||
|
positions = json.load(f)
|
||||||
|
|
||||||
|
if len(positions) < 2:
|
||||||
|
update_status(0, "Error: Need at least 2 GPS points")
|
||||||
|
Clock.schedule_once(lambda dt: popup.dismiss(), 2)
|
||||||
|
return
|
||||||
|
|
||||||
|
update_status(20, "Processing GPS coordinates...")
|
||||||
|
|
||||||
|
# Extract coordinates and timestamps
|
||||||
|
lats = [pos['latitude'] for pos in positions]
|
||||||
|
lons = [pos['longitude'] for pos in positions]
|
||||||
|
alts = [pos.get('altitude', 0) for pos in positions]
|
||||||
|
timestamps = [pos.get('fixTime', '') for pos in positions]
|
||||||
|
|
||||||
|
# Convert to numpy arrays for easier manipulation
|
||||||
|
lats = np.array(lats)
|
||||||
|
lons = np.array(lons)
|
||||||
|
alts = np.array(alts)
|
||||||
|
|
||||||
|
# Normalize coordinates for better visualization
|
||||||
|
lat_center = np.mean(lats)
|
||||||
|
lon_center = np.mean(lons)
|
||||||
|
alt_min = np.min(alts)
|
||||||
|
|
||||||
|
# Convert to relative coordinates (in meters approximately)
|
||||||
|
x = (lons - lon_center) * 111320 * np.cos(np.radians(lat_center)) # longitude to meters
|
||||||
|
y = (lats - lat_center) * 110540 # latitude to meters
|
||||||
|
z = alts - alt_min # relative altitude
|
||||||
|
|
||||||
|
update_status(30, "Creating animation frames...")
|
||||||
|
|
||||||
|
# Create frames folder
|
||||||
|
frames_folder = os.path.join(project_folder, "progressive_frames")
|
||||||
|
os.makedirs(frames_folder, exist_ok=True)
|
||||||
|
|
||||||
|
# Animation settings
|
||||||
|
fps = 10 # frames per second
|
||||||
|
points_per_frame = max(1, len(positions) // 100) # Show multiple points per frame for long routes
|
||||||
|
total_frames = len(positions) // points_per_frame
|
||||||
|
|
||||||
|
frame_files = []
|
||||||
|
|
||||||
|
# Generate frames
|
||||||
|
for frame_idx in range(total_frames):
|
||||||
|
current_progress = 30 + (frame_idx / total_frames) * 50
|
||||||
|
update_status(current_progress, f"Creating frame {frame_idx + 1}/{total_frames}...")
|
||||||
|
|
||||||
|
# Points to show in this frame
|
||||||
|
end_point = min((frame_idx + 1) * points_per_frame, len(positions))
|
||||||
|
|
||||||
|
# Create 3D plot
|
||||||
|
fig = plt.figure(figsize=(12, 9), dpi=100)
|
||||||
|
ax = fig.add_subplot(111, projection='3d')
|
||||||
|
|
||||||
|
# Plot the route progressively
|
||||||
|
if end_point > 1:
|
||||||
|
# Plot completed route in blue
|
||||||
|
ax.plot(x[:end_point], y[:end_point], z[:end_point],
|
||||||
|
'b-', linewidth=2, alpha=0.7, label='Route')
|
||||||
|
|
||||||
|
# Plot points as small dots
|
||||||
|
ax.scatter(x[:end_point], y[:end_point], z[:end_point],
|
||||||
|
c='blue', s=20, alpha=0.6)
|
||||||
|
|
||||||
|
# Highlight current position in red
|
||||||
|
if end_point > 0:
|
||||||
|
current_idx = end_point - 1
|
||||||
|
ax.scatter(x[current_idx], y[current_idx], z[current_idx],
|
||||||
|
c='red', s=100, marker='o', label='Current Position')
|
||||||
|
|
||||||
|
# Add a small trail behind current position
|
||||||
|
trail_start = max(0, current_idx - 5)
|
||||||
|
if current_idx > trail_start:
|
||||||
|
ax.plot(x[trail_start:current_idx+1],
|
||||||
|
y[trail_start:current_idx+1],
|
||||||
|
z[trail_start:current_idx+1],
|
||||||
|
'r-', linewidth=4, alpha=0.8)
|
||||||
|
|
||||||
|
# Plot remaining route in light gray (preview)
|
||||||
|
if end_point < len(positions):
|
||||||
|
ax.plot(x[end_point:], y[end_point:], z[end_point:],
|
||||||
|
'lightgray', linewidth=1, alpha=0.3, label='Remaining Route')
|
||||||
|
|
||||||
|
# Set labels and title
|
||||||
|
ax.set_xlabel('East-West (meters)')
|
||||||
|
ax.set_ylabel('North-South (meters)')
|
||||||
|
ax.set_zlabel('Elevation (meters)')
|
||||||
|
|
||||||
|
# Add progress info
|
||||||
|
progress_percent = (end_point / len(positions)) * 100
|
||||||
|
timestamp_str = timestamps[end_point-1] if end_point > 0 else "Start"
|
||||||
|
ax.set_title(f'GPS Trip Animation\nProgress: {progress_percent:.1f}% - Point {end_point}/{len(positions)}\nTime: {timestamp_str}',
|
||||||
|
fontsize=14, pad=20)
|
||||||
|
|
||||||
|
# Set consistent view limits for all frames
|
||||||
|
margin = max(np.ptp(x), np.ptp(y)) * 0.1
|
||||||
|
ax.set_xlim(np.min(x) - margin, np.max(x) + margin)
|
||||||
|
ax.set_ylim(np.min(y) - margin, np.max(y) + margin)
|
||||||
|
ax.set_zlim(np.min(z) - 10, np.max(z) + 10)
|
||||||
|
|
||||||
|
# Set viewing angle for better 3D perspective
|
||||||
|
ax.view_init(elev=20, azim=45)
|
||||||
|
|
||||||
|
# Add legend
|
||||||
|
ax.legend(loc='upper right')
|
||||||
|
|
||||||
|
# Add grid
|
||||||
|
ax.grid(True, alpha=0.3)
|
||||||
|
|
||||||
|
# Save frame with comprehensive error handling
|
||||||
|
frame_path = os.path.join(frames_folder, f"frame_{frame_idx:04d}.png")
|
||||||
|
try:
|
||||||
|
plt.savefig(frame_path, dpi=100, bbox_inches='tight',
|
||||||
|
facecolor='white', edgecolor='none',
|
||||||
|
format='png', optimize=False)
|
||||||
|
plt.close(fig)
|
||||||
|
|
||||||
|
# Verify frame was saved properly and is readable by OpenCV
|
||||||
|
if os.path.exists(frame_path) and os.path.getsize(frame_path) > 1024:
|
||||||
|
# Test if OpenCV can read the frame
|
||||||
|
test_frame = cv2.imread(frame_path)
|
||||||
|
if test_frame is not None:
|
||||||
|
frame_files.append(frame_path)
|
||||||
|
if frame_idx == 0: # Log first frame details
|
||||||
|
h, w, c = test_frame.shape
|
||||||
|
update_status(current_progress, f"First frame: {w}x{h}, size: {os.path.getsize(frame_path)} bytes")
|
||||||
|
else:
|
||||||
|
update_status(current_progress, f"Warning: Frame {frame_idx} not readable by OpenCV")
|
||||||
|
try:
|
||||||
|
os.remove(frame_path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
update_status(current_progress, f"Warning: Frame {frame_idx} too small or missing")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
update_status(current_progress, f"Error saving frame {frame_idx}: {str(e)}")
|
||||||
|
try:
|
||||||
|
plt.close(fig)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Validate frames before creating video
|
||||||
|
if not frame_files:
|
||||||
|
raise Exception("No valid frames were generated")
|
||||||
|
|
||||||
|
update_status(80, f"Creating video from {len(frame_files)} frames...")
|
||||||
|
|
||||||
|
# Create video using OpenCV with better error handling
|
||||||
|
output_video_path = os.path.join(project_folder,
|
||||||
|
f"{self.project_name}_progressive_3d_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
|
||||||
|
|
||||||
|
if frame_files:
|
||||||
|
try:
|
||||||
|
# Read first frame to get dimensions
|
||||||
|
first_frame = cv2.imread(frame_files[0])
|
||||||
|
if first_frame is None:
|
||||||
|
raise Exception(f"Could not read first frame: {frame_files[0]}")
|
||||||
|
|
||||||
|
height, width, layers = first_frame.shape
|
||||||
|
update_status(82, f"Video dimensions: {width}x{height}")
|
||||||
|
|
||||||
|
# Try different codecs for better compatibility
|
||||||
|
codecs_to_try = [
|
||||||
|
('mp4v', '.mp4'),
|
||||||
|
('XVID', '.avi'),
|
||||||
|
('MJPG', '.avi')
|
||||||
|
]
|
||||||
|
|
||||||
|
video_created = False
|
||||||
|
|
||||||
|
for codec, ext in codecs_to_try:
|
||||||
|
try:
|
||||||
|
# Update output path for different codecs
|
||||||
|
if ext != '.mp4':
|
||||||
|
test_output_path = output_video_path.replace('.mp4', ext)
|
||||||
|
else:
|
||||||
|
test_output_path = output_video_path
|
||||||
|
|
||||||
|
update_status(84, f"Trying codec {codec}...")
|
||||||
|
|
||||||
|
# Create video writer
|
||||||
|
fourcc = cv2.VideoWriter_fourcc(*codec)
|
||||||
|
video_writer = cv2.VideoWriter(test_output_path, fourcc, fps, (width, height))
|
||||||
|
|
||||||
|
if not video_writer.isOpened():
|
||||||
|
update_status(85, f"Failed to open video writer with {codec}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add frames to video
|
||||||
|
frames_written = 0
|
||||||
|
for i, frame_file in enumerate(frame_files):
|
||||||
|
frame = cv2.imread(frame_file)
|
||||||
|
if frame is not None:
|
||||||
|
# Ensure frame dimensions match
|
||||||
|
if frame.shape[:2] != (height, width):
|
||||||
|
frame = cv2.resize(frame, (width, height))
|
||||||
|
video_writer.write(frame)
|
||||||
|
frames_written += 1
|
||||||
|
|
||||||
|
if i % 10 == 0: # Update progress every 10 frames
|
||||||
|
progress = 85 + (i / len(frame_files)) * 3
|
||||||
|
update_status(progress, f"Writing frame {i+1}/{len(frame_files)} with {codec}")
|
||||||
|
|
||||||
|
video_writer.release()
|
||||||
|
|
||||||
|
# Check if video file was created and has reasonable size
|
||||||
|
if os.path.exists(test_output_path) and os.path.getsize(test_output_path) > 1024:
|
||||||
|
output_video_path = test_output_path
|
||||||
|
video_created = True
|
||||||
|
update_status(88, f"Video created successfully with {codec} ({frames_written} frames)")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
update_status(86, f"Video file not created or too small with {codec}")
|
||||||
|
|
||||||
|
except Exception as codec_error:
|
||||||
|
update_status(87, f"Error with {codec}: {str(codec_error)}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not video_created:
|
||||||
|
raise Exception("Failed to create video with any codec")
|
||||||
|
|
||||||
|
except Exception as video_error:
|
||||||
|
raise Exception(f"Video creation failed: {str(video_error)}")
|
||||||
|
|
||||||
|
update_status(90, "Cleaning up temporary files...")
|
||||||
|
|
||||||
|
# Clean up frame files
|
||||||
|
for frame_file in frame_files:
|
||||||
|
try:
|
||||||
|
os.remove(frame_file)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
os.rmdir(frames_folder)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
update_status(100, "Progressive 3D animation complete!")
|
||||||
|
|
||||||
|
def show_success(dt):
|
||||||
|
popup.dismiss()
|
||||||
|
self.show_success_popup(
|
||||||
|
"Progressive 3D Animation Complete!",
|
||||||
|
f"Your progressive 3D animation has been saved to:\n{output_video_path}\n\nThe animation shows {len(positions)} GPS points building the route progressively from start to finish.",
|
||||||
|
output_video_path
|
||||||
|
)
|
||||||
|
|
||||||
|
Clock.schedule_once(show_success, 1)
|
||||||
|
else:
|
||||||
|
raise Exception("No frames were generated")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_message = str(e)
|
||||||
|
print(f"DEBUG: Progressive animation error: {error_message}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
def show_error(dt):
|
||||||
|
popup.dismiss()
|
||||||
|
self.show_error_popup("Progressive Animation Error", error_message)
|
||||||
|
|
||||||
|
Clock.schedule_once(show_error, 0)
|
||||||
|
|
||||||
|
# Schedule the animation generation
|
||||||
|
Clock.schedule_once(lambda dt: run_progressive_animation(), 0.5)
|
||||||
|
|
||||||
|
def open_pauses_popup(self):
|
||||||
|
"""Navigate to the pause edit screen"""
|
||||||
|
pause_edit_screen = self.manager.get_screen("pause_edit")
|
||||||
|
pause_edit_screen.set_project_and_callback(self.project_name, self.on_pre_enter)
|
||||||
|
self.manager.current = "pause_edit"
|
||||||
|
|
||||||
|
def show_success_popup(self, title, message, file_path):
|
||||||
|
"""Show success popup with option to open file location"""
|
||||||
|
layout = BoxLayout(orientation='vertical', spacing=10, padding=15)
|
||||||
|
|
||||||
|
# Success message
|
||||||
|
success_label = Label(
|
||||||
|
text=message,
|
||||||
|
text_size=(None, None),
|
||||||
|
halign="center",
|
||||||
|
valign="middle"
|
||||||
|
)
|
||||||
|
layout.add_widget(success_label)
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
btn_layout = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=50)
|
||||||
|
|
||||||
|
open_folder_btn = Button(
|
||||||
|
text="Open Folder",
|
||||||
|
background_color=(0.2, 0.6, 0.9, 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ok_btn = Button(
|
||||||
|
text="OK",
|
||||||
|
background_color=(0.3, 0.7, 0.3, 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
btn_layout.add_widget(open_folder_btn)
|
||||||
|
btn_layout.add_widget(ok_btn)
|
||||||
|
layout.add_widget(btn_layout)
|
||||||
|
|
||||||
|
popup = Popup(
|
||||||
|
title=title,
|
||||||
|
content=layout,
|
||||||
|
size_hint=(0.9, 0.6),
|
||||||
|
auto_dismiss=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def open_folder(instance):
|
||||||
|
folder_path = os.path.dirname(file_path)
|
||||||
|
os.system(f'xdg-open "{folder_path}"') # Linux
|
||||||
|
popup.dismiss()
|
||||||
|
|
||||||
|
def close_popup(instance):
|
||||||
|
popup.dismiss()
|
||||||
|
|
||||||
|
open_folder_btn.bind(on_press=open_folder)
|
||||||
|
ok_btn.bind(on_press=close_popup)
|
||||||
|
|
||||||
|
popup.open()
|
||||||
|
|
||||||
|
def show_error_popup(self, title, message):
|
||||||
|
"""Show error popup"""
|
||||||
|
layout = BoxLayout(orientation='vertical', spacing=10, padding=15)
|
||||||
|
|
||||||
|
error_label = Label(
|
||||||
|
text=f"Error: {message}",
|
||||||
|
text_size=(None, None),
|
||||||
|
halign="center",
|
||||||
|
valign="middle"
|
||||||
|
)
|
||||||
|
layout.add_widget(error_label)
|
||||||
|
|
||||||
|
ok_btn = Button(
|
||||||
|
text="OK",
|
||||||
|
background_color=(0.8, 0.3, 0.3, 1),
|
||||||
|
size_hint_y=None,
|
||||||
|
height=50
|
||||||
|
)
|
||||||
|
layout.add_widget(ok_btn)
|
||||||
|
|
||||||
|
popup = Popup(
|
||||||
|
title=title,
|
||||||
|
content=layout,
|
||||||
|
size_hint=(0.8, 0.4),
|
||||||
|
auto_dismiss=False
|
||||||
|
)
|
||||||
|
|
||||||
|
ok_btn.bind(on_press=lambda x: popup.dismiss())
|
||||||
|
popup.open()
|
||||||
|
|||||||
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()
|
||||||
0
screens/pause_edit_screen.py
Normal file
0
screens/pause_edit_screen.py
Normal file
79
traccar.kv
79
traccar.kv
@@ -722,7 +722,7 @@
|
|||||||
height: 202
|
height: 202
|
||||||
size_hint_x: 1
|
size_hint_x: 1
|
||||||
|
|
||||||
# 3D Video Animation frame
|
# Progressive 3D Animation frame
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
orientation: "horizontal"
|
orientation: "horizontal"
|
||||||
size_hint_y: None
|
size_hint_y: None
|
||||||
@@ -737,7 +737,7 @@
|
|||||||
size: self.size
|
size: self.size
|
||||||
|
|
||||||
Label:
|
Label:
|
||||||
text: "Generate 3D video animation\nsimilar to Relive style"
|
text: "Generate progressive 3D animation\nBuilds trip point by point"
|
||||||
font_size: 16
|
font_size: 16
|
||||||
color: 1, 1, 1, 1
|
color: 1, 1, 1, 1
|
||||||
size_hint_x: 0.7
|
size_hint_x: 0.7
|
||||||
@@ -746,15 +746,84 @@
|
|||||||
text_size: self.size
|
text_size: self.size
|
||||||
|
|
||||||
Button:
|
Button:
|
||||||
text: "Generate\n3D Video"
|
text: "Generate\n3D Trip"
|
||||||
size_hint_x: 0.3
|
size_hint_x: 0.3
|
||||||
font_size: 14
|
font_size: 14
|
||||||
background_color: 0.8, 0.2, 0.4, 1
|
background_color: 0.2, 0.8, 0.4, 1
|
||||||
color: 1, 1, 1, 1
|
color: 1, 1, 1, 1
|
||||||
halign: "center"
|
halign: "center"
|
||||||
valign: "middle"
|
valign: "middle"
|
||||||
text_size: self.size
|
text_size: self.size
|
||||||
on_press: root.generate_3d_video()
|
on_press: root.generate_progressive_3d_animation()
|
||||||
|
|
||||||
|
# Google Earth Animation frame
|
||||||
|
BoxLayout:
|
||||||
|
orientation: "horizontal"
|
||||||
|
size_hint_y: None
|
||||||
|
height: 60
|
||||||
|
padding: [10, 10, 10, 10]
|
||||||
|
spacing: 10
|
||||||
|
canvas.before:
|
||||||
|
Color:
|
||||||
|
rgba: 0.15, 0.15, 0.18, 1
|
||||||
|
Rectangle:
|
||||||
|
pos: self.pos
|
||||||
|
size: self.size
|
||||||
|
|
||||||
|
Label:
|
||||||
|
text: "Generate Google Earth flythrough\nCinematic aerial view"
|
||||||
|
font_size: 16
|
||||||
|
color: 1, 1, 1, 1
|
||||||
|
size_hint_x: 0.7
|
||||||
|
halign: "left"
|
||||||
|
valign: "middle"
|
||||||
|
text_size: self.size
|
||||||
|
|
||||||
|
Button:
|
||||||
|
text: "Generate\nFlythrough"
|
||||||
|
size_hint_x: 0.3
|
||||||
|
font_size: 14
|
||||||
|
background_color: 0.1, 0.8, 0.1, 1
|
||||||
|
color: 1, 1, 1, 1
|
||||||
|
halign: "center"
|
||||||
|
valign: "middle"
|
||||||
|
text_size: self.size
|
||||||
|
on_press: root.generate_google_earth_animation()
|
||||||
|
|
||||||
|
# Blender Animation frame
|
||||||
|
BoxLayout:
|
||||||
|
orientation: "horizontal"
|
||||||
|
size_hint_y: None
|
||||||
|
height: 60
|
||||||
|
padding: [10, 10, 10, 10]
|
||||||
|
spacing: 10
|
||||||
|
canvas.before:
|
||||||
|
Color:
|
||||||
|
rgba: 0.15, 0.15, 0.18, 1
|
||||||
|
Rectangle:
|
||||||
|
pos: self.pos
|
||||||
|
size: self.size
|
||||||
|
|
||||||
|
Label:
|
||||||
|
text: "Generate cinema-quality animation\nProfessional 3D rendering"
|
||||||
|
font_size: 16
|
||||||
|
color: 1, 1, 1, 1
|
||||||
|
size_hint_x: 0.7
|
||||||
|
halign: "left"
|
||||||
|
valign: "middle"
|
||||||
|
text_size: self.size
|
||||||
|
|
||||||
|
Button:
|
||||||
|
text: "Generate\nCinema"
|
||||||
|
size_hint_x: 0.3
|
||||||
|
font_size: 14
|
||||||
|
background_color: 0.9, 0.6, 0.2, 1
|
||||||
|
color: 1, 1, 1, 1
|
||||||
|
halign: "center"
|
||||||
|
valign: "middle"
|
||||||
|
text_size: self.size
|
||||||
|
on_press: root.generate_blender_animation()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Widget:
|
Widget:
|
||||||
|
|||||||
Reference in New Issue
Block a user