updated app

This commit is contained in:
2025-07-07 12:20:16 +03:00
parent c28be4e083
commit a38e2b1fe9
35 changed files with 12 additions and 545606 deletions

View File

@@ -0,0 +1,157 @@
# 3D Video Animation Feature
## Overview
The 3D Video Animation feature generates Relive-style video animations from GPS route data. This creates engaging, cinematic videos that visualize your journey in a 3D perspective.
## Features
### Visual Elements
- **3D Isometric View**: Perspective projection that simulates 3D depth
- **Sky Gradient Background**: Blue gradient background that mimics sky
- **Animated Route Trail**: Color-coded path from blue (start) to red (end)
- **Pulsing Position Marker**: Animated current position indicator
- **Grid Overlay**: 3D grid effect for depth perception
- **Real-time Data Display**: Speed, timestamp, and progress information
### Technical Specifications
- **Resolution**: 1920x1080 (Full HD)
- **Frame Rate**: 30 FPS
- **Format**: MP4 video
- **Compression**: MP4V codec for broad compatibility
### Animation Effects
- **Shadow Effects**: Route lines and markers have 3D shadows
- **Elevation Simulation**: Simulated terrain elevation using sine waves
- **Smooth Transitions**: Interpolated movement between GPS points
- **Progress Indicators**: Visual progress through the route
## 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
## Processing Steps
### 1. Data Loading (10%)
- Loads GPS positions from `positions.json`
- Validates minimum route length (10+ points)
- Calculates route boundaries and center point
### 2. Route Analysis (20%)
- Determines optimal viewport and scaling
- Calculates center coordinates for camera position
- Sets up coordinate transformation matrices
### 3. Frame Generation (30-70%)
- Creates individual frames for each GPS point
- Applies 3D perspective transformation
- Renders route trail with color progression
- Adds animated markers and text overlays
### 4. Video Compilation (75-90%)
- Combines frames into MP4 video
- Applies compression and optimization
- Adds metadata and timing information
### 5. Finalization (90-100%)
- Saves video to project folder
- Cleans up temporary files
- Shows completion notification
## 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
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

Binary file not shown.

Binary file not shown.

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

