updated to pauses
This commit is contained in:
Binary file not shown.
@@ -1 +1 @@
|
|||||||
gAAAAABoQzLdIdf3DPBo8ovchYeCI6p5KPYw_8GLixB4NpVQfJRQFWvDvi1b7BmxTqwx8UTuiOFabSPBEhR25LARU7YlTVuoYxFZjahHwvu7COTzRfi98qePYrhvb1AFoJSkDNWbbbfg
|
gAAAAABoRH_Q_Meahpxk7Lunh7m8alx_y11I13YKM8SbTOBFqZrqoZ6pCxNK_RMBDRXwoeJhhDCVppaX-WPMGHljzBrsTQ9mUa9yDS6Sp8t-fvZnYEcKlIoRwdqRd3b1HdyJgBxH_AjD
|
||||||
BIN
resources/projects/aaaddd/pause_1/delete.png
Normal file
BIN
resources/projects/aaaddd/pause_1/delete.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
BIN
resources/projects/aaaddd/pause_1/edit pencil.png
Normal file
BIN
resources/projects/aaaddd/pause_1/edit pencil.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
BIN
resources/projects/aaaddd/pause_1/track.png
Normal file
BIN
resources/projects/aaaddd/pause_1/track.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
@@ -6,6 +6,7 @@
|
|||||||
"location": {
|
"location": {
|
||||||
"latitude": 45.79917944444444,
|
"latitude": 45.79917944444444,
|
||||||
"longitude": 24.085654444444444
|
"longitude": 24.085654444444444
|
||||||
}
|
},
|
||||||
|
"name": "Lucru greu"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
File diff suppressed because one or more lines are too long
BIN
resources/projects/aaaddd/preview.png
Normal file
BIN
resources/projects/aaaddd/preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 877 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 877 KiB |
Binary file not shown.
@@ -9,7 +9,7 @@ from utils import (
|
|||||||
generate_key, load_key, encrypt_data, decrypt_data,
|
generate_key, load_key, encrypt_data, decrypt_data,
|
||||||
check_server_settings, save_server_settings,
|
check_server_settings, save_server_settings,
|
||||||
test_connection, get_devices_from_server, save_route_to_file, fetch_positions_for_selected_day,
|
test_connection, get_devices_from_server, save_route_to_file, fetch_positions_for_selected_day,
|
||||||
process_preview_util
|
process_preview_util, optimize_route_entries_util
|
||||||
)
|
)
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from kivy.uix.popup import Popup
|
from kivy.uix.popup import Popup
|
||||||
@@ -34,6 +34,9 @@ from PIL import Image
|
|||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
import math
|
import math
|
||||||
|
from kivy.uix.filechooser import FileChooserIconView
|
||||||
|
from kivy.uix.textinput import TextInput
|
||||||
|
from widgets.pause_edit_popup import open_pauses_popup
|
||||||
|
|
||||||
class CreateAnimationScreen(Screen):
|
class CreateAnimationScreen(Screen):
|
||||||
project_name = StringProperty("")
|
project_name = StringProperty("")
|
||||||
@@ -66,6 +69,8 @@ class CreateAnimationScreen(Screen):
|
|||||||
count = 0
|
count = 0
|
||||||
self.ids.route_entries_label.text = f"Your route has {count} entries,"
|
self.ids.route_entries_label.text = f"Your route has {count} entries,"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def open_rename_popup(self):
|
def open_rename_popup(self):
|
||||||
from kivy.uix.popup import Popup
|
from kivy.uix.popup import Popup
|
||||||
from kivy.uix.boxlayout import BoxLayout
|
from kivy.uix.boxlayout import BoxLayout
|
||||||
@@ -118,7 +123,7 @@ class CreateAnimationScreen(Screen):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def optimize_route_entries(self):
|
def optimize_route_entries(self):
|
||||||
# Show popup with progress bar
|
# Create the popup and UI elements
|
||||||
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||||||
label = Label(text="Processing route entries...")
|
label = Label(text="Processing route entries...")
|
||||||
progress = ProgressBar(max=100, value=0)
|
progress = ProgressBar(max=100, value=0)
|
||||||
@@ -133,161 +138,17 @@ class CreateAnimationScreen(Screen):
|
|||||||
)
|
)
|
||||||
popup.open()
|
popup.open()
|
||||||
|
|
||||||
def process_entries(dt):
|
# Now call the utility function with these objects
|
||||||
import datetime
|
optimize_route_entries_util(
|
||||||
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
|
self.project_name,
|
||||||
positions_path = os.path.join(project_folder, "positions.json")
|
RESOURCES_FOLDER,
|
||||||
pauses_path = os.path.join(project_folder, "pauses.json")
|
label,
|
||||||
if not os.path.exists(positions_path):
|
progress,
|
||||||
label.text = "positions.json not found!"
|
popup,
|
||||||
progress.value = 100
|
Clock,
|
||||||
return
|
on_save=lambda: self.on_pre_enter()
|
||||||
|
)
|
||||||
|
|
||||||
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):
|
def preview_route(self):
|
||||||
# Show processing popup
|
# Show processing popup
|
||||||
@@ -325,13 +186,6 @@ class CreateAnimationScreen(Screen):
|
|||||||
0.5
|
0.5
|
||||||
)
|
)
|
||||||
|
|
||||||
def haversine(lat1, lon1, lat2, lon2):
|
def open_pauses_popup(self):
|
||||||
# Returns distance in meters between two lat/lon points
|
open_pauses_popup(self, self.project_name, RESOURCES_FOLDER, on_save_callback=self.on_pre_enter)
|
||||||
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))
|
|
||||||
|
|
||||||
|
|||||||
44
traccar.kv
44
traccar.kv
@@ -642,13 +642,46 @@
|
|||||||
valign: "middle"
|
valign: "middle"
|
||||||
text_size: self.size
|
text_size: self.size
|
||||||
on_press: root.optimize_route_entries()
|
on_press: root.optimize_route_entries()
|
||||||
|
|
||||||
|
# Pauses frame
|
||||||
|
BoxLayout:
|
||||||
|
id: pauses_frame
|
||||||
|
orientation: "horizontal"
|
||||||
|
spacing: 10
|
||||||
|
padding: 10
|
||||||
|
size_hint_y: None
|
||||||
|
height: 60
|
||||||
|
canvas.before:
|
||||||
|
Color:
|
||||||
|
rgba: 0.15, 0.15, 0.18, 1
|
||||||
|
Rectangle:
|
||||||
|
pos: self.pos
|
||||||
|
size: self.size
|
||||||
|
Label:
|
||||||
|
id: pauses_label
|
||||||
|
text: "Pauses"
|
||||||
|
font_size: 16
|
||||||
|
halign: "left"
|
||||||
|
valign: "middle"
|
||||||
|
color: 1, 1, 1, 1
|
||||||
|
size_hint_x: 0.7
|
||||||
|
text_size: self.width, None
|
||||||
|
Button:
|
||||||
|
id: pauses_edit_btn
|
||||||
|
text: "Edit"
|
||||||
|
size_hint_x: 0.3
|
||||||
|
width: 120
|
||||||
|
font_size: 16
|
||||||
|
background_color: 0.341, 0.235, 0.980, 1
|
||||||
|
color: 1, 1, 1, 1
|
||||||
|
on_press: root.open_pauses_popup()
|
||||||
|
|
||||||
# Preview frame (label + button on first row, image on second row)
|
# Preview frame (label + button on first row, image on second row)
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
orientation: "vertical"
|
orientation: "vertical"
|
||||||
size_hint_y: None
|
size_hint_y: None
|
||||||
height: 300 # Adjust as needed for your image size
|
height: 255 # Adjust as needed for your image size
|
||||||
padding: [10, 10, 10, 10]
|
padding: [5, 5, 5, 5]
|
||||||
spacing: 10
|
spacing: 10
|
||||||
canvas.before:
|
canvas.before:
|
||||||
Color:
|
Color:
|
||||||
@@ -660,8 +693,8 @@
|
|||||||
BoxLayout:
|
BoxLayout:
|
||||||
orientation: "horizontal"
|
orientation: "horizontal"
|
||||||
size_hint_y: None
|
size_hint_y: None
|
||||||
height: 60
|
height: 30
|
||||||
spacing: 10
|
spacing: 5
|
||||||
|
|
||||||
Label:
|
Label:
|
||||||
text: "Preview your route"
|
text: "Preview your route"
|
||||||
@@ -700,4 +733,5 @@
|
|||||||
background_color: 0.341, 0.235, 0.980, 1
|
background_color: 0.341, 0.235, 0.980, 1
|
||||||
color: 1, 1, 1, 1
|
color: 1, 1, 1, 1
|
||||||
font_size: 16
|
font_size: 16
|
||||||
on_press: app.root.current = "home"
|
on_press: app.root.current = "home"
|
||||||
|
|
||||||
|
|||||||
185
utils.py
185
utils.py
@@ -2,7 +2,8 @@ import os
|
|||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
from cryptography.fernet import Fernet
|
from cryptography.fernet import Fernet
|
||||||
|
import math
|
||||||
|
import datetime
|
||||||
RESOURCES_FOLDER = "resources"
|
RESOURCES_FOLDER = "resources"
|
||||||
CREDENTIALS_FILE = os.path.join(RESOURCES_FOLDER, "credentials.enc")
|
CREDENTIALS_FILE = os.path.join(RESOURCES_FOLDER, "credentials.enc")
|
||||||
KEY_FILE = os.path.join(RESOURCES_FOLDER, "key.key")
|
KEY_FILE = os.path.join(RESOURCES_FOLDER, "key.key")
|
||||||
@@ -306,3 +307,185 @@ def process_preview_util(
|
|||||||
def close_popup(dt):
|
def close_popup(dt):
|
||||||
popup.dismiss()
|
popup.dismiss()
|
||||||
Clock.schedule_once(close_popup, 2)
|
Clock.schedule_once(close_popup, 2)
|
||||||
|
|
||||||
|
|
||||||
|
def haversine(lat1, lon1, lat2, lon2):
|
||||||
|
# Returns distance in meters between two lat/lon points
|
||||||
|
R = 6371000 # Earth radius in meters
|
||||||
|
phi1 = math.radians(lat1)
|
||||||
|
phi2 = math.radians(lat2)
|
||||||
|
dphi = math.radians(lat2 - lat1)
|
||||||
|
dlambda = math.radians(lon2 - lon1)
|
||||||
|
a = math.sin(dphi/2)**2 + math.cos(phi1)*math.cos(phi2)*math.sin(dlambda/2)**2
|
||||||
|
return 2 * R * math.asin(math.sqrt(a))
|
||||||
|
|
||||||
|
def optimize_route_entries_util(
|
||||||
|
project_name,
|
||||||
|
RESOURCES_FOLDER,
|
||||||
|
label,
|
||||||
|
progress,
|
||||||
|
popup,
|
||||||
|
Clock,
|
||||||
|
on_save=None
|
||||||
|
):
|
||||||
|
def process_entries(dt):
|
||||||
|
project_folder = os.path.join(RESOURCES_FOLDER, "projects", project_name)
|
||||||
|
positions_path = os.path.join(project_folder, "positions.json")
|
||||||
|
pauses_path = os.path.join(project_folder, "pauses.json")
|
||||||
|
if not os.path.exists(positions_path):
|
||||||
|
label.text = "positions.json not found!"
|
||||||
|
progress.value = 100
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(positions_path, "r") as f:
|
||||||
|
positions = json.load(f)
|
||||||
|
|
||||||
|
# Detect duplicate positions at the start
|
||||||
|
start_remove = 0
|
||||||
|
if positions:
|
||||||
|
first = positions[0]
|
||||||
|
for pos in positions:
|
||||||
|
if pos['latitude'] == first['latitude'] and pos['longitude'] == first['longitude']:
|
||||||
|
start_remove += 1
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
if start_remove > 0:
|
||||||
|
start_remove -= 1
|
||||||
|
|
||||||
|
# Detect duplicate positions at the end
|
||||||
|
end_remove = 0
|
||||||
|
if positions:
|
||||||
|
last = positions[-1]
|
||||||
|
for pos in reversed(positions):
|
||||||
|
if pos['latitude'] == last['latitude'] and pos['longitude'] == last['longitude']:
|
||||||
|
end_remove += 1
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
if end_remove > 0:
|
||||||
|
end_remove -= 1
|
||||||
|
|
||||||
|
# Shorten the positions list
|
||||||
|
new_positions = positions[start_remove:len(positions)-end_remove if end_remove > 0 else None]
|
||||||
|
|
||||||
|
# --- PAUSE DETECTION ---
|
||||||
|
pauses = []
|
||||||
|
if new_positions:
|
||||||
|
pause_start = None
|
||||||
|
pause_end = None
|
||||||
|
pause_location = None
|
||||||
|
for i in range(1, len(new_positions)):
|
||||||
|
prev = new_positions[i-1]
|
||||||
|
curr = new_positions[i]
|
||||||
|
# Check if stopped (same location)
|
||||||
|
if curr['latitude'] == prev['latitude'] and curr['longitude'] == prev['longitude']:
|
||||||
|
if pause_start is None:
|
||||||
|
pause_start = prev['deviceTime']
|
||||||
|
pause_location = (prev['latitude'], prev['longitude'])
|
||||||
|
pause_end = curr['deviceTime']
|
||||||
|
else:
|
||||||
|
if pause_start and pause_end:
|
||||||
|
# Calculate pause duration
|
||||||
|
t1 = datetime.datetime.fromisoformat(pause_start.replace('Z', '+00:00'))
|
||||||
|
t2 = datetime.datetime.fromisoformat(pause_end.replace('Z', '+00:00'))
|
||||||
|
duration = (t2 - t1).total_seconds()
|
||||||
|
if duration >= 120:
|
||||||
|
pauses.append({
|
||||||
|
"start_time": pause_start,
|
||||||
|
"end_time": pause_end,
|
||||||
|
"duration_seconds": int(duration),
|
||||||
|
"location": {"latitude": pause_location[0], "longitude": pause_location[1]}
|
||||||
|
})
|
||||||
|
pause_start = None
|
||||||
|
pause_end = None
|
||||||
|
pause_location = None
|
||||||
|
# Check for pause at the end
|
||||||
|
if pause_start and pause_end:
|
||||||
|
t1 = datetime.datetime.fromisoformat(pause_start.replace('Z', '+00:00'))
|
||||||
|
t2 = datetime.datetime.fromisoformat(pause_end.replace('Z', '+00:00'))
|
||||||
|
duration = (t2 - t1).total_seconds()
|
||||||
|
if duration >= 120:
|
||||||
|
pauses.append({
|
||||||
|
"start_time": pause_start,
|
||||||
|
"end_time": pause_end,
|
||||||
|
"duration_seconds": int(duration),
|
||||||
|
"location": {"latitude": pause_location[0], "longitude": pause_location[1]}
|
||||||
|
})
|
||||||
|
|
||||||
|
# --- FILTER PAUSES ---
|
||||||
|
# 1. Remove pauses near start/end
|
||||||
|
filtered_pauses = []
|
||||||
|
if new_positions and pauses:
|
||||||
|
start_lat, start_lon = new_positions[0]['latitude'], new_positions[0]['longitude']
|
||||||
|
end_lat, end_lon = new_positions[-1]['latitude'], new_positions[-1]['longitude']
|
||||||
|
for pause in pauses:
|
||||||
|
plat = pause["location"]["latitude"]
|
||||||
|
plon = pause["location"]["longitude"]
|
||||||
|
dist_start = haversine(start_lat, start_lon, plat, plon)
|
||||||
|
dist_end = haversine(end_lat, end_lon, plat, plon)
|
||||||
|
if dist_start < 50 or dist_end < 50:
|
||||||
|
continue # Skip pauses near start or end
|
||||||
|
filtered_pauses.append(pause)
|
||||||
|
else:
|
||||||
|
filtered_pauses = pauses
|
||||||
|
|
||||||
|
# 2. Merge pauses close in time and space
|
||||||
|
merged_pauses = []
|
||||||
|
filtered_pauses.sort(key=lambda p: p["start_time"])
|
||||||
|
for pause in filtered_pauses:
|
||||||
|
if not merged_pauses:
|
||||||
|
merged_pauses.append(pause)
|
||||||
|
else:
|
||||||
|
last = merged_pauses[-1]
|
||||||
|
# Time difference in seconds
|
||||||
|
t1 = datetime.datetime.fromisoformat(last["end_time"].replace('Z', '+00:00'))
|
||||||
|
t2 = datetime.datetime.fromisoformat(pause["start_time"].replace('Z', '+00:00'))
|
||||||
|
time_diff = (t2 - t1).total_seconds()
|
||||||
|
# Distance in meters
|
||||||
|
last_lat = last["location"]["latitude"]
|
||||||
|
last_lon = last["location"]["longitude"]
|
||||||
|
plat = pause["location"]["latitude"]
|
||||||
|
plon = pause["location"]["longitude"]
|
||||||
|
dist = haversine(last_lat, last_lon, plat, plon)
|
||||||
|
if time_diff < 300 and dist < 50:
|
||||||
|
# Merge: extend last pause's end_time and duration
|
||||||
|
last["end_time"] = pause["end_time"]
|
||||||
|
last["duration_seconds"] += pause["duration_seconds"]
|
||||||
|
else:
|
||||||
|
merged_pauses.append(pause)
|
||||||
|
pauses = merged_pauses
|
||||||
|
|
||||||
|
progress.value = 100
|
||||||
|
label.text = (
|
||||||
|
f"Entries removable at start: {start_remove}\n"
|
||||||
|
f"Entries removable at end: {end_remove}\n"
|
||||||
|
f"Detected pauses: {len(pauses)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
from kivy.uix.button import Button
|
||||||
|
from kivy.uix.boxlayout import BoxLayout
|
||||||
|
|
||||||
|
btn_save = Button(text="Save optimized file", background_color=(0.008, 0.525, 0.290, 1))
|
||||||
|
btn_cancel = Button(text="Cancel")
|
||||||
|
btn_box = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=44)
|
||||||
|
btn_box.add_widget(btn_save)
|
||||||
|
btn_box.add_widget(btn_cancel)
|
||||||
|
popup.content.add_widget(btn_box)
|
||||||
|
|
||||||
|
def save_optimized(instance):
|
||||||
|
with open(positions_path, "w") as f:
|
||||||
|
json.dump(new_positions, f, indent=2)
|
||||||
|
with open(pauses_path, "w") as f:
|
||||||
|
json.dump(pauses, f, indent=2)
|
||||||
|
label.text = "File optimized and pauses saved!"
|
||||||
|
btn_save.disabled = True
|
||||||
|
btn_cancel.disabled = True
|
||||||
|
def close_and_refresh(dt):
|
||||||
|
popup.dismiss()
|
||||||
|
if on_save:
|
||||||
|
on_save()
|
||||||
|
Clock.schedule_once(close_and_refresh, 1)
|
||||||
|
|
||||||
|
btn_save.bind(on_press=save_optimized)
|
||||||
|
btn_cancel.bind(on_press=lambda x: popup.dismiss())
|
||||||
|
|
||||||
|
Clock.schedule_once(process_entries, 0.5)
|
||||||
|
|||||||
BIN
widgets/__pycache__/pause_edit_popup.cpython-311.pyc
Normal file
BIN
widgets/__pycache__/pause_edit_popup.cpython-311.pyc
Normal file
Binary file not shown.
329
widgets/pause_edit_popup.py
Normal file
329
widgets/pause_edit_popup.py
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
from kivy.uix.popup import Popup
|
||||||
|
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.graphics import Color, Rectangle
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
def open_pauses_popup(screen_instance, project_name, RESOURCES_FOLDER, on_save_callback=None):
|
||||||
|
project_folder = os.path.join(RESOURCES_FOLDER, "projects", project_name)
|
||||||
|
pauses_path = os.path.join(project_folder, "pauses.json")
|
||||||
|
|
||||||
|
# Main layout with dark background
|
||||||
|
layout = BoxLayout(orientation='vertical', spacing=14, padding=14)
|
||||||
|
with layout.canvas.before:
|
||||||
|
Color(0.13, 0.13, 0.16, 1)
|
||||||
|
layout.bg_rect = Rectangle(pos=layout.pos, size=layout.size)
|
||||||
|
def update_bg_rect(instance, value):
|
||||||
|
layout.bg_rect.pos = layout.pos
|
||||||
|
layout.bg_rect.size = layout.size
|
||||||
|
layout.bind(pos=update_bg_rect, size=update_bg_rect)
|
||||||
|
|
||||||
|
pauses = []
|
||||||
|
if os.path.exists(pauses_path):
|
||||||
|
with open(pauses_path, "r") as f:
|
||||||
|
pauses = json.load(f)
|
||||||
|
|
||||||
|
def suggest_location_name(lat, lon):
|
||||||
|
return f"Lat {round(lat, 4)}, Lon {round(lon, 4)}"
|
||||||
|
|
||||||
|
for idx, pause in enumerate(pauses):
|
||||||
|
pause_box = BoxLayout(orientation='vertical', spacing=10, padding=[14, 12, 14, 12], size_hint_y=None, height=220)
|
||||||
|
with pause_box.canvas.before:
|
||||||
|
Color(0.20, 0.20, 0.25, 1)
|
||||||
|
pause_box.bg_rect = Rectangle(pos=pause_box.pos, size=pause_box.size)
|
||||||
|
pause_box.bind(pos=lambda inst, val: setattr(pause_box.bg_rect, 'pos', inst.pos),
|
||||||
|
size=lambda inst, val: setattr(pause_box.bg_rect, 'size', inst.size))
|
||||||
|
|
||||||
|
# Title row
|
||||||
|
title_label = Label(
|
||||||
|
text=f"[b]Pause {idx+1}[/b]",
|
||||||
|
markup=True,
|
||||||
|
font_size=18,
|
||||||
|
color=(1, 1, 1, 1),
|
||||||
|
size_hint_y=None,
|
||||||
|
height=28,
|
||||||
|
halign="left",
|
||||||
|
valign="middle"
|
||||||
|
)
|
||||||
|
def update_title_size(instance, value):
|
||||||
|
instance.text_size = (instance.width, None)
|
||||||
|
title_label.bind(width=update_title_size)
|
||||||
|
pause_box.add_widget(title_label)
|
||||||
|
|
||||||
|
# Info row: Detected location and edit button
|
||||||
|
info_box = BoxLayout(orientation='horizontal', spacing=8, size_hint_y=None, height=32)
|
||||||
|
location_label = Label(
|
||||||
|
text=pause.get('name', suggest_location_name(pause["location"]["latitude"], pause["location"]["longitude"])),
|
||||||
|
font_size=15,
|
||||||
|
color=(0.8, 0.8, 0.8, 1),
|
||||||
|
size_hint_x=0.7,
|
||||||
|
halign="left",
|
||||||
|
valign="middle"
|
||||||
|
)
|
||||||
|
location_label.bind(width=update_title_size)
|
||||||
|
edit_loc_btn = Button(
|
||||||
|
text="Edit Place",
|
||||||
|
size_hint_x=0.3,
|
||||||
|
font_size=14,
|
||||||
|
background_color=(0.341, 0.235, 0.980, 1),
|
||||||
|
color=(1, 1, 1, 1)
|
||||||
|
)
|
||||||
|
def open_edit_popup(instance, pause=pause, location_label=location_label):
|
||||||
|
edit_layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||||||
|
with edit_layout.canvas.before:
|
||||||
|
Color(0.13, 0.13, 0.16, 1)
|
||||||
|
edit_layout.bg_rect = Rectangle(pos=edit_layout.pos, size=edit_layout.size)
|
||||||
|
edit_layout.bind(pos=lambda inst, val: setattr(edit_layout.bg_rect, 'pos', inst.pos),
|
||||||
|
size=lambda inst, val: setattr(edit_layout.bg_rect, 'size', inst.size))
|
||||||
|
input_field = TextInput(text=pause.get('name', ''), multiline=False, background_color=(0.18,0.18,0.22,1), foreground_color=(1,1,1,1))
|
||||||
|
save_btn = Button(text="Save", background_color=(0.341, 0.235, 0.980, 1), color=(1,1,1,1))
|
||||||
|
edit_layout.add_widget(Label(text="Edit Place Name:", color=(1,1,1,1)))
|
||||||
|
edit_layout.add_widget(input_field)
|
||||||
|
edit_layout.add_widget(save_btn)
|
||||||
|
edit_popup = Popup(title="Edit Place", content=edit_layout, size_hint=(0.7, None), size=(0, 200))
|
||||||
|
def save_name(instance):
|
||||||
|
pause['name'] = input_field.text
|
||||||
|
location_label.text = input_field.text
|
||||||
|
edit_popup.dismiss()
|
||||||
|
save_btn.bind(on_press=save_name)
|
||||||
|
edit_popup.open()
|
||||||
|
edit_loc_btn.bind(on_press=open_edit_popup)
|
||||||
|
info_box.add_widget(location_label)
|
||||||
|
info_box.add_widget(edit_loc_btn)
|
||||||
|
pause_box.add_widget(info_box)
|
||||||
|
|
||||||
|
# Pictures folder and list
|
||||||
|
pause_img_folder = os.path.join(project_folder, f"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))]
|
||||||
|
|
||||||
|
# Photo count label
|
||||||
|
if img_list:
|
||||||
|
photo_count_label = Label(
|
||||||
|
text=f"You have {len(img_list)} photo(s) for this pause.",
|
||||||
|
font_size=13,
|
||||||
|
color=(0.8, 0.8, 0.8, 1),
|
||||||
|
size_hint_y=None,
|
||||||
|
height=22
|
||||||
|
)
|
||||||
|
photo_list_label = Label(
|
||||||
|
text=", ".join(img_list),
|
||||||
|
font_size=12,
|
||||||
|
color=(0.7, 0.7, 0.7, 1),
|
||||||
|
size_hint_y=None,
|
||||||
|
height=18
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
photo_count_label = Label(
|
||||||
|
text="No photos are set for this pause.",
|
||||||
|
font_size=13,
|
||||||
|
color=(0.8, 0.8, 0.8, 1),
|
||||||
|
size_hint_y=None,
|
||||||
|
height=22
|
||||||
|
)
|
||||||
|
photo_list_label = Label(
|
||||||
|
text="",
|
||||||
|
font_size=12,
|
||||||
|
color=(0.7, 0.7, 0.7, 1),
|
||||||
|
size_hint_y=None,
|
||||||
|
height=18
|
||||||
|
)
|
||||||
|
pause_box.add_widget(photo_count_label)
|
||||||
|
pause_box.add_widget(photo_list_label)
|
||||||
|
|
||||||
|
# Bottom row: Browse, Delete, and Save Pause
|
||||||
|
bottom_box = BoxLayout(orientation='horizontal', spacing=8, size_hint_y=None, height=36)
|
||||||
|
|
||||||
|
browse_btn = Button(
|
||||||
|
text="Browse",
|
||||||
|
size_hint_x=0.33,
|
||||||
|
font_size=14,
|
||||||
|
background_color=(0.341, 0.235, 0.980, 1),
|
||||||
|
color=(1,1,1,1)
|
||||||
|
)
|
||||||
|
|
||||||
|
def open_filechooser(instance, pause_img_folder=pause_img_folder, photo_count_label=photo_count_label, photo_list_label=photo_list_label):
|
||||||
|
chooser_layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||||||
|
with chooser_layout.canvas.before:
|
||||||
|
Color(0.13, 0.13, 0.16, 1)
|
||||||
|
chooser_layout.bg_rect = Rectangle(pos=chooser_layout.pos, size=chooser_layout.size)
|
||||||
|
chooser_layout.bind(pos=lambda inst, val: setattr(chooser_layout.bg_rect, 'pos', inst.pos),
|
||||||
|
size=lambda inst, val: setattr(chooser_layout.bg_rect, 'size', inst.size))
|
||||||
|
chooser = FileChooserIconView(path=".", filters=['*.png', '*.jpg', '*.jpeg'], multiselect=True)
|
||||||
|
upload_btn = Button(text="Upload Selected", size_hint_y=None, height=40, background_color=(0.341, 0.235, 0.980, 1), color=(1,1,1,1))
|
||||||
|
chooser_layout.add_widget(chooser)
|
||||||
|
chooser_layout.add_widget(upload_btn)
|
||||||
|
popup = Popup(title="Select Images", content=chooser_layout, size_hint=(0.9, 0.9))
|
||||||
|
|
||||||
|
def upload_files(instance):
|
||||||
|
selections = chooser.selection
|
||||||
|
if selections:
|
||||||
|
for selected_file in selections:
|
||||||
|
dest = os.path.join(pause_img_folder, os.path.basename(selected_file))
|
||||||
|
shutil.copy(selected_file, dest)
|
||||||
|
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:
|
||||||
|
photo_count_label.text = f"You have {len(img_list)} photo(s) for this pause."
|
||||||
|
photo_list_label.text = ", ".join(img_list)
|
||||||
|
else:
|
||||||
|
photo_count_label.text = "No photos are set for this pause."
|
||||||
|
photo_list_label.text = ""
|
||||||
|
popup.dismiss()
|
||||||
|
|
||||||
|
upload_btn.bind(on_press=upload_files)
|
||||||
|
popup.open()
|
||||||
|
browse_btn.bind(on_press=open_filechooser)
|
||||||
|
|
||||||
|
# --- Delete Button ---
|
||||||
|
delete_btn = Button(
|
||||||
|
text="Delete",
|
||||||
|
size_hint_x=0.34,
|
||||||
|
font_size=14,
|
||||||
|
background_color=(0.8, 0.1, 0.1, 1),
|
||||||
|
color=(1,1,1,1)
|
||||||
|
)
|
||||||
|
|
||||||
|
def open_delete_popup(instance, pause_img_folder=pause_img_folder, photo_count_label=photo_count_label, photo_list_label=photo_list_label):
|
||||||
|
from kivy.uix.scrollview import ScrollView
|
||||||
|
from kivy.uix.image import Image as KivyImage
|
||||||
|
from kivy.uix.widget import Widget
|
||||||
|
delete_layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||||||
|
files = [f for f in os.listdir(pause_img_folder)
|
||||||
|
if f.lower().endswith(('.jpg', '.jpeg', '.png')) and os.path.isfile(os.path.join(pause_img_folder, f))
|
||||||
|
]
|
||||||
|
file_list_box = BoxLayout(orientation='vertical', spacing=8, size_hint_y=None)
|
||||||
|
file_list_box.bind(minimum_height=file_list_box.setter('height'))
|
||||||
|
|
||||||
|
selected_files = set()
|
||||||
|
file_widgets = []
|
||||||
|
|
||||||
|
for fname in files:
|
||||||
|
thumb_path = os.path.join(pause_img_folder, fname)
|
||||||
|
# Create a Button for the row
|
||||||
|
btn = Button(
|
||||||
|
size_hint_y=None,
|
||||||
|
height=60,
|
||||||
|
background_normal='',
|
||||||
|
background_color=(0.18, 0.18, 0.22, 1),
|
||||||
|
color=(1,1,1,1)
|
||||||
|
)
|
||||||
|
# Layout for image and label inside the button
|
||||||
|
box = BoxLayout(orientation='horizontal', spacing=8, padding=4)
|
||||||
|
img_widget = KivyImage(source=thumb_path, size_hint_x=None, width=60, allow_stretch=True, keep_ratio=True)
|
||||||
|
label = Label(text=fname, color=(1,1,1,1), size_hint_x=1, halign="left", valign="middle")
|
||||||
|
label.bind(size=lambda inst, val: setattr(inst, 'text_size', (inst.width, None)))
|
||||||
|
box.add_widget(img_widget)
|
||||||
|
box.add_widget(label)
|
||||||
|
btn.add_widget(box)
|
||||||
|
|
||||||
|
def make_on_release(btn, fname):
|
||||||
|
def on_release(instance):
|
||||||
|
if fname in selected_files:
|
||||||
|
selected_files.remove(fname)
|
||||||
|
btn.background_color = (0.18, 0.18, 0.22, 1)
|
||||||
|
else:
|
||||||
|
selected_files.add(fname)
|
||||||
|
btn.background_color = (0.8, 0.1, 0.1, 1)
|
||||||
|
return on_release
|
||||||
|
btn.bind(on_release=make_on_release(btn, fname))
|
||||||
|
|
||||||
|
file_widgets.append(btn)
|
||||||
|
file_list_box.add_widget(btn)
|
||||||
|
|
||||||
|
scroll = ScrollView(size_hint=(1, 0.7))
|
||||||
|
scroll.add_widget(file_list_box)
|
||||||
|
|
||||||
|
delete_file_btn = Button(
|
||||||
|
text="Delete Selected",
|
||||||
|
size_hint_y=None,
|
||||||
|
height=40,
|
||||||
|
background_color=(0.8,0.1,0.1,1),
|
||||||
|
color=(1,1,1,1)
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete_selected(instance):
|
||||||
|
for fname in list(selected_files):
|
||||||
|
fpath = os.path.join(pause_img_folder, fname)
|
||||||
|
if os.path.exists(fpath):
|
||||||
|
os.remove(fpath)
|
||||||
|
# Update labels in main popup
|
||||||
|
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:
|
||||||
|
photo_count_label.text = f"You have {len(img_list)} photo(s) for this pause."
|
||||||
|
photo_list_label.text = ", ".join(img_list)
|
||||||
|
else:
|
||||||
|
photo_count_label.text = "No photos are set for this pause."
|
||||||
|
photo_list_label.text = ""
|
||||||
|
popup.dismiss()
|
||||||
|
|
||||||
|
delete_file_btn.bind(on_press=delete_selected)
|
||||||
|
close_btn.bind(on_press=lambda x: popup.dismiss())
|
||||||
|
|
||||||
|
delete_layout.add_widget(Label(text="Tap to select images to delete:", color=(1,1,1,1), size_hint_y=None, height=30))
|
||||||
|
delete_layout.add_widget(scroll)
|
||||||
|
delete_layout.add_widget(delete_file_btn)
|
||||||
|
delete_layout.add_widget(close_btn)
|
||||||
|
|
||||||
|
popup = Popup(
|
||||||
|
title="Delete Photo(s)",
|
||||||
|
content=delete_layout,
|
||||||
|
size_hint=(0.8, 0.8),
|
||||||
|
background_color=(0.13, 0.13, 0.16, 1)
|
||||||
|
)
|
||||||
|
popup.open()
|
||||||
|
|
||||||
|
delete_btn.bind(on_press=open_delete_popup)
|
||||||
|
|
||||||
|
save_pause_btn = Button(
|
||||||
|
text="Save Pause",
|
||||||
|
size_hint_x=0.33,
|
||||||
|
font_size=14,
|
||||||
|
background_color=(0.341, 0.235, 0.980, 1),
|
||||||
|
color=(1,1,1,1)
|
||||||
|
)
|
||||||
|
def save_pause(instance, pause=pause):
|
||||||
|
with open(pauses_path, "w") as f:
|
||||||
|
json.dump(pauses, f, indent=2)
|
||||||
|
save_pause_btn.bind(on_press=save_pause)
|
||||||
|
|
||||||
|
bottom_box.add_widget(browse_btn)
|
||||||
|
bottom_box.add_widget(delete_btn)
|
||||||
|
bottom_box.add_widget(save_pause_btn)
|
||||||
|
pause_box.add_widget(bottom_box)
|
||||||
|
|
||||||
|
# Separator line
|
||||||
|
from kivy.uix.widget import Widget
|
||||||
|
from kivy.graphics import Line
|
||||||
|
sep = Widget(size_hint_y=None, height=2)
|
||||||
|
with sep.canvas:
|
||||||
|
Color(0.25, 0.25, 0.30, 1)
|
||||||
|
Line(points=[0, 1, 1000, 1], width=2)
|
||||||
|
pause_box.add_widget(sep)
|
||||||
|
|
||||||
|
layout.add_widget(pause_box)
|
||||||
|
|
||||||
|
# Save all and close
|
||||||
|
save_all_btn = Button(text="Save All & Close", size_hint_y=None, height=44, background_color=(0.341, 0.235, 0.980, 1), color=(1,1,1,1))
|
||||||
|
def save_all(instance):
|
||||||
|
with open(pauses_path, "w") as f:
|
||||||
|
json.dump(pauses, f, indent=2)
|
||||||
|
if on_save_callback:
|
||||||
|
on_save_callback()
|
||||||
|
popup.dismiss()
|
||||||
|
save_all_btn.bind(on_press=save_all)
|
||||||
|
layout.add_widget(save_all_btn)
|
||||||
|
|
||||||
|
popup = Popup(title="Edit Pauses", content=layout, size_hint=(0.95, 0.95), background_color=(0.13, 0.13, 0.16, 1))
|
||||||
|
popup.open()
|
||||||
Reference in New Issue
Block a user