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))