@@ -0,0 +1,296 @@
"""
3D Video Animation Generator
Creates Relive-style 3D video animations from GPS route data
"""
import json
import os
import math
import requests
import cv2
import numpy as np
from PIL import Image, ImageDraw, ImageFont
import tempfile
import shutil
from datetime import datetime
def generate_3d_video_animation(project_name, resources_folder, label_widget, progress_widget, popup_widget, clock_module):
"""
Generate a 3D video animation similar to Relive
Args:
project_name: Name of the project
resources_folder: Path to resources folder
label_widget: Kivy label for status updates
progress_widget: Kivy progress bar
popup_widget: Kivy popup to dismiss when done
clock_module: Kivy Clock module for scheduling
"""
def update_progress(progress_val, status_text):
"""Update UI from background thread"""
def _update(dt):
progress_widget.value = progress_val
label_widget.text = status_text
clock_module.schedule_once(_update, 0)
def finish_generation(success, message, output_path=None):
"""Finish the generation process"""
def _finish(dt):
if popup_widget:
popup_widget.dismiss()
# Show result popup
from kivy.uix.popup import Popup
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
result_layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
if success:
result_label = Label(
text=f"3D Video Generated Successfully!\n\nSaved to:\n{output_path}",
color=(0, 1, 0, 1),
halign="center"
)
open_btn = Button(
text="Open Video Folder",
size_hint_y=None,
height=40,
background_color=(0.2, 0.7, 0.2, 1)
)
open_btn.bind(on_press=lambda x: (os.system(f"xdg-open '{os.path.dirname(output_path)}'"), result_popup.dismiss()))
result_layout.add_widget(result_label)
result_layout.add_widget(open_btn)
else:
result_label = Label(
text=f"Generation Failed:\n{message}",
color=(1, 0, 0, 1),
halign="center"
)
result_layout.add_widget(result_label)
close_btn = Button(
text="Close",
size_hint_y=None,
height=40,
background_color=(0.3, 0.3, 0.3, 1)
)
result_layout.add_widget(close_btn)
result_popup = Popup(
title="3D Video Generation Result",
content=result_layout,
size_hint=(0.9, 0.6),
auto_dismiss=False
)
close_btn.bind(on_press=lambda x: result_popup.dismiss())
result_popup.open()
clock_module.schedule_once(_finish, 0)
def run_generation():
"""Main generation function"""
try:
# Step 1: Load route data
update_progress(10, "Loading route data...")
project_folder = os.path.join(resources_folder, "projects", project_name)
positions_path = os.path.join(project_folder, "positions.json")
if not os.path.exists(positions_path):
finish_generation(False, "No route data found!")
return
with open(positions_path, "r") as f:
positions = json.load(f)
if len(positions) < 10:
finish_generation(False, "Route too short for 3D animation (minimum 10 points)")
return
# Step 2: Calculate route bounds and center
update_progress(20, "Calculating route boundaries...")
lats = [pos['latitude'] for pos in positions]
lons = [pos['longitude'] for pos in positions]
center_lat = sum(lats) / len(lats)
center_lon = sum(lons) / len(lons)
min_lat, max_lat = min(lats), max(lats)
min_lon, max_lon = min(lons), max(lons)
# Step 3: Generate frames
update_progress(30, "Generating 3D frames...")
# Create temporary directory for frames
temp_dir = tempfile.mkdtemp()
frames_dir = os.path.join(temp_dir, "frames")
os.makedirs(frames_dir)
# Video settings
width, height = 1920, 1080
fps = 30
total_frames = len(positions) * 2 # 2 frames per position for smooth animation
# Generate frames
for i, pos in enumerate(positions):
progress = 30 + (i / len(positions)) * 40
update_progress(progress, f"Generating frame {i+1}/{len(positions)}...")
frame = create_3d_frame(
pos, positions, i, center_lat, center_lon,
min_lat, max_lat, min_lon, max_lon,
width, height
)
# Save frame
frame_path = os.path.join(frames_dir, f"frame_{i:06d}.png")
cv2.imwrite(frame_path, frame)
# Step 4: Create video
update_progress(75, "Compiling video...")
# Output path
output_filename = f"{project_name}_3d_animation_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4"
output_path = os.path.join(project_folder, output_filename)
# Create video writer
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
video_writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
# Add frames to video
frame_files = sorted([f for f in os.listdir(frames_dir) if f.endswith('.png')])
for frame_file in frame_files:
frame_path = os.path.join(frames_dir, frame_file)
frame = cv2.imread(frame_path)
video_writer.write(frame)
video_writer.release()
# Step 5: Add audio (optional)
update_progress(90, "Adding finishing touches...")
# Clean up
shutil.rmtree(temp_dir)
update_progress(100, "3D Video generated successfully!")
finish_generation(True, "Success!", output_path)
except Exception as e:
finish_generation(False, str(e))
# Start generation in background
import threading
thread = threading.Thread(target=run_generation)
thread.daemon = True
thread.start()
def create_3d_frame(current_pos, all_positions, frame_index, center_lat, center_lon,
min_lat, max_lat, min_lon, max_lon, width, height):
"""
Create a single 3D-style frame
"""
# Create canvas
frame = np.zeros((height, width, 3), dtype=np.uint8)
# Background gradient (sky effect)
for y in range(height):
color_intensity = int(255 * (1 - y / height))
sky_color = (min(255, color_intensity + 50), min(255, color_intensity + 100), 255)
frame[y, :] = sky_color
# Calculate perspective transformation
# Simple isometric-style projection
scale_x = width * 0.6 / (max_lon - min_lon) if max_lon != min_lon else 1000
scale_y = height * 0.6 / (max_lat - min_lat) if max_lat != min_lat else 1000
# Draw route path with 3D effect
route_points = []
for i, pos in enumerate(all_positions[:frame_index + 1]):
# Convert GPS to screen coordinates
x = int((pos['longitude'] - min_lon) * scale_x + width * 0.2)
y = int(height * 0.8 - (pos['latitude'] - min_lat) * scale_y)
# Add 3D effect (elevation simulation)
elevation_offset = int(20 * math.sin(i * 0.1)) # Simulated elevation
y -= elevation_offset
route_points.append((x, y))
# Draw route trail with gradient
if len(route_points) > 1:
for i in range(1, len(route_points)):
# Color gradient from blue to red
progress = i / len(route_points)
color_r = int(255 * progress)
color_b = int(255 * (1 - progress))
color = (color_b, 100, color_r)
# Draw thick line with 3D shadow effect
pt1, pt2 = route_points[i-1], route_points[i]
# Shadow
cv2.line(frame, (pt1[0]+2, pt1[1]+2), (pt2[0]+2, pt2[1]+2), (50, 50, 50), 8)
# Main line
cv2.line(frame, pt1, pt2, color, 6)
# Draw current position marker
if route_points:
current_point = route_points[-1]
# Pulsing effect
pulse_size = int(15 + 10 * math.sin(frame_index * 0.3))
# Shadow
cv2.circle(frame, (current_point[0]+3, current_point[1]+3), pulse_size, (0, 0, 0), -1)
# Main marker
cv2.circle(frame, current_point, pulse_size, (0, 255, 255), -1)
cv2.circle(frame, current_point, pulse_size-3, (255, 255, 255), 2)
# Add grid effect for 3D feel
grid_spacing = 50
for x in range(0, width, grid_spacing):
cv2.line(frame, (x, 0), (x, height), (100, 100, 100), 1)
for y in range(0, height, grid_spacing):
cv2.line(frame, (0, y), (width, y), (100, 100, 100), 1)
# Add text overlay
try:
# Position info
speed = current_pos.get('speed', 0) if current_pos else 0
timestamp = current_pos.get('deviceTime', '') if current_pos else ''
text_y = 50
cv2.putText(frame, f"Speed: {speed:.1f} km/h", (50, text_y),
cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
text_y += 40
if timestamp:
cv2.putText(frame, f"Time: {timestamp[:16]}", (50, text_y),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2)
text_y += 40
cv2.putText(frame, f"Point: {frame_index + 1}/{len(all_positions)}", (50, text_y),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2)
except Exception:
pass # Skip text if font issues
return frame
def get_elevation_data(lat, lon):
"""
Get elevation data for a coordinate (optional enhancement)
"""
try:
# Using a free elevation API
url = f"https://api.open-elevation.com/api/v1/lookup?locations={lat},{lon}"
response = requests.get(url, timeout=5)
if response.status_code == 200:
data = response.json()
return data['results'][0]['elevation']
except Exception:
pass
return 0 # Default elevation

15
py_scripts/webview.py Normal file
View File

@@ -0,0 +1,15 @@
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
chrome_options = Options()
chrome_options.add_argument("--headless")
chrome_options.add_argument("--window-size=800,600")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
service = Service('/usr/bin/chromedriver')
driver = webdriver.Chrome(service=service, options=chrome_options)
driver.get("https://www.google.com")
driver.save_screenshot("/home/pi/Desktop/test.png")
driver.quit()