Compare commits

...

21 Commits

Author SHA1 Message Date
7df9de12ce Repository cleanup: Remove large files, cache files, sensitive data, and junk files
- Updated .gitignore with comprehensive patterns for Python, media files, and project data
- Removed all __pycache__ directories and .pyc files from tracking
- Removed large media files (PNG frames, MP4 videos) from git history
- Removed sensitive credential files (credentials.enc, key.key, server_settings.enc)
- Removed test files and temporary data from tracking
- Removed junk_files directory from tracking
- Repository size optimization and security improvement
2025-07-15 15:20:39 +03:00
9f8c1c27dc saved 2025-07-15 14:55:51 +03:00
1d0dc05a7b uploaded 2025-07-10 15:26:18 +03:00
911143dfc5 updated strategi 2025-07-10 13:50:44 +03:00
29fd68f732 updated to generate trip 2025-07-10 13:46:05 +03:00
4fa7ed2a48 updated v 2025-07-09 16:47:17 +03:00
35d3bb8442 updated versions 2025-07-09 16:39:51 +03:00
507f526433 updated structure 2025-07-09 12:22:33 +03:00
a565cd67e1 still updating the 3d video 2025-07-08 15:26:33 +03:00
2532bf6219 updated video generation 2025-07-08 10:08:07 +03:00
a38e2b1fe9 updated app 2025-07-07 12:20:16 +03:00
c28be4e083 updated without video 2025-07-03 10:21:41 +03:00
291e5bab44 updated to video creation 2025-07-02 16:41:44 +03:00
3ccbf72599 updated 2025-07-02 14:13:34 +03:00
cb632752a3 uploadet project 2025-06-12 16:16:09 +03:00
0cc77fd89a aaaaaa 2025-06-10 07:25:21 +03:00
069227abf9 editihg ui 2025-06-08 11:45:47 +03:00
6cac2381cd saved 2025-06-07 21:09:59 +03:00
fa3a11ee4b updated to pauses 2025-06-07 21:08:23 +03:00
0ebdbc6b74 updates preview and pauses 2025-06-06 21:29:07 +03:00
6240042901 updated to utils th plot and convert to image solution 2025-06-06 16:38:52 +03:00
48 changed files with 5382 additions and 98644 deletions

37
.gitignore vendored
View File

@@ -1,2 +1,39 @@
# Ignore the virtual environment folder
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.

View File

