Files
traccar_animation/screens/create_animation_screen.py
2025-06-06 21:29:07 +03:00

338 lines
14 KiB
Python

import kivy
from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen
import os
import json
from kivy.clock import Clock
from kivy.properties import StringProperty, NumericProperty, AliasProperty
from utils import (
generate_key, load_key, encrypt_data, decrypt_data,
check_server_settings, save_server_settings,
test_connection, get_devices_from_server, save_route_to_file, fetch_positions_for_selected_day,
process_preview_util
)
from datetime import date
from kivy.uix.popup import Popup
from kivy.uix.gridlayout import GridLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.boxlayout import BoxLayout
from threading import Thread
from kivy.clock import mainthread
from kivy.uix.image import Image
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.progressbar import ProgressBar
from config import RESOURCES_FOLDER, CREDENTIALS_FILE
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from PIL import Image
import time
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from PIL import Image
import time
import os
import math
class CreateAnimationScreen(Screen):
project_name = StringProperty("")
preview_html_path = StringProperty("") # Path to the HTML file for preview
preview_image_path = StringProperty("") # Add this line
preview_image_version = NumericProperty(0) # Add this line
def get_preview_image_source(self):
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
img_path = os.path.join(project_folder, "preview.png")
if os.path.exists(img_path):
return img_path
return "resources/images/track.png"
preview_image_source = AliasProperty(
get_preview_image_source, None, bind=['project_name', 'preview_image_version']
)
def on_pre_enter(self):
# Update the route entries label with the actual number of entries
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
positions_path = os.path.join(project_folder, "positions.json")
count = 0
if os.path.exists(positions_path):
with open(positions_path, "r") as f:
try:
positions = json.load(f)
count = len(positions)
except Exception:
count = 0
self.ids.route_entries_label.text = f"Your route has {count} entries,"
def open_rename_popup(self):
from kivy.uix.popup import Popup
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.label import Label
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Enter new project name:")
input_field = TextInput(text=self.project_name, multiline=False)
btn_save = Button(text="Save", background_color=(0.008, 0.525, 0.290, 1))
btn_cancel = Button(text="Cancel")
layout.add_widget(label)
layout.add_widget(input_field)
layout.add_widget(btn_save)
layout.add_widget(btn_cancel)
popup = Popup(
title="Rename Project",
content=layout,
size_hint=(0.92, None),
size=(0, 260),
auto_dismiss=False
)
def do_rename(instance):
new_name = input_field.text.strip()
if new_name and new_name != self.project_name:
if self.rename_project_folder(self.project_name, new_name):
self.project_name = new_name
popup.dismiss()
self.on_pre_enter() # Refresh label
else:
label.text = "Rename failed (name exists?)"
else:
label.text = "Please enter a new name."
btn_save.bind(on_press=do_rename)
btn_cancel.bind(on_press=lambda x: popup.dismiss())
popup.open()
def rename_project_folder(self, old_name, new_name):
import os
old_path = os.path.join(RESOURCES_FOLDER, "projects", old_name)
new_path = os.path.join(RESOURCES_FOLDER, "projects", new_name)
if os.path.exists(old_path) and not os.path.exists(new_path):
os.rename(old_path, new_path)
return True
return False
def optimize_route_entries(self):
# Show popup with progress bar
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Processing route entries...")
progress = ProgressBar(max=100, value=0)
layout.add_widget(label)
layout.add_widget(progress)
popup = Popup(
title="Optimizing Route",
content=layout,
size_hint=(0.92, None),
size=(0, 260),
auto_dismiss=False
)
popup.open()
def process_entries(dt):
import datetime
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.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 >= 180:
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
start_lat, start_lon = new_positions[0]['latitude'], new_positions[0]['longitude']
end_lat, end_lon = new_positions[-1]['latitude'], new_positions[-1]['longitude']
filtered_pauses = []
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)
# 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)}"
)
btn_save = Button(text="Save optimized file", background_color=(0.008, 0.525, 0.290, 1))
btn_cancel = Button(text="Cancel")
btn_box = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=44)
btn_box.add_widget(btn_save)
btn_box.add_widget(btn_cancel)
layout.add_widget(btn_box)
def save_optimized(instance):
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()
self.on_pre_enter() # Refresh the screen
Clock.schedule_once(close_and_refresh, 1)
btn_save.bind(on_press=save_optimized)
btn_cancel.bind(on_press=lambda x: popup.dismiss())
Clock.schedule_once(process_entries, 0.5)
def preview_route(self):
# Show processing popup
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text="Processing route preview...")
progress = ProgressBar(max=100, value=0)
layout.add_widget(label)
layout.add_widget(progress)
popup = Popup(
title="Previewing Route",
content=layout,
size_hint=(0.8, None),
size=(0, 180),
auto_dismiss=False
)
popup.open()
def set_preview_image_path(path):
self.preview_image_path = path
self.preview_image_version += 1 # Force AliasProperty to update
self.property('preview_image_source').dispatch(self)
self.ids.preview_image.reload()
# Schedule the processing function
Clock.schedule_once(
lambda dt: process_preview_util(
self.project_name,
RESOURCES_FOLDER,
label,
progress,
popup,
self.ids.preview_image,
set_preview_image_path,
Clock
),
0.5
)
def 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))