updated to video creation
This commit is contained in:
157
3D_VIDEO_DOCUMENTATION.md
Normal file
157
3D_VIDEO_DOCUMENTATION.md
Normal 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.
|
||||
105
PAUSE_EDIT_IMPROVEMENTS.md
Normal file
105
PAUSE_EDIT_IMPROVEMENTS.md
Normal 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
|
||||
BIN
__pycache__/main.cpython-311.pyc
Normal file
BIN
__pycache__/main.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/video_3d_generator.cpython-311.pyc
Normal file
BIN
__pycache__/video_3d_generator.cpython-311.pyc
Normal file
Binary file not shown.
2
main.py
2
main.py
@@ -32,7 +32,7 @@ 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 screens.pause_edit_screen import PauseEditScreen
|
||||
from screens.pause_edit_screen_improved import PauseEditScreen
|
||||
from config import RESOURCES_FOLDER
|
||||
kivy.require("2.0.0")
|
||||
from kivy.core.window import Window
|
||||
|
||||
@@ -4,4 +4,10 @@ kiwy-garden
|
||||
folium
|
||||
selenium
|
||||
pillow
|
||||
geopy
|
||||
geopy
|
||||
opencv-python
|
||||
moviepy
|
||||
requests
|
||||
numpy
|
||||
matplotlib
|
||||
scipy
|
||||
@@ -1 +1 @@
|
||||
gAAAAABoZPqiU5moDgLAGyehIgyLPV8d2Bdf2Fg8PZHf-d0O3MX1BopCMa_-9iEMeuiLoO9i2UFiafydQxVrGtH7db57u8CrjFKA383mrHn8O6yqvxa3GcIQmoIlCO1d0I5cWwNWQzEd
|
||||
gAAAAABoZS4Ed-JJHBU90HAwhBvMJ7HfCSU-AeSaFDEt-Hg1wJW57NBoZJw_9lXEw-uooZCbQnMEUAd9mmF_I_FR1oRC-u_6cqU01jHGQq8OzJubmvBiZ1zYVlCO-kpmmFB8jv0LaBLU
|
||||
Binary file not shown.
BIN
resources/projects/Strategica/pause_6/123.jpeg
Normal file
BIN
resources/projects/Strategica/pause_6/123.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 537 KiB |
232
resources/projects/Strategica/pauses.json
Normal file
232
resources/projects/Strategica/pauses.json
Normal file
@@ -0,0 +1,232 @@
|
||||
[
|
||||
{
|
||||
"start_time": "2025-06-15T06:00:23.000+00:00",
|
||||
"end_time": "2025-06-15T06:15:19.000+00:00",
|
||||
"duration_seconds": 897,
|
||||
"location": {
|
||||
"latitude": 45.78012944444444,
|
||||
"longitude": 24.16865166666667
|
||||
},
|
||||
"location_suggestion": "\u0218oseaua Sibiului"
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-15T07:27:39.000+00:00",
|
||||
"end_time": "2025-06-15T07:56:41.000+00:00",
|
||||
"duration_seconds": 1744,
|
||||
"location": {
|
||||
"latitude": 45.35275055555555,
|
||||
"longitude": 24.042727222222222
|
||||
},
|
||||
"location_suggestion": "Strada Podul \u0218ipotului 1"
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-15T08:36:25.000+00:00",
|
||||
"end_time": "2025-06-15T08:48:10.000+00:00",
|
||||
"duration_seconds": 702,
|
||||
"location": {
|
||||
"latitude": 45.390987777777774,
|
||||
"longitude": 23.980355
|
||||
},
|
||||
"location_suggestion": "Strategica"
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-15T09:02:08.000+00:00",
|
||||
"end_time": "2025-06-15T09:09:53.765+00:00",
|
||||
"duration_seconds": 466,
|
||||
"location": {
|
||||
"latitude": 45.409075,
|
||||
"longitude": 23.938845
|
||||
},
|
||||
"location_suggestion": "Strategica"
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-15T10:29:06.000+00:00",
|
||||
"end_time": "2025-06-15T11:04:51.000+00:00",
|
||||
"duration_seconds": 2180,
|
||||
"location": {
|
||||
"latitude": 45.40981166666667,
|
||||
"longitude": 23.793883888888892
|
||||
},
|
||||
"location_suggestion": "Strategica"
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-15T11:21:38.000+00:00",
|
||||
"end_time": "2025-06-15T11:24:05.928+00:00",
|
||||
"duration_seconds": 147,
|
||||
"location": {
|
||||
"latitude": 45.38780444444444,
|
||||
"longitude": 23.751438888888888
|
||||
},
|
||||
"location_suggestion": "Strategica"
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-15T11:25:30.000+00:00",
|
||||
"end_time": "2025-06-15T11:28:36.000+00:00",
|
||||
"duration_seconds": 186,
|
||||
"location": {
|
||||
"latitude": 45.390328333333336,
|
||||
"longitude": 23.74829666666667
|
||||
},
|
||||
"location_suggestion": "Strategica"
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-15T11:42:42.000+00:00",
|
||||
"end_time": "2025-06-15T11:51:12.000+00:00",
|
||||
"duration_seconds": 501,
|
||||
"location": {
|
||||
"latitude": 45.39019166666667,
|
||||
"longitude": 23.70160277777778
|
||||
},
|
||||
"location_suggestion": "Strategica"
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-15T11:52:48.000+00:00",
|
||||
"end_time": "2025-06-15T12:01:47.000+00:00",
|
||||
"duration_seconds": 540,
|
||||
"location": {
|
||||
"latitude": 45.39456,
|
||||
"longitude": 23.69228777777778
|
||||
},
|
||||
"location_suggestion": "Strategica"
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-15T12:12:38.000+00:00",
|
||||
"end_time": "2025-06-15T12:19:57.845+00:00",
|
||||
"duration_seconds": 440,
|
||||
"location": {
|
||||
"latitude": 45.38043944444445,
|
||||
"longitude": 23.651888333333336
|
||||
},
|
||||
"location_suggestion": "Strategica"
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-15T12:58:38.000+00:00",
|
||||
"end_time": "2025-06-15T13:53:36.000+00:00",
|
||||
"duration_seconds": 3297,
|
||||
"location": {
|
||||
"latitude": 45.57911,
|
||||
"longitude": 23.62189888888889
|
||||
},
|
||||
"location_suggestion": "Transalpina"
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-15T14:28:22.000+00:00",
|
||||
"end_time": "2025-06-15T14:38:39.000+00:00",
|
||||
"duration_seconds": 618,
|
||||
"location": {
|
||||
"latitude": 45.78371777777778,
|
||||
"longitude": 23.613761666666665
|
||||
},
|
||||
"location_suggestion": "Strada Valea Frumoasei"
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-19T05:25:20.000+00:00",
|
||||
"end_time": "2025-06-19T14:09:05.000+00:00",
|
||||
"duration_seconds": 31426,
|
||||
"location": {
|
||||
"latitude": 45.79909222222222,
|
||||
"longitude": 24.085676666666668
|
||||
},
|
||||
"location_suggestion": "Strada Monaco"
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-20T05:18:03.000+00:00",
|
||||
"end_time": "2025-06-20T14:05:26.784+00:00",
|
||||
"duration_seconds": 31644,
|
||||
"location": {
|
||||
"latitude": 45.798976111111116,
|
||||
"longitude": 24.085469444444445
|
||||
},
|
||||
"location_suggestion": "Strada Monaco"
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-21T07:45:07.000+00:00",
|
||||
"end_time": "2025-06-21T08:04:04.000+00:00",
|
||||
"duration_seconds": 1049,
|
||||
"location": {
|
||||
"latitude": 45.82062444444445,
|
||||
"longitude": 24.151072222222222
|
||||
},
|
||||
"location_suggestion": "Calea \u0218urii Mari 36"
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-23T05:16:09.000+00:00",
|
||||
"end_time": "2025-06-23T05:18:52.000+00:00",
|
||||
"duration_seconds": 163,
|
||||
"location": {
|
||||
"latitude": 45.79242277777778,
|
||||
"longitude": 24.139758888888892
|
||||
},
|
||||
"location_suggestion": "\u0218oseaua Alba Iulia"
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-23T05:26:59.000+00:00",
|
||||
"end_time": "2025-06-23T13:42:32.000+00:00",
|
||||
"duration_seconds": 29558,
|
||||
"location": {
|
||||
"latitude": 45.79897166666667,
|
||||
"longitude": 24.085509444444444
|
||||
},
|
||||
"location_suggestion": "Strada Monaco"
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-24T05:02:36.000+00:00",
|
||||
"end_time": "2025-06-24T05:06:24.161+00:00",
|
||||
"duration_seconds": 228,
|
||||
"location": {
|
||||
"latitude": 45.79917111111111,
|
||||
"longitude": 24.086246666666668
|
||||
},
|
||||
"location_suggestion": "Strada Europa Unit\u0103"
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-24T05:06:26.000+00:00",
|
||||
"end_time": "2025-06-24T13:44:34.000+00:00",
|
||||
"duration_seconds": 31088,
|
||||
"location": {
|
||||
"latitude": 45.79897666666667,
|
||||
"longitude": 24.08552166666667
|
||||
},
|
||||
"location_suggestion": "Strada Monaco"
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-25T05:23:38.000+00:00",
|
||||
"end_time": "2025-06-25T13:52:00.000+00:00",
|
||||
"duration_seconds": 30500,
|
||||
"location": {
|
||||
"latitude": 45.799183888888884,
|
||||
"longitude": 24.085637777777777
|
||||
},
|
||||
"location_suggestion": "Strada Monaco"
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-26T05:10:24.000+00:00",
|
||||
"end_time": "2025-06-26T14:09:48.774+00:00",
|
||||
"duration_seconds": 32365,
|
||||
"location": {
|
||||
"latitude": 45.799045,
|
||||
"longitude": 24.085640555555557
|
||||
},
|
||||
"location_suggestion": "Strada Monaco"
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-27T05:13:04.000+00:00",
|
||||
"end_time": "2025-06-27T14:08:01.630+00:00",
|
||||
"duration_seconds": 32098,
|
||||
"location": {
|
||||
"latitude": 45.79897444444445,
|
||||
"longitude": 24.085511666666665
|
||||
},
|
||||
"location_suggestion": "Strada Monaco"
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-30T04:39:06.000+00:00",
|
||||
"end_time": "2025-06-30T13:13:43.000+00:00",
|
||||
"duration_seconds": 30878,
|
||||
"location": {
|
||||
"latitude": 45.798997222222226,
|
||||
"longitude": 24.085517777777778
|
||||
},
|
||||
"location_suggestion": "Strada Monaco"
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
1296
resources/projects/Strategica/preview.html
Normal file
1296
resources/projects/Strategica/preview.html
Normal file
File diff suppressed because one or more lines are too long
BIN
resources/projects/Strategica/preview.png
Normal file
BIN
resources/projects/Strategica/preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 792 KiB |
@@ -1,101 +0,0 @@
|
||||
[
|
||||
{
|
||||
"start_time": "2025-06-19T05:25:20.000+00:00",
|
||||
"end_time": "2025-06-19T14:09:05.000+00:00",
|
||||
"duration_seconds": 31426,
|
||||
"location": {
|
||||
"latitude": 45.79909222222222,
|
||||
"longitude": 24.085676666666668
|
||||
}
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-20T05:18:03.000+00:00",
|
||||
"end_time": "2025-06-20T14:05:26.784+00:00",
|
||||
"duration_seconds": 31644,
|
||||
"location": {
|
||||
"latitude": 45.798976111111116,
|
||||
"longitude": 24.085469444444445
|
||||
}
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-21T07:45:07.000+00:00",
|
||||
"end_time": "2025-06-21T08:04:04.000+00:00",
|
||||
"duration_seconds": 1049,
|
||||
"location": {
|
||||
"latitude": 45.82062444444445,
|
||||
"longitude": 24.151072222222222
|
||||
}
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-23T05:16:09.000+00:00",
|
||||
"end_time": "2025-06-23T05:18:52.000+00:00",
|
||||
"duration_seconds": 163,
|
||||
"location": {
|
||||
"latitude": 45.79242277777778,
|
||||
"longitude": 24.139758888888892
|
||||
}
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-23T05:26:59.000+00:00",
|
||||
"end_time": "2025-06-23T13:42:32.000+00:00",
|
||||
"duration_seconds": 29558,
|
||||
"location": {
|
||||
"latitude": 45.79897166666667,
|
||||
"longitude": 24.085509444444444
|
||||
}
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-24T05:02:36.000+00:00",
|
||||
"end_time": "2025-06-24T05:06:24.161+00:00",
|
||||
"duration_seconds": 228,
|
||||
"location": {
|
||||
"latitude": 45.79917111111111,
|
||||
"longitude": 24.086246666666668
|
||||
}
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-24T05:06:26.000+00:00",
|
||||
"end_time": "2025-06-24T13:44:34.000+00:00",
|
||||
"duration_seconds": 31088,
|
||||
"location": {
|
||||
"latitude": 45.79897666666667,
|
||||
"longitude": 24.08552166666667
|
||||
}
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-25T05:23:38.000+00:00",
|
||||
"end_time": "2025-06-25T13:52:00.000+00:00",
|
||||
"duration_seconds": 30500,
|
||||
"location": {
|
||||
"latitude": 45.799183888888884,
|
||||
"longitude": 24.085637777777777
|
||||
}
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-26T05:10:24.000+00:00",
|
||||
"end_time": "2025-06-26T14:09:48.774+00:00",
|
||||
"duration_seconds": 32365,
|
||||
"location": {
|
||||
"latitude": 45.799045,
|
||||
"longitude": 24.085640555555557
|
||||
}
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-27T05:13:04.000+00:00",
|
||||
"end_time": "2025-06-27T14:08:01.630+00:00",
|
||||
"duration_seconds": 32098,
|
||||
"location": {
|
||||
"latitude": 45.79897444444445,
|
||||
"longitude": 24.085511666666665
|
||||
}
|
||||
},
|
||||
{
|
||||
"start_time": "2025-06-30T04:39:06.000+00:00",
|
||||
"end_time": "2025-06-30T13:13:43.000+00:00",
|
||||
"duration_seconds": 30878,
|
||||
"location": {
|
||||
"latitude": 45.798997222222226,
|
||||
"longitude": 24.085517777777778
|
||||
}
|
||||
}
|
||||
]
|
||||
Binary file not shown.
BIN
screens/__pycache__/pause_edit_screen_improved.cpython-311.pyc
Normal file
BIN
screens/__pycache__/pause_edit_screen_improved.cpython-311.pyc
Normal file
Binary file not shown.
@@ -8,6 +8,7 @@ from kivy.properties import StringProperty, NumericProperty, AliasProperty
|
||||
from utils import (
|
||||
process_preview_util, optimize_route_entries_util
|
||||
)
|
||||
from video_3d_generator import generate_3d_video_animation
|
||||
from kivy.uix.popup import Popup
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.label import Label
|
||||
@@ -170,3 +171,33 @@ class CreateAnimationScreen(Screen):
|
||||
pause_edit_screen.set_project_and_callback(self.project_name, self.on_pre_enter)
|
||||
self.manager.current = "pause_edit"
|
||||
|
||||
def generate_3d_video(self):
|
||||
"""Generate a 3D video animation similar to Relive"""
|
||||
# Show processing popup
|
||||
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||||
label = Label(text="Preparing 3D video generation...")
|
||||
progress = ProgressBar(max=100, value=0)
|
||||
layout.add_widget(label)
|
||||
layout.add_widget(progress)
|
||||
popup = Popup(
|
||||
title="Generating 3D Video Animation",
|
||||
content=layout,
|
||||
size_hint=(0.9, None),
|
||||
size=(0, 200),
|
||||
auto_dismiss=False
|
||||
)
|
||||
popup.open()
|
||||
|
||||
# Schedule the 3D video generation
|
||||
Clock.schedule_once(
|
||||
lambda dt: generate_3d_video_animation(
|
||||
self.project_name,
|
||||
RESOURCES_FOLDER,
|
||||
label,
|
||||
progress,
|
||||
popup,
|
||||
Clock
|
||||
),
|
||||
0.5
|
||||
)
|
||||
|
||||
|
||||
@@ -7,14 +7,18 @@ 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
|
||||
|
||||
@@ -25,10 +29,91 @@ class PauseEditScreen(Screen):
|
||||
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.build_pause_layout()
|
||||
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 and improved location suggestion with focus on practical results"""
|
||||
|
||||
889
screens/pause_edit_screen_improved.py
Normal file
889
screens/pause_edit_screen_improved.py
Normal 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()
|
||||
1962
screens/pause_edit_screen_legacy.py
Normal file
1962
screens/pause_edit_screen_legacy.py
Normal file
File diff suppressed because it is too large
Load Diff
34
traccar.kv
34
traccar.kv
@@ -722,6 +722,40 @@
|
||||
height: 202
|
||||
size_hint_x: 1
|
||||
|
||||
# 3D Video 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 3D video animation\nsimilar to Relive style"
|
||||
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 Video"
|
||||
size_hint_x: 0.3
|
||||
font_size: 14
|
||||
background_color: 0.8, 0.2, 0.4, 1
|
||||
color: 1, 1, 1, 1
|
||||
halign: "center"
|
||||
valign: "middle"
|
||||
text_size: self.size
|
||||
on_press: root.generate_3d_video()
|
||||
|
||||
|
||||
Widget:
|
||||
size_hint_y: 1
|
||||
|
||||
296
video_3d_generator.py
Normal file
296
video_3d_generator.py
Normal 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
|
||||
Reference in New Issue
Block a user