@@ -5,7 +5,7 @@ import os
import json
from kivy.clock import Clock
from kivy.properties import StringProperty, ListProperty
from utils import (
from py_scripts.utils import (
generate_key, load_key, encrypt_data, decrypt_data,
check_server_settings, save_server_settings,
test_connection, get_devices_from_server, save_route_to_file, fetch_positions_for_selected_day
@@ -32,7 +32,8 @@ from screens.get_trip_from_server import GetTripFromServer
from screens.create_animation_screen import CreateAnimationScreen
from screens.settings_screen import SettingsScreen
from screens.settings_screen import RegisterScreen
from config import RESOURCES_FOLDER, CREDENTIALS_FILE
from screens.pause_edit_screen_improved import PauseEditScreen
from config import RESOURCES_FOLDER
kivy.require("2.0.0")
from kivy.core.window import Window
Window.size = (360, 780)
@@ -49,6 +50,7 @@ class TraccarApp(App):
sm.add_widget(SettingsScreen(name="settings"))
sm.add_widget(RegisterScreen(name="register"))
sm.add_widget(CreateAnimationScreen(name="create_animation"))
sm.add_widget(PauseEditScreen(name="pause_edit"))
print("Screens added to ScreenManager:", [screen.name for screen in sm.screens])
return sm

46
proba
View File

@@ -1,46 +0,0 @@
class CreateAnimationScreen(Screen):
project_name = StringProperty("")
preview_html_path = StringProperty("") # Path to the HTML file for preview
preview_image_path = StringProperty("") # Add this line
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=(None, None), size=(350, 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()

View File

@@ -0,0 +1,242 @@
# Professional Google Earth-Style 3D Video Animation
## Overview
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.
## Major Visual Enhancements
### Realistic Google Earth Visuals
- **Authentic Earth Sphere Rendering**: Realistic planetary view from space with proper curvature
- **Professional Terrain Textures**: Multi-layer terrain with forests, mountains, plains, deserts, and water bodies
- **Geographic Feature Simulation**: Coastlines, rivers, and landmasses with fractal-like detail
- **Atmospheric Scattering**: Realistic atmospheric effects and color gradients
- **Cloud Layer Rendering**: Dynamic cloud formations with proper shadows
### 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)
- **Frame Rate**: 30 FPS (smooth motion)
- **Format**: MP4 video (universal 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)
## Advanced Animation Features
- **Space Entry Sequence**: Spectacular 3-second descent from space to route
- **Earth Curvature Rendering**: Realistic planetary curvature at high altitudes
- **Atmospheric Transition**: Smooth space-to-atmosphere visual effects
- **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
### Core Dependencies
- **OpenCV (cv2)**: Video generation and frame composition
- **NumPy**: Mathematical operations and array handling
- **PIL/Pillow**: Image processing and text rendering
- **Requests**: API calls for elevation data (future enhancement)
### Optional Enhancements
- **MoviePy**: Advanced video editing and effects
- **Matplotlib**: Additional visualization options
- **SciPy**: Mathematical transformations
## Usage
1. **Navigate** to the Create Animation screen
2. **Select** a project with GPS route data
3. **Click** "Generate 3D Video" button
4. **Wait** for processing (can take several minutes)
5. **View** the generated video in the project folder
## Enhanced Processing Pipeline
### 1. Route Analysis & Camera Planning (10-20%)
- Advanced GPS data analysis and validation
- Dynamic camera path calculation
- Elevation profile generation
- Viewport optimization for route coverage
### 2. 3D Scene Setup (20-30%)
- Camera position and target calculation
- 3D coordinate system establishment
- Terrain mesh generation
- Lighting and atmosphere setup
### 3. Enhanced Frame Generation (30-75%)
- Dynamic camera positioning for each frame
- 3D-to-2D perspective projection
- Depth-sorted object rendering
- Advanced route visualization with gradients
- Multi-layer UI element composition
- Atmospheric effect application
### 4. Video Assembly & Optimization (75-90%)
- Frame sequence compilation
- Advanced compression with quality optimization
- Metadata embedding
- Audio track preparation (future enhancement)
### 5. Post-Processing & Output (90-100%)
- Final quality optimization
- File system integration
- 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
### Naming Convention
```
{project_name}_3d_animation_{timestamp}.mp4
```
### Example
```
MyTrip_3d_animation_20250702_143522.mp4
```
### Location
Videos are saved in the project folder:
```
resources/projects/{project_name}/
```
## Customization Options
### Future Enhancements
- **Real Elevation Data**: Integration with elevation APIs
- **Custom Colors**: User-selectable color schemes
- **Speed Control**: Variable playback speeds
- **Camera Angles**: Multiple perspective options
- **Terrain Textures**: Realistic ground textures
- **Weather Effects**: Animated weather overlays
### Performance Optimization
- **Multi-threading**: Parallel frame generation
- **GPU Acceleration**: OpenGL rendering support
- **Compression Options**: Quality vs. file size settings
- **Preview Mode**: Lower quality for faster processing
## Error Handling
### Common Issues
- **Insufficient GPS Data**: Minimum 10 points required
- **Memory Limitations**: Large routes may require optimization
- **Storage Space**: Videos can be 50-200MB depending on route length
- **Processing Time**: Can take 5-15 minutes for long routes
### Troubleshooting
- **Reduce Route Size**: Use route optimization before generation
- **Free Disk Space**: Ensure adequate storage available
- **Close Other Apps**: Free memory for processing
- **Check File Permissions**: Ensure write access to project folder
## Technical Architecture
### Frame Generation Pipeline
```
GPS Point → Coordinate Transform → 3D Projection →
Visual Effects → Text Overlay → Frame Export
```
### Video Assembly Pipeline
```
Frame Sequence → Video Encoder → Compression →
Metadata Addition → File Output
```
### Memory Management
- **Temporary Files**: Frames stored in temp directory
- **Batch Processing**: Processes frames in chunks
- **Automatic Cleanup**: Removes temporary files after completion
## Integration
### UI Integration
- **Progress Bar**: Real-time processing updates
- **Status Messages**: Step-by-step progress information
- **Error Dialogs**: User-friendly error messages
- **Result Notification**: Success/failure feedback
### File System Integration
- **Project Structure**: Maintains existing folder organization
- **Automatic Naming**: Prevents file name conflicts
- **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.

View File

@@ -0,0 +1,105 @@
# Pause Edit Screen Improvements
## Overview
The pause edit screen has been completely redesigned for better mobile usability and enhanced user experience.
## New Features Implemented
### 1. Loading Popup
- **Purpose**: Indicates that the app is loading pause data and location suggestions
- **Implementation**: Shows a progress bar animation while loading data in background
- **User Experience**: Prevents the app from appearing unresponsive during startup
### 2. Carousel Navigation
- **When**: Automatically activated when there are more than 2 pauses
- **Features**:
- Swipe navigation between pauses
- Loop mode for continuous navigation
- Visual indicators showing current pause (e.g., "Pause 2 of 5")
- **Fallback**: Simple scroll view for 1-2 pauses
### 3. Vertical Photo Scrolling
- **Implementation**: Each pause has a vertical scroll area for photos
- **Features**:
- Thumbnail image previews (55px width)
- Traditional vertical list layout for better mobile usability
- Improved photo item styling with borders and file information
- View and delete buttons for each photo
- File size and format information display
### 4. Enhanced Location Suggestions
- **Caching**: Location suggestions are pre-loaded and cached during startup
- **Multi-strategy**: Uses multiple approaches to find meaningful location names
- **Fallback**: Graceful degradation to coordinates if no location found
### 5. Mobile-Optimized UI
- **Responsive Design**: Better layout for phone screens
- **Touch-Friendly**: Larger buttons and touch targets
- **Visual Feedback**: Better borders, colors, and spacing
### 6. Delete Pause Functionality
- **Purpose**: Allow users to completely remove unwanted pauses
- **Implementation**: Delete button next to save button for each pause
- **Features**:
- Confirmation dialog before deletion
- Removes pause from locations list
- Deletes all associated photos and folder
- Automatically reorganizes remaining pause folders
- Updates pause numbering sequence
## Updated Features (Latest Changes)
### Photo Scrolling Direction Changed
- **From**: Horizontal scrolling with large previews
- **To**: Vertical scrolling with compact thumbnail layout
- **Benefit**: Better mobile usability and more familiar interface
### Delete Pause Button Added
- **Location**: Next to "Save Pause Info" button
- **Functionality**: Complete pause removal with confirmation
- **Safety**: Confirmation dialog prevents accidental deletion
- **Clean-up**: Automatic folder reorganization and numbering
## File Structure
- `pause_edit_screen_improved.py`: New, clean implementation with all features
- `pause_edit_screen_legacy.py`: Original file (renamed for backup)
- `main.py`: Updated to use the improved version
## Technical Details
### Loading Process
1. Show loading popup immediately
2. Load pause data in background thread
3. Pre-process location suggestions
4. Build UI on main thread
5. Dismiss loading popup
### Carousel Logic
```python
if len(pauses) > 2:
use_carousel_layout()
else:
use_simple_scroll_layout()
```
### Photo Scrolling
- Vertical ScrollView with `do_scroll_y=True, do_scroll_x=False`
- Fixed-height photo items (60px)
- Dynamic content height based on number of photos
- Thumbnail layout with file information display
## Benefits
1. **Improved Performance**: Background loading prevents UI freezing
2. **Better Navigation**: Carousel makes it easy to navigate many pauses
3. **Enhanced Photo Management**: Vertical scrolling provides familiar mobile interface
4. **Professional Feel**: Loading indicators and smooth animations
5. **Mobile-First**: Optimized for touch interaction
6. **Complete Control**: Can delete unwanted pauses with safety confirmation
7. **Better Organization**: Automatic reorganization maintains clean folder structure
## Usage
The improved screen is now the default pause edit screen in the application. Users will automatically see:
- Loading popup on screen entry
- Carousel navigation for 3+ pauses
- Horizontal photo scrolling in each pause
- Cached location suggestions for faster loading

2
py_scripts/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
# py_scripts package
# Contains utility scripts for the Traccar Animation application

File diff suppressed because it is too large Load Diff

View File

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

492
py_scripts/utils.py Normal file
View File

@@ -0,0 +1,492 @@
import os
import json
import requests
from cryptography.fernet import Fernet
import math
import datetime
RESOURCES_FOLDER = "resources"
CREDENTIALS_FILE = os.path.join(RESOURCES_FOLDER, "credentials.enc")
KEY_FILE = os.path.join(RESOURCES_FOLDER, "key.key")
SERVER_SETTINGS_FILE = os.path.join(RESOURCES_FOLDER, "server_settings.enc")
# --- Encryption Utilities ---
def generate_key():
"""Generate and save a key for encryption."""
if not os.path.exists(KEY_FILE):
key = Fernet.generate_key()
with open(KEY_FILE, "wb") as key_file:
key_file.write(key)
def load_key():
"""Load the encryption key."""
with open(KEY_FILE, "rb") as key_file:
return key_file.read()
def encrypt_data(data):
"""Encrypt data using the encryption key."""
key = load_key()
fernet = Fernet(key)
return fernet.encrypt(data.encode())
def decrypt_data(data):
"""Decrypt data using the encryption key."""
key = load_key()
fernet = Fernet(key)
return fernet.decrypt(data).decode()
# --- Server Settings ---
def check_server_settings():
"""Load and decrypt server settings from file."""
if not os.path.exists(SERVER_SETTINGS_FILE):
return None
try:
with open(SERVER_SETTINGS_FILE, "rb") as file:
encrypted_data = file.read()
decrypted_data = decrypt_data(encrypted_data)
settings = json.loads(decrypted_data)
return settings
except Exception as e:
print(f"Failed to load server settings: {e}")
return None
def save_server_settings(settings_data):
"""Encrypt and save server settings."""
encrypted_data = encrypt_data(json.dumps(settings_data))
with open(SERVER_SETTINGS_FILE, "wb") as file:
file.write(encrypted_data)
# --- Traccar Server Connection ---
def test_connection(server_url, username=None, password=None, token=None):
"""
Test the connection with the Traccar server.
Returns: dict with 'status' (bool) and 'message' (str)
"""
if not server_url:
return {"status": False, "message": "Please provide the server URL."}
if not token and (not username or not password):
return {"status": False, "message": "Please provide either a token or username and password."}
try:
headers = {"Authorization": f"Bearer {token}"} if token else None
auth = None if token else (username, password)
response = requests.get(f"{server_url}/api/server", headers=headers, auth=auth, timeout=10)
if response.status_code == 200:
return {"status": True, "message": "Connection successful! Server is reachable."}
else:
return {"status": False, "message": f"Error: {response.status_code} - {response.reason}"}
except requests.exceptions.Timeout:
return {"status": False, "message": "Connection timed out. Please try again."}
except requests.exceptions.RequestException as e:
return {"status": False, "message": f"Connection failed: {str(e)}"}
# --- Device Fetching ---
def get_devices_from_server():
"""Retrieve a mapping of device names to IDs from the Traccar server."""
settings = check_server_settings()
if not settings:
return None
server_url = settings.get("server_url")
token = settings.get("token")
if not server_url or not token:
return None
headers = {"Authorization": f"Bearer {token}"}
try:
response = requests.get(f"{server_url}/api/devices", headers=headers)
if response.status_code == 200:
devices = response.json()
return {device.get("name", "Unnamed Device"): device.get("id", "Unknown ID") for device in devices}
else:
print(f"Error: {response.status_code} - {response.reason}")
return None
except Exception as e:
print(f"Error retrieving devices: {str(e)}")
return None
# --- Route Saving ---
def save_route_to_file(route_name, positions, base_folder="resources/projects"):
"""
Save the given positions as a route in resources/projects/<route_name>/positions.json.
Returns (success, message, file_path)
"""
if not route_name:
return False, "Please enter a route name.", None
if not positions:
return False, "No positions to save.", None
folder_path = os.path.join(base_folder, route_name)
os.makedirs(folder_path, exist_ok=True)
file_path = os.path.join(folder_path, "positions.json")
try:
with open(file_path, "w") as f:
json.dump(positions, f, indent=2)
return True, f"Route '{route_name}' saved!", file_path
except Exception as e:
return False, f"Failed to save route: {str(e)}", None
def fetch_positions(server_url, token, device_id, from_time, to_time):
"""
Fetch positions from the Traccar API.
Returns (positions, error_message)
"""
url = f"{server_url}/api/reports/route"
headers = {"Authorization": f"Bearer {token}", "Accept": "application/json"}
params = {
"deviceId": device_id,
"from": from_time,
"to": to_time
}
try:
response = requests.get(url, params=params, headers=headers, timeout=15)
if response.status_code == 200:
return response.json(), None
elif response.status_code == 400:
return None, "Bad Request: Please check the request payload and token."
else:
return None, f"Failed: {response.status_code} - {response.reason}"
except requests.exceptions.RequestException as e:
return None, f"Error fetching positions: {str(e)}"
def fetch_positions_for_selected_day(settings, device_mapping, device_name, start_date, end_date, start_hour, end_hour):
"""
Fetch positions for the selected day/device using Traccar API.
Returns (positions, error_message)
"""
if not settings:
return [], "Server settings not found."
server_url = settings.get("server_url")
token = settings.get("token")
device_id = device_mapping.get(device_name)
if not device_id:
return [], "Device ID not found."
from_time = f"{start_date}T{start_hour}:00:00Z"
to_time = f"{end_date}T{end_hour}:59:59Z"
positions, error = fetch_positions(server_url, token, device_id, from_time, to_time)
if error:
return [], error
return positions, None
def html_to_image(html_path, img_path, width=1280, height=720, delay=2, driver_path='/usr/bin/chromedriver'):
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from PIL import Image
import time
import os
selenium_height = int(height * 1.2) # 10% taller for compensation
chrome_options = Options()
chrome_options.add_argument("--headless")
chrome_options.add_argument(f"--window-size={width},{selenium_height}")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
service = Service(driver_path)
driver = webdriver.Chrome(service=service, options=chrome_options)
try:
driver.set_window_size(width, selenium_height)
driver.get("file://" + os.path.abspath(html_path))
time.sleep(delay)
tmp_img = img_path + ".tmp.png"
driver.save_screenshot(tmp_img)
driver.quit()
img = Image.open(tmp_img)
img = img.crop((0, 0, width, height)) # Crop to original map size
img.save(img_path)
os.remove(tmp_img)
print(f"Image saved to: {img_path} ({width}x{height})")
except Exception as e:
print(f"Error converting HTML to image: {e}")
driver.quit()
def process_preview_util(
project_name,
RESOURCES_FOLDER,
label,
progress,
popup,
preview_image_widget,
set_preview_image_path,
Clock,
width=800,
height=600
):
import folium
import os
import json
# Import html_to_image function from within the same module
# (it's defined later in this file)
try:
project_folder = os.path.join(RESOURCES_FOLDER, "projects", project_name)
positions_path = os.path.join(project_folder, "positions.json")
html_path = os.path.join(project_folder, "preview.html")
img_path = os.path.join(project_folder, "preview.png")
if not os.path.exists(positions_path):
label.text = "positions.json not found!"
progress.value = 100
return
with open(positions_path, "r") as f:
positions = json.load(f)
if not positions:
label.text = "No positions to preview."
progress.value = 100
return
coords = [(pos['latitude'], pos['longitude']) for pos in positions]
width, height = 1280, 720 # 16:9 HD
m = folium.Map(
location=coords[0],
width=width,
height=height,
control_scale=True
)
folium.PolyLine(coords, color="blue", weight=4.5, opacity=1).add_to(m)
folium.Marker(coords[0], tooltip="Start", icon=folium.Icon(color="green")).add_to(m)
folium.Marker(coords[-1], tooltip="End", icon=folium.Icon(color="red")).add_to(m)
# --- Add pause markers if pauses.json exists ---
pauses_path = os.path.join(project_folder, "pauses.json")
if os.path.exists(pauses_path):
with open(pauses_path, "r") as pf:
pauses = json.load(pf)
for pause in pauses:
lat = pause["location"]["latitude"]
lon = pause["location"]["longitude"]
duration = pause["duration_seconds"]
start = pause["start_time"]
end = pause["end_time"]
folium.Marker(
[lat, lon],
tooltip=f"Pause: {duration//60} min {duration%60} sec",
popup=f"Pause from {start} to {end} ({duration//60} min {duration%60} sec)",
icon=folium.Icon(color="orange", icon="pause", prefix="fa")
).add_to(m)
m.fit_bounds(coords, padding=(80, 80))
m.get_root().html.add_child(folium.Element(f"""
<style>
html, body {{
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}}
#{m.get_name()} {{
position: absolute;
top: 0; bottom: 0; left: 0; right: 0;
width: 100vw;
height: 100vh;
}}
</style>
"""))
m.save(html_path)
html_to_image(html_path, img_path, width=width, height=height)
set_preview_image_path(img_path)
preview_image_widget.reload()
label.text = "Preview ready!"
progress.value = 100
def close_popup(dt):
popup.dismiss()
Clock.schedule_once(close_popup, 1)
except Exception as e:
label.text = f"Error: {e}"
progress.value = 100
def close_popup(dt):
popup.dismiss()
Clock.schedule_once(close_popup, 2)
def haversine(lat1, lon1, lat2, lon2):
# Returns distance in meters between two lat/lon points
R = 6371000 # Earth radius in meters
phi1 = math.radians(lat1)
phi2 = math.radians(lat2)
dphi = math.radians(lat2 - lat1)
dlambda = math.radians(lon2 - lon1)
a = math.sin(dphi/2)**2 + math.cos(phi1)*math.cos(phi2)*math.sin(dlambda/2)**2
return 2 * R * math.asin(math.sqrt(a))
def optimize_route_entries_util(
project_name,
RESOURCES_FOLDER,
label,
progress,
popup,
Clock,
on_save=None
):
def process_entries(dt):
project_folder = os.path.join(RESOURCES_FOLDER, "projects", project_name)
positions_path = os.path.join(project_folder, "positions.json")
pauses_path = os.path.join(project_folder, "pauses.json")
if not os.path.exists(positions_path):
label.text = "positions.json not found!"
progress.value = 100
return
with open(positions_path, "r") as f:
positions = json.load(f)
# Detect duplicate positions at the start
start_remove = 0
if positions:
first = positions[0]
for pos in positions:
if pos['latitude'] == first['latitude'] and pos['longitude'] == first['longitude']:
start_remove += 1
else:
break
if start_remove > 0:
start_remove -= 1
# Detect duplicate positions at the end
end_remove = 0
if positions:
last = positions[-1]
for pos in reversed(positions):
if pos['latitude'] == last['latitude'] and pos['longitude'] == last['longitude']:
end_remove += 1
else:
break
if end_remove > 0:
end_remove -= 1
# Shorten the positions list
new_positions = positions[start_remove:len(positions)-end_remove if end_remove > 0 else None]
# --- PAUSE DETECTION ---
pauses = []
if new_positions:
pause_start = None
pause_end = None
pause_location = None
for i in range(1, len(new_positions)):
prev = new_positions[i-1]
curr = new_positions[i]
# Check if stopped (same location)
if curr['latitude'] == prev['latitude'] and curr['longitude'] == prev['longitude']:
if pause_start is None:
pause_start = prev['deviceTime']
pause_location = (prev['latitude'], prev['longitude'])
pause_end = curr['deviceTime']
else:
if pause_start and pause_end:
# Calculate pause duration
t1 = datetime.datetime.fromisoformat(pause_start.replace('Z', '+00:00'))
t2 = datetime.datetime.fromisoformat(pause_end.replace('Z', '+00:00'))
duration = (t2 - t1).total_seconds()
if duration >= 120:
pauses.append({
"start_time": pause_start,
"end_time": pause_end,
"duration_seconds": int(duration),
"location": {"latitude": pause_location[0], "longitude": pause_location[1]}
})
pause_start = None
pause_end = None
pause_location = None
# Check for pause at the end
if pause_start and pause_end:
t1 = datetime.datetime.fromisoformat(pause_start.replace('Z', '+00:00'))
t2 = datetime.datetime.fromisoformat(pause_end.replace('Z', '+00:00'))
duration = (t2 - t1).total_seconds()
if duration >= 120:
pauses.append({
"start_time": pause_start,
"end_time": pause_end,
"duration_seconds": int(duration),
"location": {"latitude": pause_location[0], "longitude": pause_location[1]}
})
# --- FILTER PAUSES ---
# 1. Remove pauses near start/end
filtered_pauses = []
if new_positions and pauses:
start_lat, start_lon = new_positions[0]['latitude'], new_positions[0]['longitude']
end_lat, end_lon = new_positions[-1]['latitude'], new_positions[-1]['longitude']
for pause in pauses:
plat = pause["location"]["latitude"]
plon = pause["location"]["longitude"]
dist_start = haversine(start_lat, start_lon, plat, plon)
dist_end = haversine(end_lat, end_lon, plat, plon)
if dist_start < 50 or dist_end < 50:
continue # Skip pauses near start or end
filtered_pauses.append(pause)
else:
filtered_pauses = pauses
# 2. Merge pauses close in time and space
merged_pauses = []
filtered_pauses.sort(key=lambda p: p["start_time"])
for pause in filtered_pauses:
if not merged_pauses:
merged_pauses.append(pause)
else:
last = merged_pauses[-1]
# Time difference in seconds
t1 = datetime.datetime.fromisoformat(last["end_time"].replace('Z', '+00:00'))
t2 = datetime.datetime.fromisoformat(pause["start_time"].replace('Z', '+00:00'))
time_diff = (t2 - t1).total_seconds()
# Distance in meters
last_lat = last["location"]["latitude"]
last_lon = last["location"]["longitude"]
plat = pause["location"]["latitude"]
plon = pause["location"]["longitude"]
dist = haversine(last_lat, last_lon, plat, plon)
if time_diff < 300 and dist < 50:
# Merge: extend last pause's end_time and duration
last["end_time"] = pause["end_time"]
last["duration_seconds"] += pause["duration_seconds"]
else:
merged_pauses.append(pause)
pauses = merged_pauses
progress.value = 100
label.text = (
f"Entries removable at start: {start_remove}\n"
f"Entries removable at end: {end_remove}\n"
f"Detected pauses: {len(pauses)}"
)
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
btn_save = Button(text="Save optimized file", background_color=(0.008, 0.525, 0.290, 1))
btn_cancel = Button(text="Cancel")
btn_box = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=44)
btn_box.add_widget(btn_save)
btn_box.add_widget(btn_cancel)
popup.content.add_widget(btn_box)
def save_optimized(instance):
with open(positions_path, "w") as f:
json.dump(new_positions, f, indent=2)
with open(pauses_path, "w") as f:
json.dump(pauses, f, indent=2)
label.text = "File optimized and pauses saved!"
btn_save.disabled = True
btn_cancel.disabled = True
def close_and_refresh(dt):
popup.dismiss()
if on_save:
on_save()
Clock.schedule_once(close_and_refresh, 1)
btn_save.bind(on_press=save_optimized)
btn_cancel.bind(on_press=lambda x: popup.dismiss())
Clock.schedule_once(process_entries, 0.5)

View File

@@ -1,6 +0,0 @@
kivy
cryptography
kiwy-garden
folium
selenium
pillow

20
requirements.txt Normal file
View File

@@ -0,0 +1,20 @@
kivy
cryptography
kivy-garden
folium
selenium
pillow
geopy
opencv-python
requests
numpy
matplotlib
scipy
imageio
ffmpeg-python
pydeck
plotly
dash
pandas
geopandas
bpy

View File

@@ -1 +0,0 @@
gAAAAABoQuJn-THhBcB9uQut4cng4vNqljWnzVOe-jvl4j8_nDzq1KiWNF5G2BKJCxy-u2Lf72PE9WMHOA7n2EMYsLzwmF0mi_2me3DnrckEE4kaC4reSowP0AiiKNdYqrZVFcemUf7w

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,169 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.3/dist/leaflet.js"></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.3/dist/leaflet.css"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css"/>
<link rel="stylesheet" href="https://netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-glyphicons.css"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.2.0/css/all.min.css"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.css"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/python-visualization/folium/folium/templates/leaflet.awesome.rotate.min.css"/>
<meta name="viewport" content="width=device-width,
initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<style>
#map_bf67b2d92a9afef5449c1b1b9845da94 {
position: relative;
width: 100.0%;
height: 100.0%;
left: 0.0%;
top: 0.0%;
}
.leaflet-container { font-size: 1rem; }
</style>
<style>html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
</style>
<style>#map {
position:absolute;
top:0;
bottom:0;
right:0;
left:0;
}
</style>
<script>
L_NO_TOUCH = false;
L_DISABLE_3D = false;
</script>
</head>
<body>
<div class="folium-map" id="map_bf67b2d92a9afef5449c1b1b9845da94" ></div>
</body>
<script>
var map_bf67b2d92a9afef5449c1b1b9845da94 = L.map(
"map_bf67b2d92a9afef5449c1b1b9845da94",
{
center: [45.805146666666666, 24.126355555555556],
crs: L.CRS.EPSG3857,
...{
"zoom": 14,
"zoomControl": true,
"preferCanvas": false,
}
}
);
var tile_layer_48f00dde609689cd95b3e5b1020d2d03 = L.tileLayer(
"https://tile.openstreetmap.org/{z}/{x}/{y}.png",
{
"minZoom": 0,
"maxZoom": 19,
"maxNativeZoom": 19,
"noWrap": false,
"attribution": "\u0026copy; \u003ca href=\"https://www.openstreetmap.org/copyright\"\u003eOpenStreetMap\u003c/a\u003e contributors",
"subdomains": "abc",
"detectRetina": false,
"tms": false,
"opacity": 1,
}
);
tile_layer_48f00dde609689cd95b3e5b1020d2d03.addTo(map_bf67b2d92a9afef5449c1b1b9845da94);
var poly_line_ac2b74b1096aa06a1d4ab84860beacdc = L.polyline(
[[45.805146666666666, 24.126355555555556], [45.80562444444445, 24.123990555555554], [45.805820555555556, 24.122884444444445], [45.806001111111115, 24.121864444444444], [45.80658944444445, 24.118647777777777], [45.80706166666667, 24.11584], [45.80744277777778, 24.113130555555554], [45.80744444444444, 24.111027777777778], [45.807554999999994, 24.10904111111111], [45.80765388888889, 24.10791777777778], [45.80775722222222, 24.106204444444444], [45.80775722222222, 24.106204444444444], [45.807792777777784, 24.10529888888889], [45.80769222222222, 24.105220555555558], [45.807494444444444, 24.10537666666667], [45.80721722222222, 24.10552888888889], [45.80721722222222, 24.10552888888889], [45.80452833333334, 24.106312222222222], [45.80452833333334, 24.106312222222222], [45.802245000000006, 24.106793888888888], [45.802245000000006, 24.106793888888888], [45.80039166666667, 24.107621666666667], [45.80039166666667, 24.107621666666667], [45.79863111111111, 24.10826], [45.79706388888889, 24.109215], [45.796372222222224, 24.109560000000002], [45.79611444444444, 24.109526666666667], [45.79596611111111, 24.109244999999998], [45.79575722222222, 24.107441666666666], [45.79575722222222, 24.107441666666666], [45.79544, 24.105129444444444], [45.79544, 24.105129444444444], [45.795164444444445, 24.103232777777777], [45.794825555555555, 24.100786111111113], [45.79484444444444, 24.10045277777778], [45.79482, 24.100100555555557], [45.79452388888888, 24.098648333333333], [45.794362222222226, 24.097596666666668], [45.794362222222226, 24.097596666666668], [45.794362222222226, 24.097596666666668], [45.79418555555556, 24.09649111111111], [45.79419388888889, 24.096272777777777], [45.79433111111111, 24.095743333333335], [45.795445, 24.094136111111112], [45.796870000000006, 24.09261777777778], [45.797534444444445, 24.091910555555554], [45.79878277777778, 24.090588888888888], [45.79978833333333, 24.089429444444445], [45.799776111111115, 24.089080555555554], [45.79944055555555, 24.086607777777775], [45.79913277777778, 24.086008333333332], [45.79909722222222, 24.08582277777778], [45.79911555555555, 24.085697222222223], [45.79911555555555, 24.085697222222223], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79921, 24.085612222222224]],
{"bubblingMouseEvents": true, "color": "blue", "dashArray": null, "dashOffset": null, "fill": false, "fillColor": "blue", "fillOpacity": 0.2, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "noClip": false, "opacity": 1, "smoothFactor": 1.0, "stroke": true, "weight": 4.5}
).addTo(map_bf67b2d92a9afef5449c1b1b9845da94);
var marker_508cb899cfad4984ec8c6bacbc7d4450 = L.marker(
[45.805146666666666, 24.126355555555556],
{
}
).addTo(map_bf67b2d92a9afef5449c1b1b9845da94);
var icon_27f251d0f4b490eac1364fdc7c0e4bcb = L.AwesomeMarkers.icon(
{
"markerColor": "green",
"iconColor": "white",
"icon": "info-sign",
"prefix": "glyphicon",
"extraClasses": "fa-rotate-0",
}
);
marker_508cb899cfad4984ec8c6bacbc7d4450.bindTooltip(
`<div>
Start
</div>`,
{
"sticky": true,
}
);
marker_508cb899cfad4984ec8c6bacbc7d4450.setIcon(icon_27f251d0f4b490eac1364fdc7c0e4bcb);
var marker_fdc9ee2260616a462ff09a9869579e98 = L.marker(
[45.79921, 24.085612222222224],
{
}
).addTo(map_bf67b2d92a9afef5449c1b1b9845da94);
var icon_5ab5f7e75df70e10f7a0380fd99ababf = L.AwesomeMarkers.icon(
{
"markerColor": "red",
"iconColor": "white",
"icon": "info-sign",
"prefix": "glyphicon",
"extraClasses": "fa-rotate-0",
}
);
marker_fdc9ee2260616a462ff09a9869579e98.bindTooltip(
`<div>
End
</div>`,
{
"sticky": true,
}
);
marker_fdc9ee2260616a462ff09a9869579e98.setIcon(icon_5ab5f7e75df70e10f7a0380fd99ababf);
</script>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 546 KiB

View File

@@ -1 +0,0 @@
gAAAAABoPu5x585IL9U8GSHw4j-KQTpHJixfiwEHQf9KHR25D2fFcYDz6HrJzFP4U3iFxcV9dQQ1VhgDfDPO_nVDafVjMz9kiJdbp1KtiSyB8odqNmq1v6ZfLr_YXqzqNhMHfuA1zr4NgUkaivF-dQr84Z4WA4i1crmR-BA7tMIQti7rDjtmIxQATfKrNw1zD5yYrDiI2jOkUAGiJ1hIY0Ue-x0wmykzktwD_xIsixxX3IOeqgY39gZ7XmwRYA4boZsSbWqqmVDgjBElaUYCUKlp_t-50vHeMNySt5AHDwmY3cOb0zePMEVYzQiKMOTRsSMrAavnIquY6BHytWKOJuuOoWS5aTiuy1YGw6wMQZT7MFcza9u4iYjJm39cdLnGl4tWn8StvawbXepPFqrwcoJXAfkvd8f--eCPuAXIFi__EMM0jlO2PGSbj-5YjFnCdKspnycrlLB6

View File

@@ -1,9 +0,0 @@
To update the colors to the specified values, we will convert the hex color codes to RGBA format (values between 0 and 1) and update the `server_box_color` property in the `HomeScreen` class.
Here are the RGBA equivalents of the provided hex colors:
- **Yellow (#FB8D14)**: `(0.984, 0.553, 0.078, 1)`
- **Red (#E8083E)**: `(0.909, 0.031, 0.243, 1)`
- **Green (#02864A)**: `(0.008, 0.525, 0.290, 1)`
The RGBA equivalent of `#573CFA` is `(0.341, 0.235, 0.980, 1)`.

View File

@@ -1,3 +0,0 @@
python -m venv track
source track/bin/activate
pip install -r reqirements.txt

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +0,0 @@
import requests
def get_device_route(server_url, token, device_id, from_time, to_time):
"""
Fetch all positions for a device in a time frame from Traccar server.
"""
url = f"{server_url}/reports/route"
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/json"
}
params = {
"deviceId": device_id,
"from": from_time,
"to": to_time
}
response = requests.get(url, headers=headers, params=params)
if response.status_code == 200:
try:
positions = response.json()
print(f"Retrieved {len(positions)} positions.")
return positions
except Exception as e:
print(f"Error parsing JSON: {e}")
print(response.text)
return []
else:
print(f"Failed to fetch positions: {response.status_code} - {response.text}")
return []
# Example usage:
if __name__ == "__main__":
# Use your actual Traccar API endpoint (not /reports/route)
server_url = "https://gps.moto-adv.com/api"
token = "SDBGAiEA4sNXvVhL8w_Jd-5oCiXAuS5La5yelCQemfNysZYItaMCIQDOkzaoWKHbNnbZJw9ruGlsvbp35d90x3EGOZLXW_Gls3sidSI6MSwiZSI6IjIwMjUtMDYtMDlUMjE6MDA6MDAuMDAwKzAwOjAwIn0"
device_id = 1 # Replace with your device ID
from_time = "2024-06-02T21:00:00Z"
to_time = "2025-06-03T20:59:00Z"
positions = get_device_route(server_url, token, device_id, from_time, to_time)
for pos in positions:
print(f"{pos['deviceTime']}: {pos['latitude']}, {pos['longitude']}")

View File

@@ -1 +0,0 @@
SDBGAiEA4sNXvVhL8w_Jd-5oCiXAuS5La5yelCQemfNysZYItaMCIQDOkzaoWKHbNnbZJw9ruGlsvbp35d90x3EGOZLXW_Gls3sidSI6MSwiZSI6IjIwMjUtMDYtMDlUMjE6MDA6MDAuMDAwKzAwOjAwIn0

View File

@@ -1,53 +1,40 @@
import kivy
from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen
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, ListProperty, AliasProperty
from utils import (
generate_key, load_key, encrypt_data, decrypt_data,
check_server_settings, save_server_settings,
test_connection, get_devices_from_server, save_route_to_file, fetch_positions_for_selected_day
from kivy.properties import StringProperty, NumericProperty, AliasProperty
from py_scripts.utils import (
process_preview_util, optimize_route_entries_util
)
from datetime import date
from py_scripts.advanced_3d_generator import NavigationAnimationGenerator
# BlenderGPSAnimator imported conditionally when needed
from kivy.uix.popup import Popup
from kivy.uix.gridlayout import GridLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.boxlayout import BoxLayout
from threading import Thread
from kivy.clock import mainthread
from kivy.uix.image import Image
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.progressbar import ProgressBar
from config import RESOURCES_FOLDER, CREDENTIALS_FILE
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from PIL import Image
import time
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from PIL import Image
import time
import os
from utils import html_to_image
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):
# Add a dummy query string to force reload
return f"{img_path}?{int(time.time())}"
return img_path
return "resources/images/track.png"
preview_image_source = AliasProperty(get_preview_image_source, None, bind=['project_name'])
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
@@ -115,7 +102,7 @@ class CreateAnimationScreen(Screen):
return False
def optimize_route_entries(self):
# Show popup with progress bar
# 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)
@@ -130,71 +117,17 @@ class CreateAnimationScreen(Screen):
)
popup.open()
def process_entries(dt):
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):
label.text = "positions.json not found!"
progress.value = 100
return
with open(positions_path, "r") as f:
positions = json.load(f)
# Detect duplicate positions at the start
start_remove = 0
if positions:
first = positions[0]
for pos in positions:
if pos['latitude'] == first['latitude'] and pos['longitude'] == first['longitude']:
start_remove += 1
else:
break
if start_remove > 0:
start_remove -= 1
# Detect duplicate positions at the end
end_remove = 0
if positions:
last = positions[-1]
for pos in reversed(positions):
if pos['latitude'] == last['latitude'] and pos['longitude'] == last['longitude']:
end_remove += 1
else:
break
if end_remove > 0:
end_remove -= 1
progress.value = 100
label.text = (
f"Entries removable at start: {start_remove}\n"
f"Entries removable at end: {end_remove}"
# 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()
)
btn_save = Button(text="Save optimized file", background_color=(0.008, 0.525, 0.290, 1))
btn_cancel = Button(text="Cancel")
btn_box = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=44)
btn_box.add_widget(btn_save)
btn_box.add_widget(btn_cancel)
layout.add_widget(btn_box)
def save_optimized(instance):
new_positions = positions[start_remove:len(positions)-end_remove if end_remove > 0 else None]
with open(positions_path, "w") as f:
json.dump(new_positions, f, indent=2)
label.text = "File optimized and saved!"
btn_save.disabled = True
btn_cancel.disabled = True
def close_and_refresh(dt):
popup.dismiss()
self.on_pre_enter() # Refresh the screen
Clock.schedule_once(close_and_refresh, 1)
btn_save.bind(on_press=save_optimized)
btn_cancel.bind(on_press=lambda x: popup.dismiss())
Clock.schedule_once(process_entries, 0.5)
def preview_route(self):
# Show processing popup
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
@@ -211,54 +144,798 @@ class CreateAnimationScreen(Screen):
)
popup.open()
def process_preview(dt):
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 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:
import folium
# 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")
html_path = os.path.join(project_folder, "preview.html")
img_path = os.path.join(project_folder, "preview.png")
if not os.path.exists(positions_path):
label.text = "positions.json not found!"
progress.value = 100
update_status(0, "Error: No GPS data found")
Clock.schedule_once(lambda dt: popup.dismiss(), 2)
return
with open(positions_path, "r") as f:
positions = json.load(f)
update_status(10, "Loading GPS data...")
if not positions:
label.text = "No positions to preview."
progress.value = 100
return
# Check dependencies first
generator = NavigationAnimationGenerator(project_folder)
generator.check_dependencies()
coords = [(pos['latitude'], pos['longitude']) for pos in positions]
m = folium.Map(location=coords[0], zoom_start=14)
folium.PolyLine(coords, color="blue", weight=4.5, opacity=1).add_to(m)
folium.Marker(coords[0], tooltip="Start", icon=folium.Icon(color="green")).add_to(m)
folium.Marker(coords[-1], tooltip="End", icon=folium.Icon(color="red")).add_to(m)
m.save(html_path)
update_status(20, "Processing GPS coordinates...")
df = generator.load_gps_data(positions_path)
# Convert HTML to image
html_to_image(html_path, img_path)
self.property('preview_image_source').dispatch(self)
# Set the image path for Kivy Image widget
self.preview_image_path = img_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")
label.text = "Preview ready!"
progress.value = 100
# Progress callback for the generator
def generator_progress(progress, message):
update_status(40 + (progress * 0.5), message) # Map 0-100% to 40-90%
def close_popup(dt):
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()
Clock.schedule_once(close_popup, 1)
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:
label.text = f"Error: {e}"
progress.value = 100
def close_popup(dt):
error_message = str(e)
def show_error(dt):
popup.dismiss()
Clock.schedule_once(close_popup, 2)
self.show_error_popup("Google Earth Animation Error", error_message)
Clock.schedule_once(process_preview, 0.5)
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()

View File

@@ -0,0 +1,633 @@
import kivy
from kivy.uix.screenmanager import Screen
import os
import json
import math
from datetime import datetime
from kivy.clock import Clock
from kivy.properties import StringProperty, NumericProperty, AliasProperty
from py_scripts.utils import (
process_preview_util, optimize_route_entries_util
)
from py_scripts.advanced_3d_generator import NavigationAnimationGenerator
from py_scripts.blender_animator import BlenderGPSAnimator
from kivy.uix.popup import Popup
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.progressbar import ProgressBar
from kivy.uix.textinput import TextInput
from config import RESOURCES_FOLDER
class CreateAnimationScreen(Screen):
project_name = StringProperty("")
preview_html_path = StringProperty("") # Path to the HTML file for preview
preview_image_path = StringProperty("") # Add this line
preview_image_version = NumericProperty(0) # Add this line
def get_preview_image_source(self):
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
img_path = os.path.join(project_folder, "preview.png")
if os.path.exists(img_path):
return img_path
return "resources/images/track.png"
preview_image_source = AliasProperty(
get_preview_image_source, None, bind=['project_name', 'preview_image_version']
)
def on_pre_enter(self):
# Update the route entries label with the actual number of entries
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
positions_path = os.path.join(project_folder, "positions.json")
count = 0
if os.path.exists(positions_path):
with open(positions_path, "r") as f:
try:
positions = json.load(f)
count = len(positions)
except Exception:
count = 0
self.ids.route_entries_label.text = f"Your route has {count} entries,"
def open_rename_popup(self):
from kivy.uix.popup import Popup
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.label import Label
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Enter new project name:")
input_field = TextInput(text=self.project_name, multiline=False)
btn_save = Button(text="Save", background_color=(0.008, 0.525, 0.290, 1))
btn_cancel = Button(text="Cancel")
layout.add_widget(label)
layout.add_widget(input_field)
layout.add_widget(btn_save)
layout.add_widget(btn_cancel)
popup = Popup(
title="Rename Project",
content=layout,
size_hint=(0.92, None),
size=(0, 260),
auto_dismiss=False
)
def do_rename(instance):
new_name = input_field.text.strip()
if new_name and new_name != self.project_name:
if self.rename_project_folder(self.project_name, new_name):
self.project_name = new_name
popup.dismiss()
self.on_pre_enter() # Refresh label
else:
label.text = "Rename failed (name exists?)"
else:
label.text = "Please enter a new name."
btn_save.bind(on_press=do_rename)
btn_cancel.bind(on_press=lambda x: popup.dismiss())
popup.open()
def rename_project_folder(self, old_name, new_name):
import os
old_path = os.path.join(RESOURCES_FOLDER, "projects", old_name)
new_path = os.path.join(RESOURCES_FOLDER, "projects", new_name)
if os.path.exists(old_path) and not os.path.exists(new_path):
os.rename(old_path, new_path)
return True
return False
def optimize_route_entries(self):
# Create the popup and UI elements
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Processing route entries...")
progress = ProgressBar(max=100, value=0)
layout.add_widget(label)
layout.add_widget(progress)
popup = Popup(
title="Optimizing Route",
content=layout,
size_hint=(0.92, None),
size=(0, 260),
auto_dismiss=False
)
popup.open()
# Now call the utility function with these objects
optimize_route_entries_util(
self.project_name,
RESOURCES_FOLDER,
label,
progress,
popup,
Clock,
on_save=lambda: self.on_pre_enter()
)
def preview_route(self):
# Show processing popup
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Processing route preview...")
progress = ProgressBar(max=100, value=0)
layout.add_widget(label)
layout.add_widget(progress)
popup = Popup(
title="Previewing Route",
content=layout,
size_hint=(0.8, None),
size=(0, 180),
auto_dismiss=False
)
popup.open()
def set_preview_image_path(path):
self.preview_image_path = path
self.preview_image_version += 1 # Force AliasProperty to update
self.property('preview_image_source').dispatch(self)
self.ids.preview_image.reload()
# Schedule the processing function
Clock.schedule_once(
lambda dt: process_preview_util(
self.project_name,
RESOURCES_FOLDER,
label,
progress,
popup,
self.ids.preview_image,
set_preview_image_path,
Clock
),
0.5
)
def open_pauses_popup(self):
"""Navigate to the pause edit screen"""
pause_edit_screen = self.manager.get_screen("pause_edit")
pause_edit_screen.set_project_and_callback(self.project_name, self.on_pre_enter)
self.manager.current = "pause_edit"
def show_video_generation_options(self):
"""Show popup with video generation mode options including new advanced animations"""
from kivy.uix.popup import Popup
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
layout = BoxLayout(orientation='vertical', spacing=12, padding=15)
# Title
title_label = Label(
text="Choose Animation Style & Quality",
font_size=20,
size_hint_y=None,
height=40,
color=(1, 1, 1, 1)
)
layout.add_widget(title_label)
# Classic 3D Mode
classic_layout = BoxLayout(orientation='vertical', spacing=5)
classic_title = Label(
text="🏃‍♂️ Classic 3D (Original Pipeline)",
font_size=16,
size_hint_y=None,
height=30,
color=(0.2, 0.8, 0.2, 1)
)
classic_desc = Label(
text="• Traditional OpenCV/PIL approach\n• Fast generation\n• Good for simple tracks\n• Test (720p) or Production (2K)",
font_size=11,
size_hint_y=None,
height=70,
color=(0.9, 0.9, 0.9, 1),
halign="left",
valign="middle"
)
classic_desc.text_size = (None, None)
classic_layout.add_widget(classic_title)
classic_layout.add_widget(classic_desc)
layout.add_widget(classic_layout)
# Classic buttons
classic_btn_layout = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=45)
classic_test_btn = Button(
text="Classic 720p",
background_color=(0.2, 0.8, 0.2, 1),
font_size=12
)
classic_prod_btn = Button(
text="Classic 2K",
background_color=(0.3, 0.6, 0.3, 1),
font_size=12
)
classic_btn_layout.add_widget(classic_test_btn)
classic_btn_layout.add_widget(classic_prod_btn)
layout.add_widget(classic_btn_layout)
# Advanced Navigation Mode
advanced_layout = BoxLayout(orientation='vertical', spacing=5)
advanced_title = Label(
text="🧭 Navigation Animation",
font_size=16,
size_hint_y=None,
height=30,
color=(0.2, 0.6, 0.9, 1)
)
advanced_desc = Label(
text="• Satellite terrain details\n• 3D camera following at 1000-2000m\n• Google Earth entry scene\n• Professional navigation view",
font_size=11,
size_hint_y=None,
height=70,
color=(0.9, 0.9, 0.9, 1),
halign="left",
valign="middle"
)
advanced_desc.text_size = (None, None)
advanced_layout.add_widget(advanced_title)
advanced_layout.add_widget(advanced_desc)
layout.add_widget(advanced_layout)
# Advanced button
advanced_btn = Button(
text="Generate Navigation Animation",
background_color=(0.2, 0.6, 0.9, 1),
size_hint_y=None,
height=45,
font_size=13
)
layout.add_widget(advanced_btn)
# Google Earth Flythrough Mode
google_earth_layout = BoxLayout(orientation='vertical', spacing=5)
google_earth_title = Label(
text="🌍 Google Earth Flythrough",
font_size=16,
size_hint_y=None,
height=30,
color=(0.1, 0.8, 0.1, 1)
)
google_earth_desc = Label(
text="• Realistic 3D terrain with mountains\n• Cinematic camera following at 1000-2000m\n• Google Earth-style flythrough\n• Professional geographic animation",
font_size=11,
size_hint_y=None,
height=70,
color=(0.9, 0.9, 0.9, 1),
halign="left",
valign="middle"
)
google_earth_desc.text_size = (None, None)
google_earth_layout.add_widget(google_earth_title)
google_earth_layout.add_widget(google_earth_desc)
layout.add_widget(google_earth_layout)
# Google Earth button
google_earth_btn = Button(
text="Generate Google Earth Flythrough",
background_color=(0.1, 0.8, 0.1, 1),
size_hint_y=None,
height=45,
font_size=13
)
layout.add_widget(google_earth_btn)
# Blender Cinema Mode
blender_layout = BoxLayout(orientation='vertical', spacing=5)
blender_title = Label(
text="<22> Cinema Quality (Blender)",
font_size=16,
size_hint_y=None,
height=30,
color=(0.9, 0.6, 0.2, 1)
)
blender_desc = Label(
text="• Professional 3D rendering\n• Photorealistic visuals\n• Cinema-grade quality\n• Longer processing time",
font_size=11,
size_hint_y=None,
height=70,
color=(0.9, 0.9, 0.9, 1),
halign="left",
valign="middle"
)
blender_desc.text_size = (None, None)
blender_layout.add_widget(blender_title)
blender_layout.add_widget(blender_desc)
layout.add_widget(blender_layout)
# Blender button
blender_btn = Button(
text="Generate Blender Cinema Animation",
background_color=(0.9, 0.6, 0.2, 1),
size_hint_y=None,
height=45,
font_size=13
)
layout.add_widget(blender_btn)
# Cancel button
cancel_btn = Button(
text="Cancel",
background_color=(0.5, 0.5, 0.5, 1),
size_hint_y=None,
height=40,
font_size=12
)
layout.add_widget(cancel_btn)
popup = Popup(
title="Select Animation Style",
content=layout,
size_hint=(0.95, 0.9),
auto_dismiss=False
)
def start_classic_test(instance):
popup.dismiss()
self.generate_3d_video_test_mode()
def start_classic_production(instance):
popup.dismiss()
self.generate_3d_video_production_mode()
def start_advanced_3d(instance):
popup.dismiss()
self.generate_advanced_3d_animation()
def start_google_earth(instance):
popup.dismiss()
self.generate_google_earth_animation()
def start_blender_animation(instance):
popup.dismiss()
self.generate_blender_animation()
classic_test_btn.bind(on_press=start_classic_test)
classic_prod_btn.bind(on_press=start_classic_production)
advanced_btn.bind(on_press=start_advanced_3d)
google_earth_btn.bind(on_press=start_google_earth)
blender_btn.bind(on_press=start_blender_animation)
cancel_btn.bind(on_press=lambda x: popup.dismiss())
popup.open()
def generate_blender_animation(self):
"""Generate cinema-quality animation using Blender"""
# Show processing popup
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Initializing Blender rendering pipeline...")
progress = ProgressBar(max=100, value=0)
layout.add_widget(label)
layout.add_widget(progress)
popup = Popup(
title="Generating Blender Cinema Animation",
content=layout,
size_hint=(0.9, None),
size=(0, 200),
auto_dismiss=False
)
popup.open()
def run_blender_animation():
try:
# Update status
def update_status(progress_val, status_text):
def _update(dt):
progress.value = progress_val
label.text = status_text
Clock.schedule_once(_update, 0)
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
positions_path = os.path.join(project_folder, "positions.json")
if not os.path.exists(positions_path):
update_status(0, "Error: No GPS data found")
Clock.schedule_once(lambda dt: popup.dismiss(), 2)
return
update_status(10, "Loading GPS data into Blender...")
# Check dependencies first
animator = BlenderGPSAnimator(project_folder)
animator.check_dependencies()
update_status(25, "Processing GPS coordinates...")
gps_data = animator.load_gps_data(positions_path)
output_video_path = os.path.join(project_folder, f"{self.project_name}_blender_cinema_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
# Progress callback for the animator
def animator_progress(progress, message):
update_status(25 + (progress * 0.6), message) # Map 0-100% to 25-85%
update_status(85, "Rendering cinema-quality video...")
success = animator.create_gps_animation(
positions_path,
output_video_path,
progress_callback=animator_progress
)
if success:
update_status(100, "Blender cinema animation complete!")
output_path = output_video_path
else:
raise Exception("Failed to render Blender animation")
def show_success(dt):
popup.dismiss()
self.show_success_popup(
"Blender Cinema Animation Complete!",
f"Your cinema-quality animation has been rendered to:\n{output_path}",
output_path
)
Clock.schedule_once(show_success, 1)
except Exception as e:
error_message = str(e)
def show_error(dt):
popup.dismiss()
self.show_error_popup("Blender Animation Error", error_message)
Clock.schedule_once(show_error, 0)
# Schedule the animation generation
Clock.schedule_once(lambda dt: run_blender_animation(), 0.5)
def generate_google_earth_animation(self):
"""Generate Google Earth-style flythrough animation with terrain"""
# Show processing popup
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Initializing Google Earth flythrough...")
progress = ProgressBar(max=100, value=0)
layout.add_widget(label)
layout.add_widget(progress)
popup = Popup(
title="Generating Google Earth Flythrough",
content=layout,
size_hint=(0.9, None),
size=(0, 200),
auto_dismiss=False
)
popup.open()
def run_google_earth_animation():
try:
# Update status
def update_status(progress_val, status_text):
def _update(dt):
progress.value = progress_val
label.text = status_text
Clock.schedule_once(_update, 0)
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
positions_path = os.path.join(project_folder, "positions.json")
if not os.path.exists(positions_path):
update_status(0, "Error: No GPS data found")
Clock.schedule_once(lambda dt: popup.dismiss(), 2)
return
update_status(10, "Checking dependencies...")
# Check dependencies first
generator = NavigationAnimationGenerator(project_folder)
generator.check_dependencies()
update_status(20, "Loading GPS data...")
df = generator.load_gps_data(positions_path)
update_status(30, "Generating navigation flythrough...")
output_video_path = os.path.join(project_folder, f"{self.project_name}_navigation_flythrough_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
# Progress callback for the generator
def generator_progress(progress, message):
update_status(30 + (progress * 0.5), message) # Map 0-100% to 30-80%
update_status(80, "Creating navigation flythrough...")
success = generator.generate_3d_animation(
positions_path,
output_video_path,
style='advanced',
progress_callback=generator_progress
)
if success:
update_status(100, "Navigation flythrough complete!")
output_path = output_video_path
else:
raise Exception("Failed to generate navigation flythrough")
def show_success(dt):
popup.dismiss()
self.show_success_popup(
"Google Earth Flythrough Complete!",
f"Your cinematic flythrough has been created:\n{output_path}",
output_path
)
Clock.schedule_once(show_success, 1)
except Exception as e:
error_message = str(e)
def show_error(dt):
popup.dismiss()
self.show_error_popup("Google Earth Animation Error", error_message)
Clock.schedule_once(show_error, 0)
# Schedule the animation generation
Clock.schedule_once(lambda dt: run_google_earth_animation(), 0.5)
def show_success_popup(self, title, message, file_path=None):
"""Show success popup with option to open file location"""
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
success_label = Label(
text=message,
text_size=(400, None),
halign="center",
valign="middle"
)
layout.add_widget(success_label)
button_layout = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=50)
if file_path:
open_btn = Button(text="Open Folder", background_color=(0.2, 0.8, 0.2, 1))
open_btn.bind(on_press=lambda x: self.open_file_location(file_path))
button_layout.add_widget(open_btn)
ok_btn = Button(text="OK", background_color=(0.2, 0.6, 0.9, 1))
button_layout.add_widget(ok_btn)
layout.add_widget(button_layout)
popup = Popup(
title=title,
content=layout,
size_hint=(0.8, None),
size=(0, 250),
auto_dismiss=False
)
ok_btn.bind(on_press=lambda x: popup.dismiss())
popup.open()
def show_error_popup(self, title, message):
"""Show error popup"""
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
error_label = Label(
text=f"Error: {message}",
text_size=(400, None),
halign="center",
valign="middle",
color=(1, 0.3, 0.3, 1)
)
layout.add_widget(error_label)
ok_btn = Button(text="OK", background_color=(0.8, 0.2, 0.2, 1), size_hint_y=None, height=50)
layout.add_widget(ok_btn)
popup = Popup(
title=title,
content=layout,
size_hint=(0.8, None),
size=(0, 200),
auto_dismiss=False
)
ok_btn.bind(on_press=lambda x: popup.dismiss())
popup.open()
def open_file_location(self, file_path):
"""Open file location in system file manager"""
import subprocess
import platform
folder_path = os.path.dirname(file_path)
try:
if platform.system() == "Linux":
subprocess.run(["xdg-open", folder_path])
elif platform.system() == "Darwin": # macOS
subprocess.run(["open", folder_path])
elif platform.system() == "Windows":
subprocess.run(["explorer", folder_path])
except Exception as e:
print(f"Could not open folder: {e}")

View File

@@ -0,0 +1,419 @@
import kivy
from kivy.uix.screenmanager import Screen
import os
import json
import math
from datetime import datetime
from kivy.clock import Clock
from kivy.properties import StringProperty, NumericProperty, AliasProperty
from py_scripts.utils import (
process_preview_util, optimize_route_entries_util
)
from py_scripts.advanced_3d_generator import NavigationAnimationGenerator
from py_scripts.blender_animator import BlenderGPSAnimator
from kivy.uix.popup import Popup
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.progressbar import ProgressBar
from kivy.uix.textinput import TextInput
from config import RESOURCES_FOLDER
class CreateAnimationScreen(Screen):
project_name = StringProperty("")
preview_html_path = StringProperty("") # Path to the HTML file for preview
preview_image_path = StringProperty("") # Add this line
preview_image_version = NumericProperty(0) # Add this line
def get_preview_image_source(self):
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
img_path = os.path.join(project_folder, "preview.png")
if os.path.exists(img_path):
return img_path
return "resources/images/track.png"
preview_image_source = AliasProperty(
get_preview_image_source, None, bind=['project_name', 'preview_image_version']
)
def on_pre_enter(self):
# Update the route entries label with the actual number of entries
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
positions_path = os.path.join(project_folder, "positions.json")
count = 0
if os.path.exists(positions_path):
with open(positions_path, "r") as f:
try:
positions = json.load(f)
count = len(positions)
except Exception:
count = 0
self.ids.route_entries_label.text = f"Your route has {count} entries,"
def open_rename_popup(self):
from kivy.uix.popup import Popup
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.label import Label
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Enter new project name:")
input_field = TextInput(text=self.project_name, multiline=False)
btn_save = Button(text="Save", background_color=(0.008, 0.525, 0.290, 1))
btn_cancel = Button(text="Cancel")
layout.add_widget(label)
layout.add_widget(input_field)
layout.add_widget(btn_save)
layout.add_widget(btn_cancel)
popup = Popup(
title="Rename Project",
content=layout,
size_hint=(0.92, None),
size=(0, 260),
auto_dismiss=False
)
def do_rename(instance):
new_name = input_field.text.strip()
if new_name and new_name != self.project_name:
if self.rename_project_folder(self.project_name, new_name):
self.project_name = new_name
popup.dismiss()
self.on_pre_enter() # Refresh label
else:
label.text = "Rename failed (name exists?)"
else:
label.text = "Please enter a new name."
btn_save.bind(on_press=do_rename)
btn_cancel.bind(on_press=lambda x: popup.dismiss())
popup.open()
def rename_project_folder(self, old_name, new_name):
import os
old_path = os.path.join(RESOURCES_FOLDER, "projects", old_name)
new_path = os.path.join(RESOURCES_FOLDER, "projects", new_name)
if os.path.exists(old_path) and not os.path.exists(new_path):
os.rename(old_path, new_path)
return True
return False
def optimize_route_entries(self):
# Create the popup and UI elements
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Processing route entries...")
progress = ProgressBar(max=100, value=0)
layout.add_widget(label)
layout.add_widget(progress)
popup = Popup(
title="Optimizing Route",
content=layout,
size_hint=(0.92, None),
size=(0, 260),
auto_dismiss=False
)
popup.open()
# Now call the utility function with these objects
optimize_route_entries_util(
self.project_name,
RESOURCES_FOLDER,
label,
progress,
popup,
Clock,
on_save=lambda: self.on_pre_enter()
)
def preview_route(self):
# Show processing popup
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Processing route preview...")
progress = ProgressBar(max=100, value=0)
layout.add_widget(label)
layout.add_widget(progress)
popup = Popup(
title="Previewing Route",
content=layout,
size_hint=(0.8, None),
size=(0, 180),
auto_dismiss=False
)
popup.open()
def set_preview_image_path(path):
self.preview_image_path = path
self.preview_image_version += 1 # Force AliasProperty to update
self.property('preview_image_source').dispatch(self)
self.ids.preview_image.reload()
# Schedule the processing function
Clock.schedule_once(
lambda dt: process_preview_util(
self.project_name,
RESOURCES_FOLDER,
label,
progress,
popup,
self.ids.preview_image,
set_preview_image_path,
Clock
),
0.5
)
def open_pauses_popup(self):
"""Navigate to the pause edit screen"""
pause_edit_screen = self.manager.get_screen("pause_edit")
pause_edit_screen.set_project_and_callback(self.project_name, self.on_pre_enter)
self.manager.current = "pause_edit"
def generate_google_earth_animation(self):
"""Generate Google Earth-style flythrough animation using NavigationAnimationGenerator"""
# Show processing popup
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Initializing Google Earth flythrough...")
progress = ProgressBar(max=100, value=0)
layout.add_widget(label)
layout.add_widget(progress)
popup = Popup(
title="Generating Google Earth Flythrough",
content=layout,
size_hint=(0.9, None),
size=(0, 200),
auto_dismiss=False
)
popup.open()
def run_google_earth_animation():
try:
# Update status
def update_status(progress_val, status_text):
def _update(dt):
progress.value = progress_val
label.text = status_text
Clock.schedule_once(_update, 0)
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
positions_path = os.path.join(project_folder, "positions.json")
if not os.path.exists(positions_path):
update_status(0, "Error: No GPS data found")
Clock.schedule_once(lambda dt: popup.dismiss(), 2)
return
update_status(10, "Loading GPS data...")
# Check dependencies first
generator = NavigationAnimationGenerator(project_folder)
generator.check_dependencies()
update_status(20, "Processing GPS coordinates...")
df = generator.load_gps_data(positions_path)
update_status(40, "Creating Google Earth flythrough...")
output_video_path = os.path.join(project_folder, f"{self.project_name}_google_earth_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
# Progress callback for the generator
def generator_progress(progress, message):
update_status(40 + (progress * 0.5), message) # Map 0-100% to 40-90%
update_status(90, "Creating flythrough video...")
success = generator.generate_frames(positions_path, style='google_earth', progress_callback=generator_progress)
if success and len(success) > 0:
update_status(95, "Rendering final video...")
video_success = generator.create_video(success, output_video_path, generator_progress)
if video_success:
update_status(100, "Google Earth flythrough complete!")
output_path = output_video_path
else:
raise Exception("Failed to create video from frames")
else:
raise Exception("Failed to generate frames")
def show_success(dt):
popup.dismiss()
self.show_success_popup(
"Google Earth Flythrough Complete!",
f"Your Google Earth-style flythrough has been saved to:\n{output_path}",
output_path
)
Clock.schedule_once(show_success, 1)
except Exception as e:
error_message = str(e)
def show_error(dt):
popup.dismiss()
self.show_error_popup("Google Earth Animation Error", error_message)
Clock.schedule_once(show_error, 0)
# Schedule the animation generation
Clock.schedule_once(lambda dt: run_google_earth_animation(), 0.5)
def generate_blender_animation(self):
"""Generate cinema-quality animation using Blender"""
# Show processing popup
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Initializing Blender rendering pipeline...")
progress = ProgressBar(max=100, value=0)
layout.add_widget(label)
layout.add_widget(progress)
popup = Popup(
title="Generating Blender Cinema Animation",
content=layout,
size_hint=(0.9, None),
size=(0, 200),
auto_dismiss=False
)
popup.open()
def run_blender_animation():
try:
# Update status
def update_status(progress_val, status_text):
def _update(dt):
progress.value = progress_val
label.text = status_text
Clock.schedule_once(_update, 0)
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
positions_path = os.path.join(project_folder, "positions.json")
if not os.path.exists(positions_path):
update_status(0, "Error: No GPS data found")
Clock.schedule_once(lambda dt: popup.dismiss(), 2)
return
update_status(10, "Loading GPS data into Blender...")
# Check dependencies first
animator = BlenderGPSAnimator(project_folder)
animator.check_dependencies()
update_status(25, "Processing GPS coordinates...")
gps_data = animator.load_gps_data(positions_path)
output_video_path = os.path.join(project_folder, f"{self.project_name}_blender_cinema_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
# Progress callback for the animator
def animator_progress(progress, message):
update_status(25 + (progress * 0.6), message) # Map 0-100% to 25-85%
update_status(85, "Rendering cinema-quality video...")
success = animator.create_gps_animation(
positions_path,
output_video_path,
progress_callback=animator_progress
)
if success:
update_status(100, "Blender cinema animation complete!")
output_path = output_video_path
else:
raise Exception("Failed to generate Blender animation")
def show_success(dt):
popup.dismiss()
self.show_success_popup(
"Blender Cinema Animation Complete!",
f"Your cinema-quality animation has been saved to:\n{output_path}",
output_path
)
Clock.schedule_once(show_success, 1)
except Exception as e:
error_message = str(e)
def show_error(dt):
popup.dismiss()
self.show_error_popup("Blender Animation Error", error_message)
Clock.schedule_once(show_error, 0)
# Schedule the animation generation
Clock.schedule_once(lambda dt: run_blender_animation(), 0.5)
def show_success_popup(self, title, message, file_path):
"""Show success popup with option to open file location"""
layout = BoxLayout(orientation='vertical', spacing=10, padding=15)
# Success message
success_label = Label(
text=message,
text_size=(None, None),
halign="center",
valign="middle"
)
layout.add_widget(success_label)
# Buttons
btn_layout = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=50)
open_folder_btn = Button(
text="Open Folder",
background_color=(0.2, 0.6, 0.9, 1)
)
ok_btn = Button(
text="OK",
background_color=(0.3, 0.7, 0.3, 1)
)
btn_layout.add_widget(open_folder_btn)
btn_layout.add_widget(ok_btn)
layout.add_widget(btn_layout)
popup = Popup(
title=title,
content=layout,
size_hint=(0.9, 0.6),
auto_dismiss=False
)
def open_folder(instance):
folder_path = os.path.dirname(file_path)
os.system(f'xdg-open "{folder_path}"') # Linux
popup.dismiss()
def close_popup(instance):
popup.dismiss()
open_folder_btn.bind(on_press=open_folder)
ok_btn.bind(on_press=close_popup)
popup.open()
def show_error_popup(self, title, message):
"""Show error popup"""
layout = BoxLayout(orientation='vertical', spacing=10, padding=15)
error_label = Label(
text=f"Error: {message}",
text_size=(None, None),
halign="center",
valign="middle"
)
layout.add_widget(error_label)
ok_btn = Button(
text="OK",
background_color=(0.8, 0.3, 0.3, 1),
size_hint_y=None,
height=50
)
layout.add_widget(ok_btn)
popup = Popup(
title=title,
content=layout,
size_hint=(0.8, 0.4),
auto_dismiss=False
)
ok_btn.bind(on_press=lambda x: popup.dismiss())
popup.open()

View File

@@ -5,7 +5,7 @@ import os
import json
from kivy.clock import Clock
from kivy.properties import StringProperty, ListProperty
from utils import (
from py_scripts.utils import (
generate_key, load_key, encrypt_data, decrypt_data,
check_server_settings, save_server_settings,
test_connection, get_devices_from_server, save_route_to_file, fetch_positions_for_selected_day

View File

@@ -9,7 +9,7 @@ from kivy.clock import Clock
import os
import json
from config import RESOURCES_FOLDER, CREDENTIALS_FILE
from utils import encrypt_data, decrypt_data, check_server_settings, save_server_settings, test_connection
from py_scripts.utils import encrypt_data, decrypt_data, check_server_settings, save_server_settings, test_connection
class IconButton(ButtonBehavior, Image):

View File

@@ -9,7 +9,7 @@ from kivy.clock import Clock
import os
import json
from config import RESOURCES_FOLDER, CREDENTIALS_FILE
from utils import encrypt_data, decrypt_data, check_server_settings, save_server_settings, test_connection
from py_scripts.utils import encrypt_data, decrypt_data, check_server_settings, save_server_settings, test_connection
class LoginScreen(Screen):

View File

View File

@@ -0,0 +1,889 @@
import kivy
from kivy.uix.screenmanager import Screen
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.filechooser import FileChooserIconView
from kivy.uix.widget import Widget
from kivy.uix.image import Image
from kivy.uix.carousel import Carousel
from kivy.uix.progressbar import ProgressBar
from kivy.graphics import Color, Rectangle, Line
from kivy.uix.scrollview import ScrollView
from kivy.uix.popup import Popup
from kivy.uix.gridlayout import GridLayout
from kivy.properties import StringProperty
from kivy.clock import Clock
import os
import json
import shutil
import threading
from geopy.geocoders import Nominatim
from config import RESOURCES_FOLDER
class PauseEditScreen(Screen):
project_name = StringProperty("")
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.pauses = []
self.on_save_callback = None
self.loading_popup = None
self.carousel = None
def on_pre_enter(self):
"""Called when entering the screen"""
self.show_loading_popup()
# Delay the layout building to show loading popup first
Clock.schedule_once(self.start_loading_process, 0.1)
def show_loading_popup(self):
"""Show loading popup while building the layout"""
layout = BoxLayout(orientation='vertical', spacing=20, padding=20)
# Loading animation/progress bar
progress = ProgressBar(
max=100,
size_hint_y=None,
height=20
)
# Animate the progress bar
def animate_progress(dt):
if progress.value < 95:
progress.value += 5
else:
progress.value = 10 # Reset for continuous animation
Clock.schedule_interval(animate_progress, 0.1)
loading_label = Label(
text="Loading pause information...\nPlease wait",
color=(1, 1, 1, 1),
font_size=16,
halign="center",
text_size=(300, None)
)
layout.add_widget(loading_label)
layout.add_widget(progress)
self.loading_popup = Popup(
title="Loading Pauses",
content=layout,
size_hint=(0.8, 0.3),
auto_dismiss=False
)
self.loading_popup.open()
def start_loading_process(self, dt):
"""Start the loading process in background"""
# Run the heavy loading in a separate thread
thread = threading.Thread(target=self.load_data_background)
thread.daemon = True
thread.start()
def load_data_background(self):
"""Load pause data in background thread"""
try:
# Load pauses
self.load_pauses()
# Pre-process location suggestions to speed up UI
for pause in self.pauses:
lat = pause["location"]["latitude"]
lon = pause["location"]["longitude"]
# Cache the location suggestion
if 'location_suggestion' not in pause:
pause['location_suggestion'] = self.suggest_location_name(lat, lon)
# Schedule UI update on main thread
Clock.schedule_once(self.finish_loading, 0)
except Exception as e:
print(f"Error loading pause data: {e}")
Clock.schedule_once(self.finish_loading, 0)
def finish_loading(self, dt):
"""Finish loading and build the UI"""
try:
self.build_pause_layout()
finally:
# Close loading popup
if self.loading_popup:
self.loading_popup.dismiss()
self.loading_popup = None
def suggest_location_name(self, lat, lon):
"""Simplified location suggestion"""
try:
geolocator = Nominatim(user_agent="traccar_animation")
location = geolocator.reverse((lat, lon), exactly_one=True, timeout=8, addressdetails=True)
if location and location.raw:
address = location.raw.get('address', {})
# Look for street address first
if 'road' in address:
road = address['road']
if 'house_number' in address:
return f"{road} {address['house_number']}"
return road
# Look for area name
for key in ['neighbourhood', 'suburb', 'village', 'town', 'city']:
if key in address and address[key]:
return address[key]
return f"Location {lat:.4f}, {lon:.4f}"
except Exception:
return f"Location {lat:.4f}, {lon:.4f}"
def load_pauses(self):
"""Load pauses from the project folder"""
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
pauses_path = os.path.join(project_folder, "pauses.json")
if os.path.exists(pauses_path):
with open(pauses_path, "r") as f:
self.pauses = json.load(f)
else:
self.pauses = []
def save_pauses(self):
"""Save pauses to the project folder"""
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
pauses_path = os.path.join(project_folder, "pauses.json")
with open(pauses_path, "w") as f:
json.dump(self.pauses, f, indent=2)
def build_pause_layout(self):
"""Build the main pause editing layout with carousel for multiple pauses"""
self.clear_widgets()
# Main layout with dark background
main_layout = BoxLayout(orientation='vertical', spacing=5, padding=[5, 5, 5, 5])
with main_layout.canvas.before:
Color(0.11, 0.10, 0.15, 1)
main_layout.bg_rect = Rectangle(pos=main_layout.pos, size=main_layout.size)
main_layout.bind(pos=self.update_bg_rect, size=self.update_bg_rect)
# Header with back button and pause counter
header = BoxLayout(orientation='horizontal', size_hint_y=None, height=40, spacing=10)
back_btn = Button(
text="← Back",
size_hint_x=None,
width=70,
font_size=14,
background_color=(0.341, 0.235, 0.980, 1),
color=(1, 1, 1, 1)
)
back_btn.bind(on_press=self.go_back)
# Dynamic title based on number of pauses
pause_count = len(self.pauses)
if pause_count > 2:
title_text = f"Edit Pauses ({pause_count} total)\nSwipe to navigate"
else:
title_text = "Edit Pauses"
title_label = Label(
text=title_text,
font_size=14 if pause_count > 2 else 16,
color=(1, 1, 1, 1),
halign="center",
bold=True
)
header.add_widget(back_btn)
header.add_widget(title_label)
header.add_widget(Widget(size_hint_x=None, width=70))
main_layout.add_widget(header)
# Choose layout based on number of pauses
if pause_count > 2:
# Use carousel for multiple pauses
content_area = self.create_carousel_layout()
else:
# Use simple scroll view for 1-2 pauses
content_area = self.create_simple_scroll_layout()
main_layout.add_widget(content_area)
# Save all button at bottom
save_all_btn = Button(
text="Save All Changes & Go Back",
size_hint_y=None,
height=45,
font_size=14,
background_color=(0.2, 0.7, 0.2, 1),
color=(1, 1, 1, 1),
bold=True
)
save_all_btn.bind(on_press=self.save_all_and_close)
main_layout.add_widget(save_all_btn)
self.add_widget(main_layout)
def create_carousel_layout(self):
"""Create carousel layout for multiple pauses"""
# Create carousel
self.carousel = Carousel(
direction='right',
loop=True,
size_hint=(1, 1)
)
# Add each pause as a slide
for idx, pause in enumerate(self.pauses):
# Create a slide container
slide = BoxLayout(orientation='vertical', spacing=5, padding=[5, 5, 5, 5])
# Add pause indicator
indicator = Label(
text=f"Pause {idx + 1} of {len(self.pauses)} - Swipe for more",
font_size=12,
color=(0.8, 0.8, 0.8, 1),
size_hint_y=None,
height=25,
halign="center"
)
slide.add_widget(indicator)
# Create pause frame
pause_frame = self.create_pause_frame(idx, pause)
# Wrap in scroll view for this slide
scroll = ScrollView(size_hint=(1, 1))
scroll_content = BoxLayout(orientation='vertical', size_hint_y=None, padding=[2, 2, 2, 2])
scroll_content.bind(minimum_height=scroll_content.setter('height'))
scroll_content.add_widget(pause_frame)
scroll.add_widget(scroll_content)
slide.add_widget(scroll)
self.carousel.add_widget(slide)
return self.carousel
def create_simple_scroll_layout(self):
"""Create simple scroll layout for 1-2 pauses"""
scroll_content = BoxLayout(orientation='vertical', spacing=10, size_hint_y=None, padding=[5, 5, 5, 5])
scroll_content.bind(minimum_height=scroll_content.setter('height'))
for idx, pause in enumerate(self.pauses):
pause_frame = self.create_pause_frame(idx, pause)
scroll_content.add_widget(pause_frame)
scroll = ScrollView(size_hint=(1, 1))
scroll.add_widget(scroll_content)
return scroll
def update_bg_rect(self, instance, value):
"""Update background rectangle"""
instance.bg_rect.pos = instance.pos
instance.bg_rect.size = instance.size
def create_pause_frame(self, idx, pause):
"""Create a frame for a single pause"""
# Main frame with border
frame = BoxLayout(
orientation='vertical',
spacing=8,
padding=[8, 8, 8, 8],
size_hint_y=None,
height=380 # Increased height for better photo scrolling
)
with frame.canvas.before:
Color(0.18, 0.18, 0.22, 1) # Frame background
frame.bg_rect = Rectangle(pos=frame.pos, size=frame.size)
Color(0.4, 0.6, 1.0, 1) # Frame border
frame.border_line = Line(rectangle=(frame.x, frame.y, frame.width, frame.height), width=2)
def update_frame(instance, value, frame_widget=frame):
frame_widget.bg_rect.pos = frame_widget.pos
frame_widget.bg_rect.size = frame_widget.size
frame_widget.border_line.rectangle = (frame_widget.x, frame_widget.y, frame_widget.width, frame_widget.height)
frame.bind(pos=update_frame, size=update_frame)
# 1. Pause number label (centered)
pause_number_label = Label(
text=f"[b]PAUSE {idx + 1}[/b]",
markup=True,
font_size=16,
color=(1, 1, 1, 1),
size_hint_y=None,
height=30,
halign="center",
valign="middle"
)
pause_number_label.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None)))
frame.add_widget(pause_number_label)
# 2. Location suggestion (left aligned) - use cached version if available
suggested_place = pause.get('location_suggestion') or self.suggest_location_name(
pause["location"]["latitude"], pause["location"]["longitude"]
)
location_label = Label(
text=f"Location: {suggested_place}",
font_size=12,
color=(0.8, 0.9, 1, 1),
size_hint_y=None,
height=25,
halign="left",
valign="middle"
)
location_label.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None)))
frame.add_widget(location_label)
# 3. Custom name entry and save button
name_layout = BoxLayout(orientation='horizontal', spacing=5, size_hint_y=None, height=35)
name_input = TextInput(
text=pause.get('name', ''),
hint_text="Enter custom location name...",
multiline=False,
background_color=(0.25, 0.25, 0.3, 1),
foreground_color=(1, 1, 1, 1),
font_size=12,
padding=[8, 8, 8, 8]
)
save_name_btn = Button(
text="Save Name",
size_hint_x=None,
width=80,
font_size=11,
background_color=(0.341, 0.235, 0.980, 1),
color=(1, 1, 1, 1)
)
save_name_btn.bind(on_press=lambda x: self.save_pause_name(pause, name_input))
name_layout.add_widget(name_input)
name_layout.add_widget(save_name_btn)
frame.add_widget(name_layout)
# 4. Photos area - vertical scrolling
photos_area = self.create_photos_area_vertical(idx, pause)
frame.add_widget(photos_area)
# 5. Save and Delete buttons row
button_layout = BoxLayout(orientation='horizontal', spacing=5, size_hint_y=None, height=30)
save_pause_btn = Button(
text="Save Pause Info",
font_size=12,
background_color=(0.2, 0.6, 0.8, 1),
color=(1, 1, 1, 1)
)
save_pause_btn.bind(on_press=lambda x: self.save_individual_pause(idx))
delete_pause_btn = Button(
text="Delete Pause",
font_size=12,
background_color=(0.8, 0.2, 0.2, 1),
color=(1, 1, 1, 1)
)
delete_pause_btn.bind(on_press=lambda x: self.delete_pause(idx))
button_layout.add_widget(save_pause_btn)
button_layout.add_widget(delete_pause_btn)
frame.add_widget(button_layout)
return frame
def create_photos_area_vertical(self, pause_idx, pause):
"""Create photos area with vertical scrolling"""
photos_layout = BoxLayout(orientation='vertical', spacing=5, size_hint_y=None, height=200)
# Photos header with add button
photos_header = BoxLayout(orientation='horizontal', size_hint_y=None, height=30)
photos_title = Label(
text="Photos:",
font_size=12,
color=(1, 1, 1, 1),
size_hint_x=0.5,
halign="left",
valign="middle"
)
photos_title.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None)))
add_photos_btn = Button(
text="Add Photos",
size_hint_x=0.5,
font_size=11,
background_color=(0.2, 0.7, 0.2, 1),
color=(1, 1, 1, 1)
)
add_photos_btn.bind(on_press=lambda x: self.add_photos(pause_idx))
photos_header.add_widget(photos_title)
photos_header.add_widget(add_photos_btn)
photos_layout.add_widget(photos_header)
# Get photos for this pause
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
pause_img_folder = os.path.join(project_folder, f"pause_{pause_idx+1}")
os.makedirs(pause_img_folder, exist_ok=True)
img_list = [f for f in os.listdir(pause_img_folder) if os.path.isfile(os.path.join(pause_img_folder, f))]
if img_list:
# Create vertical scrolling photo gallery
photos_scroll = ScrollView(size_hint=(1, 1), do_scroll_y=True, do_scroll_x=False)
photos_content = BoxLayout(orientation='vertical', spacing=2, size_hint_y=None, padding=[2, 2, 2, 2])
photos_content.bind(minimum_height=photos_content.setter('height'))
for img_file in img_list:
photo_item = self.create_vertical_photo_item(pause_idx, img_file, photos_content)
photos_content.add_widget(photo_item)
photos_scroll.add_widget(photos_content)
photos_layout.add_widget(photos_scroll)
else:
no_photos_label = Label(
text="No photos added yet",
font_size=12,
color=(0.6, 0.6, 0.6, 1),
size_hint_y=1,
halign="center"
)
photos_layout.add_widget(no_photos_label)
return photos_layout
def create_vertical_photo_item(self, pause_idx, img_file, parent_layout):
"""Create a photo item for vertical scrolling"""
# Main container with border
photo_item = BoxLayout(orientation='horizontal', spacing=5, size_hint_y=None, height=60, padding=[2, 2, 2, 2])
# Add border and background to photo item
with photo_item.canvas.before:
Color(0.25, 0.25, 0.30, 1) # Background
photo_item.bg_rect = Rectangle(pos=photo_item.pos, size=photo_item.size)
Color(0.4, 0.4, 0.5, 1) # Border
photo_item.border_line = Line(rectangle=(photo_item.x, photo_item.y, photo_item.width, photo_item.height), width=1)
def update_photo_item(instance, value, item=photo_item):
item.bg_rect.pos = item.pos
item.bg_rect.size = item.size
item.border_line.rectangle = (item.x, item.y, item.width, item.height)
photo_item.bind(pos=update_photo_item, size=update_photo_item)
# Get full path to the image
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
pause_img_folder = os.path.join(project_folder, f"pause_{pause_idx+1}")
img_path = os.path.join(pause_img_folder, img_file)
# Image thumbnail container
image_container = BoxLayout(size_hint_x=None, width=55, padding=[2, 2, 2, 2])
try:
photo_image = Image(
source=img_path,
size_hint=(1, 1),
allow_stretch=True,
keep_ratio=True
)
except Exception:
# Fallback to a placeholder if image can't be loaded
photo_image = Widget(size_hint=(1, 1))
with photo_image.canvas:
Color(0.3, 0.3, 0.3, 1)
Rectangle(pos=photo_image.pos, size=photo_image.size)
image_container.add_widget(photo_image)
# Photo info layout (filename and details)
info_layout = BoxLayout(orientation='vertical', spacing=2, size_hint_x=0.55, padding=[5, 2, 5, 2])
# Filename label (truncate if too long)
display_name = img_file if len(img_file) <= 20 else f"{img_file[:17]}..."
filename_label = Label(
text=display_name,
font_size=9,
color=(1, 1, 1, 1),
halign="left",
valign="top",
size_hint_y=0.6,
bold=True
)
filename_label.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None)))
# File size and type info
try:
file_size = os.path.getsize(img_path)
if file_size < 1024:
size_text = f"{file_size} B"
elif file_size < 1024*1024:
size_text = f"{file_size/1024:.1f} KB"
else:
size_text = f"{file_size/(1024*1024):.1f} MB"
# Get file extension
file_ext = os.path.splitext(img_file)[1].upper().replace('.', '')
info_text = f"{file_ext}{size_text}"
except:
info_text = "Unknown format"
size_label = Label(
text=info_text,
font_size=7,
color=(0.8, 0.8, 0.8, 1),
halign="left",
valign="bottom",
size_hint_y=0.4
)
size_label.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None)))
info_layout.add_widget(filename_label)
info_layout.add_widget(size_label)
# Button layout for actions
button_layout = BoxLayout(orientation='vertical', spacing=2, size_hint_x=0.28, padding=[2, 2, 2, 2])
# View button to show full image
view_btn = Button(
text="👁 View",
font_size=8,
background_color=(0.2, 0.6, 0.8, 1),
color=(1, 1, 1, 1),
size_hint_y=0.5
)
view_btn.bind(on_press=lambda x: self.view_full_image(img_path, img_file))
# Delete button
delete_btn = Button(
text="🗑 Del",
font_size=8,
background_color=(0.8, 0.2, 0.2, 1),
color=(1, 1, 1, 1),
size_hint_y=0.5
)
delete_btn.bind(on_press=lambda x: self.delete_single_photo(pause_idx, img_file, photo_item, parent_layout))
button_layout.add_widget(view_btn)
button_layout.add_widget(delete_btn)
# Add all components to photo item
photo_item.add_widget(image_container)
photo_item.add_widget(info_layout)
photo_item.add_widget(button_layout)
return photo_item
def delete_single_photo(self, pause_idx, img_file, photo_item, parent_layout):
"""Delete a single photo with confirmation"""
def confirm_delete():
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
pause_img_folder = os.path.join(project_folder, f"pause_{pause_idx+1}")
file_path = os.path.join(pause_img_folder, img_file)
if os.path.exists(file_path):
os.remove(file_path)
parent_layout.remove_widget(photo_item)
parent_layout.height = max(20, len(parent_layout.children) * 62)
self.show_confirmation(
f"Delete Photo",
f"Are you sure you want to delete '{img_file}'?",
confirm_delete
)
def delete_pause(self, pause_idx):
"""Delete an entire pause with confirmation"""
def confirm_delete_pause():
try:
# Remove pause from the list
if 0 <= pause_idx < len(self.pauses):
self.pauses.pop(pause_idx)
# Save the updated pauses list
self.save_pauses()
# Remove pause folder and its contents
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
pause_img_folder = os.path.join(project_folder, f"pause_{pause_idx+1}")
if os.path.exists(pause_img_folder):
shutil.rmtree(pause_img_folder)
# Reorganize remaining pause folders
self.reorganize_pause_folders()
# Refresh the entire layout
self.show_message("Pause Deleted", f"Pause {pause_idx + 1} has been deleted successfully!")
Clock.schedule_once(lambda dt: self.build_pause_layout(), 0.5)
except Exception as e:
self.show_message("Error", f"Failed to delete pause: {str(e)}")
self.show_confirmation(
"Delete Pause",
f"Are you sure you want to delete Pause {pause_idx + 1}?\nThis will remove the pause location and all its photos permanently.",
confirm_delete_pause
)
def reorganize_pause_folders(self):
"""Reorganize pause folders after deletion to maintain sequential numbering"""
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
# Get all existing pause folders
existing_folders = []
for item in os.listdir(project_folder):
item_path = os.path.join(project_folder, item)
if os.path.isdir(item_path) and item.startswith("pause_"):
try:
folder_num = int(item.split("_")[1])
existing_folders.append((folder_num, item_path))
except (IndexError, ValueError):
continue
# Sort by folder number
existing_folders.sort(key=lambda x: x[0])
# Rename folders to be sequential starting from 1
temp_folders = []
for i, (old_num, old_path) in enumerate(existing_folders):
new_num = i + 1
if old_num != new_num:
# Create temporary name to avoid conflicts
temp_path = os.path.join(project_folder, f"temp_pause_{new_num}")
os.rename(old_path, temp_path)
temp_folders.append((temp_path, new_num))
else:
temp_folders.append((old_path, new_num))
# Final rename to correct names
for temp_path, new_num in temp_folders:
final_path = os.path.join(project_folder, f"pause_{new_num}")
if temp_path != final_path:
if os.path.exists(final_path):
shutil.rmtree(final_path)
os.rename(temp_path, final_path)
def save_pause_name(self, pause, name_input):
"""Save the custom name for a pause"""
pause['name'] = name_input.text
def save_individual_pause(self, pause_idx):
"""Save individual pause info"""
self.save_pauses()
# Show confirmation
self.show_message("Pause Saved", f"Pause {pause_idx + 1} information saved successfully!")
def add_photos(self, pause_idx):
"""Open file browser to add photos"""
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
pause_img_folder = os.path.join(project_folder, f"pause_{pause_idx+1}")
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
with layout.canvas.before:
Color(0.13, 0.13, 0.16, 1)
layout.bg_rect = Rectangle(pos=layout.pos, size=layout.size)
layout.bind(
pos=lambda inst, val: setattr(layout.bg_rect, 'pos', inst.pos),
size=lambda inst, val: setattr(layout.bg_rect, 'size', inst.size)
)
title_label = Label(
text=f"Select photos for Pause {pause_idx + 1}:",
color=(1, 1, 1, 1),
font_size=14,
size_hint_y=None,
height=30
)
filechooser = FileChooserIconView(
filters=['*.png', '*.jpg', '*.jpeg', '*.gif', '*.bmp'],
path=os.path.expanduser('~'),
multiselect=True
)
btn_layout = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=40)
add_btn = Button(
text="Add Selected",
background_color=(0.2, 0.7, 0.2, 1),
color=(1, 1, 1, 1),
font_size=12
)
cancel_btn = Button(
text="Cancel",
background_color=(0.6, 0.3, 0.3, 1),
color=(1, 1, 1, 1),
font_size=12
)
btn_layout.add_widget(cancel_btn)
btn_layout.add_widget(add_btn)
layout.add_widget(title_label)
layout.add_widget(filechooser)
layout.add_widget(btn_layout)
popup = Popup(
title="Add Photos",
content=layout,
size_hint=(0.95, 0.9),
auto_dismiss=False
)
def add_selected_files(instance):
if filechooser.selection:
for file_path in filechooser.selection:
if os.path.isfile(file_path):
filename = os.path.basename(file_path)
dest_path = os.path.join(pause_img_folder, filename)
if not os.path.exists(dest_path):
shutil.copy2(file_path, dest_path)
Clock.schedule_once(lambda dt: self.refresh_photos_display(), 0.1)
popup.dismiss()
else:
popup.dismiss()
add_btn.bind(on_press=add_selected_files)
cancel_btn.bind(on_press=lambda x: popup.dismiss())
popup.open()
def refresh_photos_display(self):
"""Refresh the entire display to show updated photos"""
self.build_pause_layout()
def show_message(self, title, message):
"""Show a simple message popup"""
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
msg_label = Label(
text=message,
color=(1, 1, 1, 1),
font_size=12,
halign="center"
)
msg_label.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None)))
ok_btn = Button(
text="OK",
size_hint_y=None,
height=35,
background_color=(0.341, 0.235, 0.980, 1),
color=(1, 1, 1, 1)
)
layout.add_widget(msg_label)
layout.add_widget(ok_btn)
popup = Popup(title=title, content=layout, size_hint=(0.8, 0.4))
ok_btn.bind(on_press=lambda x: popup.dismiss())
popup.open()
def show_confirmation(self, title, message, confirm_callback):
"""Show a confirmation dialog"""
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
msg_label = Label(
text=message,
color=(1, 1, 1, 1),
font_size=12,
halign="center"
)
msg_label.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None)))
btn_layout = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=35)
cancel_btn = Button(
text="Cancel",
background_color=(0.6, 0.3, 0.3, 1),
color=(1, 1, 1, 1)
)
confirm_btn = Button(
text="Confirm",
background_color=(0.8, 0.2, 0.2, 1),
color=(1, 1, 1, 1)
)
btn_layout.add_widget(cancel_btn)
btn_layout.add_widget(confirm_btn)
layout.add_widget(msg_label)
layout.add_widget(btn_layout)
popup = Popup(title=title, content=layout, size_hint=(0.8, 0.4), auto_dismiss=False)
cancel_btn.bind(on_press=lambda x: popup.dismiss())
confirm_btn.bind(on_press=lambda x: (confirm_callback(), popup.dismiss()))
popup.open()
def save_all_and_close(self, instance):
"""Save all pauses and return to previous screen"""
self.save_pauses()
if self.on_save_callback:
self.on_save_callback()
self.go_back()
def go_back(self, instance=None):
"""Return to the previous screen"""
self.manager.current = "create_animation"
def set_project_and_callback(self, project_name, callback=None):
"""Set the project name and callback for this screen"""
self.project_name = project_name
self.on_save_callback = callback
def view_full_image(self, img_path, img_file):
"""Show full image in a popup"""
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
with layout.canvas.before:
Color(0.05, 0.05, 0.08, 1)
layout.bg_rect = Rectangle(pos=layout.pos, size=layout.size)
layout.bind(
pos=lambda inst, val: setattr(layout.bg_rect, 'pos', inst.pos),
size=lambda inst, val: setattr(layout.bg_rect, 'size', inst.size)
)
# Image display
try:
full_image = Image(
source=img_path,
allow_stretch=True,
keep_ratio=True
)
except Exception:
full_image = Label(
text="Unable to load image",
color=(1, 1, 1, 1),
font_size=16
)
# Close button
close_btn = Button(
text="Close",
size_hint_y=None,
height=40,
background_color=(0.341, 0.235, 0.980, 1),
color=(1, 1, 1, 1),
font_size=14
)
layout.add_widget(full_image)
layout.add_widget(close_btn)
popup = Popup(
title=f"Photo: {img_file}",
content=layout,
size_hint=(0.95, 0.95),
auto_dismiss=False
)
close_btn.bind(on_press=lambda x: popup.dismiss())
popup.open()

View File

@@ -5,7 +5,7 @@ import os
import json
from kivy.clock import Clock
from kivy.properties import StringProperty, ListProperty
from utils import (
from py_scripts.utils import (
generate_key, load_key, encrypt_data, decrypt_data,
check_server_settings, save_server_settings,
test_connection, get_devices_from_server, save_route_to_file, fetch_positions_for_selected_day

View File

@@ -643,12 +643,45 @@
text_size: self.size
on_press: root.optimize_route_entries()
# Pauses frame
BoxLayout:
id: pauses_frame
orientation: "horizontal"
spacing: 10
padding: 10
size_hint_y: None
height: 60
canvas.before:
Color:
rgba: 0.15, 0.15, 0.18, 1
Rectangle:
pos: self.pos
size: self.size
Label:
id: pauses_label
text: "Pauses"
font_size: 16
halign: "left"
valign: "middle"
color: 1, 1, 1, 1
size_hint_x: 0.7
text_size: self.width, None
Button:
id: pauses_edit_btn
text: "Edit"
size_hint_x: 0.3
width: 120
font_size: 16
background_color: 0.341, 0.235, 0.980, 1
color: 1, 1, 1, 1
on_press: root.open_pauses_popup()
# Preview frame (label + button on first row, image on second row)
BoxLayout:
orientation: "vertical"
size_hint_y: None
height: 300 # Adjust as needed for your image size
padding: [10, 10, 10, 10]
height: 255 # Adjust as needed for your image size
padding: [5, 5, 5, 5]
spacing: 10
canvas.before:
Color:
@@ -660,8 +693,8 @@
BoxLayout:
orientation: "horizontal"
size_hint_y: None
height: 60
spacing: 10
height: 30
spacing: 5
Label:
text: "Preview your route"
@@ -683,12 +716,115 @@
Image:
id: preview_image
source: root.preview_image_source
allow_stretch: True
keep_ratio: False
allow_stretch: False
keep_ratio: True
size_hint_y: None
height: 220
height: 202
size_hint_x: 1
# Progressive 3D 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 progressive 3D animation\nBuilds trip point by point"
font_size: 16
color: 1, 1, 1, 1
size_hint_x: 0.7
halign: "left"
valign: "middle"
text_size: self.size
Button:
text: "Generate\n3D Trip"
size_hint_x: 0.3
font_size: 14
background_color: 0.2, 0.8, 0.4, 1
color: 1, 1, 1, 1
halign: "center"
valign: "middle"
text_size: self.size
on_press: root.generate_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:
size_hint_y: 1
@@ -701,3 +837,16 @@
color: 1, 1, 1, 1
font_size: 16
on_press: app.root.current = "home"
<PauseEditScreen>:
BoxLayout:
orientation: "vertical"
spacing: 8
padding: 8
canvas.before:
Color:
rgba: 0.11, 0.10, 0.15, 1
Rectangle:
pos: self.pos
size: self.size

211
utils.py
View File

@@ -1,211 +0,0 @@
import os
import json
import requests
from cryptography.fernet import Fernet
RESOURCES_FOLDER = "resources"
CREDENTIALS_FILE = os.path.join(RESOURCES_FOLDER, "credentials.enc")
KEY_FILE = os.path.join(RESOURCES_FOLDER, "key.key")
SERVER_SETTINGS_FILE = os.path.join(RESOURCES_FOLDER, "server_settings.enc")
# --- Encryption Utilities ---
def generate_key():
"""Generate and save a key for encryption."""
if not os.path.exists(KEY_FILE):
key = Fernet.generate_key()
with open(KEY_FILE, "wb") as key_file:
key_file.write(key)
def load_key():
"""Load the encryption key."""
with open(KEY_FILE, "rb") as key_file:
return key_file.read()
def encrypt_data(data):
"""Encrypt data using the encryption key."""
key = load_key()
fernet = Fernet(key)
return fernet.encrypt(data.encode())
def decrypt_data(data):
"""Decrypt data using the encryption key."""
key = load_key()
fernet = Fernet(key)
return fernet.decrypt(data).decode()
# --- Server Settings ---
def check_server_settings():
"""Load and decrypt server settings from file."""
if not os.path.exists(SERVER_SETTINGS_FILE):
return None
try:
with open(SERVER_SETTINGS_FILE, "rb") as file:
encrypted_data = file.read()
decrypted_data = decrypt_data(encrypted_data)
settings = json.loads(decrypted_data)
return settings
except Exception as e:
print(f"Failed to load server settings: {e}")
return None
def save_server_settings(settings_data):
"""Encrypt and save server settings."""
encrypted_data = encrypt_data(json.dumps(settings_data))
with open(SERVER_SETTINGS_FILE, "wb") as file:
file.write(encrypted_data)
# --- Traccar Server Connection ---
def test_connection(server_url, username=None, password=None, token=None):
"""
Test the connection with the Traccar server.
Returns: dict with 'status' (bool) and 'message' (str)
"""
if not server_url:
return {"status": False, "message": "Please provide the server URL."}
if not token and (not username or not password):
return {"status": False, "message": "Please provide either a token or username and password."}
try:
headers = {"Authorization": f"Bearer {token}"} if token else None
auth = None if token else (username, password)
response = requests.get(f"{server_url}/api/server", headers=headers, auth=auth, timeout=10)
if response.status_code == 200:
return {"status": True, "message": "Connection successful! Server is reachable."}
else:
return {"status": False, "message": f"Error: {response.status_code} - {response.reason}"}
except requests.exceptions.Timeout:
return {"status": False, "message": "Connection timed out. Please try again."}
except requests.exceptions.RequestException as e:
return {"status": False, "message": f"Connection failed: {str(e)}"}
# --- Device Fetching ---
def get_devices_from_server():
"""Retrieve a mapping of device names to IDs from the Traccar server."""
settings = check_server_settings()
if not settings:
return None
server_url = settings.get("server_url")
token = settings.get("token")
if not server_url or not token:
return None
headers = {"Authorization": f"Bearer {token}"}
try:
response = requests.get(f"{server_url}/api/devices", headers=headers)
if response.status_code == 200:
devices = response.json()
return {device.get("name", "Unnamed Device"): device.get("id", "Unknown ID") for device in devices}
else:
print(f"Error: {response.status_code} - {response.reason}")
return None
except Exception as e:
print(f"Error retrieving devices: {str(e)}")
return None
# --- Route Saving ---
def save_route_to_file(route_name, positions, base_folder="resources/projects"):
"""
Save the given positions as a route in resources/projects/<route_name>/positions.json.
Returns (success, message, file_path)
"""
if not route_name:
return False, "Please enter a route name.", None
if not positions:
return False, "No positions to save.", None
folder_path = os.path.join(base_folder, route_name)
os.makedirs(folder_path, exist_ok=True)
file_path = os.path.join(folder_path, "positions.json")
try:
with open(file_path, "w") as f:
json.dump(positions, f, indent=2)
return True, f"Route '{route_name}' saved!", file_path
except Exception as e:
return False, f"Failed to save route: {str(e)}", None
def fetch_positions(server_url, token, device_id, from_time, to_time):
"""
Fetch positions from the Traccar API.
Returns (positions, error_message)
"""
url = f"{server_url}/api/reports/route"
headers = {"Authorization": f"Bearer {token}", "Accept": "application/json"}
params = {
"deviceId": device_id,
"from": from_time,
"to": to_time
}
try:
response = requests.get(url, params=params, headers=headers, timeout=15)
if response.status_code == 200:
return response.json(), None
elif response.status_code == 400:
return None, "Bad Request: Please check the request payload and token."
else:
return None, f"Failed: {response.status_code} - {response.reason}"
except requests.exceptions.RequestException as e:
return None, f"Error fetching positions: {str(e)}"
def fetch_positions_for_selected_day(settings, device_mapping, device_name, start_date, end_date, start_hour, end_hour):
"""
Fetch positions for the selected day/device using Traccar API.
Returns (positions, error_message)
"""
if not settings:
return [], "Server settings not found."
server_url = settings.get("server_url")
token = settings.get("token")
device_id = device_mapping.get(device_name)
if not device_id:
return [], "Device ID not found."
from_time = f"{start_date}T{start_hour}:00:00Z"
to_time = f"{end_date}T{end_hour}:59:59Z"
positions, error = fetch_positions(server_url, token, device_id, from_time, to_time)
if error:
return [], error
return positions, None
def html_to_image(html_path, img_path, width=800, height=600, delay=2, driver_path='/usr/bin/chromedriver'):
"""
Convert an HTML file to an image using Selenium and Pillow.
Args:
html_path (str): Path to the HTML file.
img_path (str): Path to save the output image (PNG).
width (int): Width of the browser window.
height (int): Height of the browser window.
delay (int): Seconds to wait for the page to render.
driver_path (str): Path to chromedriver binary.
"""
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from PIL import Image
import time
import os
chrome_options = Options()
chrome_options.add_argument("--headless")
chrome_options.add_argument(f"--window-size={width},{height}")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
service = Service(driver_path)
driver = webdriver.Chrome(service=service, options=chrome_options)
try:
driver.get("file://" + os.path.abspath(html_path))
time.sleep(delay) # Wait for the page to render
tmp_img = img_path + ".tmp.png"
driver.save_screenshot(tmp_img)
driver.quit()
img = Image.open(tmp_img)
img = img.crop((0, 0, width, height))
img.save(img_path)
os.remove(tmp_img)
print(f"Image saved to: {img_path}")
except Exception as e:
print(f"Error converting HTML to image: {e}")
driver.quit()