saved preview picture

This commit is contained in:
2025-06-06 16:02:50 +03:00
parent 86d81d4501
commit 5627c790f5
28 changed files with 52840 additions and 11797 deletions

Binary file not shown.

Binary file not shown.

13
config.py Normal file
View File

@@ -0,0 +1,13 @@
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.popup import Popup
from kivy.uix.image import Image
from kivy.uix.behaviors import ButtonBehavior
from kivy.clock import Clock
import os
import json
RESOURCES_FOLDER = "resources"
CREDENTIALS_FILE = f"{RESOURCES_FOLDER}/credentials.enc"

818
main.py
View File

@@ -26,821 +26,17 @@ from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from PIL import Image
import time
from screens.home_screen import HomeScreen
from screens.login_screen import LoginScreen
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 config import RESOURCES_FOLDER, CREDENTIALS_FILE
kivy.require("2.0.0")
from kivy.core.window import Window
Window.size = (360, 780)
RESOURCES_FOLDER = "resources"
CREDENTIALS_FILE = os.path.join(RESOURCES_FOLDER, "credentials.enc")
class LoginScreen(Screen):
def login(self):
username = self.ids.username_input.text.strip()
password = self.ids.username_input.text.strip()
if not username or not password:
self.manager.get_screen("home").ids.result_label.text = "Please fill in all fields."
return
credentials = {"username": username, "password": password}
encrypted_data = encrypt_data(json.dumps(credentials))
with open(CREDENTIALS_FILE, "wb") as file:
file.write(encrypted_data)
self.manager.current = "home"
class SettingsScreen(Screen):
server_response = "Waiting to test connection..."
def on_pre_enter(self):
settings = check_server_settings()
if settings:
self.ids.server_url_input.text = settings.get("server_url", "")
self.ids.username_input.text = settings.get("username", "")
self.ids.password_input.text = settings.get("password", "")
self.ids.token_input.text = settings.get("token", "")
else:
self.ids.server_url_input.text = ""
self.ids.username_input.text = ""
self.ids.password_input.text = ""
self.ids.token_input.text = ""
def test_connection(self):
server_url = self.ids.server_url_input.text.strip()
username = self.ids.username_input.text.strip()
password = self.ids.password_input.text.strip()
token = self.ids.token_input.text.strip()
result = test_connection(server_url, username, password, token)
self.server_response = result["message"]
self.ids.result_label.text = self.server_response
def save_settings(self):
server_url = self.ids.server_url_input.text.strip()
username = self.ids.username_input.text.strip()
password = self.ids.password_input.text.strip()
token = self.ids.token_input.text.strip()
if not server_url or not username or not password or not token:
self.ids.result_label.text = "Please fill in all fields."
return
settings_data = {
"server_url": server_url,
"username": username,
"password": password,
"token": token,
}
try:
save_server_settings(settings_data)
self.ids.result_label.text = "Settings saved successfully!"
self.manager.current = "home"
except Exception as e:
self.ids.result_label.text = f"Failed to save settings: {str(e)}"
# get trip from server screen
class GetTripFromServer(Screen):
server_info_text = StringProperty("LOADING DATA...")
server_box_color = ListProperty([0.984, 0.553, 0.078, 1])
device_mapping = {}
def on_pre_enter(self):
self.server_box_color = [0.984, 0.553, 0.078, 1]
self.server_info_text = "LOADING DATA..."
Clock.schedule_once(self.check_server_settings, 1)
def check_server_settings(self, dt):
settings = check_server_settings()
if settings:
server_url = settings["server_url"]
username = settings["username"]
password = settings["password"]
token = settings["token"]
self.server_info_text = f"CHECKING server: {server_url}"
self.server_box_color = [0.984, 0.553, 0.078, 1]
self.ids.devices_spinner.text = "Loading devices..."
Clock.schedule_once(lambda dt: self.test_connection(server_url, username, password, token), 1)
else:
self.server_info_text = "Go to settings and set Traccar Server"
self.server_box_color = [0.909, 0.031, 0.243, 1]
self.ids.devices_spinner.text = "No devices available"
def set_result_message(self, message, color=(1, 1, 1, 1)):
self.ids.result_label.text = message
self.ids.result_label.color = color
def update_devices_spinner(self, devices):
if devices:
self.device_mapping = devices
device_names = list(devices.keys())
self.ids.devices_spinner.values = device_names
self.ids.devices_spinner.text = "Select a device"
else:
self.ids.devices_spinner.text = "No devices found"
self.ids.devices_spinner.values = []
def test_connection(self, server_url, username, password, token):
result = test_connection(server_url, username, password, token)
if result["status"]:
self.server_info_text = f"Connected to {server_url}"
self.server_box_color = [0.008, 0.525, 0.290, 1]
devices = get_devices_from_server()
self.update_devices_spinner(devices)
else:
self.server_info_text = result["message"]
self.server_box_color = [0.909, 0.031, 0.243, 1]
def on_device_selected(self, device_name):
if device_name != "Loading devices..." and device_name != "No devices found":
self.ids.devices_spinner.background_color = (0.008, 0.525, 0.290, 1)
else:
self.ids.devices_spinner.background_color = (1, 1, 1, 1)
def open_date_picker(self, which):
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.popup import Popup
import calendar
today = date.today()
selected = {"year": today.year, "month": today.month}
def update_grid():
grid.clear_widgets()
days_in_month = calendar.monthrange(selected["year"], selected["month"])[1]
for day in range(1, days_in_month + 1):
btn = Button(
text=str(day),
size_hint=(None, None),
size=(38, 38),
background_color=(0.341, 0.235, 0.980, 1),
color=(1, 1, 1, 1),
font_size=15
)
def on_date_selected(instance, day=day):
date_str = f"{selected['year']}-{selected['month']:02d}-{day:02d}"
if which == 'start':
self.ids.start_date_picker_button.text = date_str
else:
self.ids.end_date_picker_button.text = date_str
popup.dismiss()
btn.bind(on_press=on_date_selected)
grid.add_widget(btn)
def prev_month(instance):
if selected["month"] > 1:
selected["month"] -= 1
else:
selected["year"] -= 1
selected["month"] = 12
# Don't allow future months
if (selected["year"], selected["month"]) > (today.year, today.month):
selected["year"], selected["month"] = today.year, today.month
title_label.text = f"Select a Date ({selected['year']}-{selected['month']:02d})"
update_grid()
def next_month(instance):
# Only allow up to current month
if (selected["year"], selected["month"]) < (today.year, today.month):
if selected["month"] < 12:
selected["month"] += 1
else:
selected["year"] += 1
selected["month"] = 1
title_label.text = f"Select a Date ({selected['year']}-{selected['month']:02d})"
update_grid()
def on_cancel(instance):
popup.dismiss()
# Main vertical layout
main_layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
# Month navigation row
nav_layout = BoxLayout(orientation='horizontal', size_hint_y=None, height=40, spacing=10)
prev_btn = Button(text="<", size_hint_x=None, width=40, font_size=18, background_color=(0.341, 0.235, 0.980, 1), color=(1,1,1,1))
next_btn = Button(text=">", size_hint_x=None, width=40, font_size=18, background_color=(0.341, 0.235, 0.980, 1), color=(1,1,1,1))
title_label = Label(
text=f"Select a Date ({selected['year']}-{selected['month']:02d})",
size_hint_x=1,
font_size=18,
color=(1, 1, 1, 1)
)
prev_btn.bind(on_press=prev_month)
next_btn.bind(on_press=next_month)
nav_layout.add_widget(prev_btn)
nav_layout.add_widget(title_label)
nav_layout.add_widget(next_btn)
main_layout.add_widget(nav_layout)
# Grid of days: 6 columns
grid = GridLayout(cols=6, spacing=6, size_hint_y=None)
grid.bind(minimum_height=grid.setter('height'))
main_layout.add_widget(grid)
# Cancel button
cancel_btn = Button(
text="Cancel",
size_hint_y=None,
height=44,
background_color=(0.909, 0.031, 0.243, 1),
color=(1, 1, 1, 1),
font_size=16
)
cancel_btn.bind(on_press=on_cancel)
main_layout.add_widget(cancel_btn)
popup = Popup(
title="",
content=main_layout,
size_hint=(0.95, 0.8),
background_color=(0.11, 0.10, 0.15, 1),
separator_height=0,
auto_dismiss=False
)
update_grid()
popup.open()
def open_hour_picker(self, which):
from kivy.uix.popup import Popup
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.scrollview import ScrollView
from kivy.uix.gridlayout import GridLayout
popup_layout = BoxLayout(orientation='vertical', spacing=8, padding=8)
scroll = ScrollView(size_hint=(1, 1))
grid = GridLayout(cols=1, size_hint_y=None, spacing=4)
grid.bind(minimum_height=grid.setter('height'))
# Add hour buttons
for h in range(24):
hour_str = f"{h:02d}"
btn = Button(
text=hour_str,
size_hint_y=None,
height=44,
font_size=18,
background_color=(0.341, 0.235, 0.980, 1),
color=(1, 1, 1, 1)
)
def set_hour(instance, hour=hour_str):
if which == 'start':
self.ids.start_hour_button.text = hour
else:
self.ids.end_hour_button.text = hour
popup.dismiss()
btn.bind(on_press=set_hour)
grid.add_widget(btn)
scroll.add_widget(grid)
popup_layout.add_widget(scroll)
# Cancel button
cancel_btn = Button(
text="Cancel",
size_hint_y=None,
height=44,
background_color=(0.909, 0.031, 0.243, 1),
color=(1, 1, 1, 1),
font_size=16
)
cancel_btn.bind(on_press=lambda x: popup.dismiss())
popup_layout.add_widget(cancel_btn)
popup = Popup(
title="Select Hour",
content=popup_layout,
size_hint=(0.6, 0.7),
auto_dismiss=False
)
popup.open()
def update_points_count(self, count):
"""Update the label showing the number of points."""
self.ids.points_count_label.text = f"Points: {count}"
def save_route(self):
"""Save the current list of positions as a route in resources/projects/<route_name>/positions.json."""
route_name = self.ids.route_name_input.text.strip()
positions = getattr(self, "last_positions", None)
success, message, file_path = save_route_to_file(route_name, positions)
self.ids.result_label.text = message
if success:
# Reset UI fields
self.ids.devices_spinner.text = "Select a device"
self.ids.start_date_picker_button.text = "Select Start Date"
self.ids.end_date_picker_button.text = "Select End Date"
self.ids.start_hour_button.text = "00"
self.ids.end_hour_button.text = "23"
self.ids.points_count_label.text = "Points: 0"
self.ids.route_name_input.text = ""
# Show popup and schedule reroute to home
popup = Popup(title="Success",
content=Label(text=f"Route '{route_name}' saved!"),
size_hint=(None, None), size=(300, 150),
auto_dismiss=False)
popup.open()
def close_and_go_home(dt):
popup.dismiss()
self.manager.current = "home"
Clock.schedule_once(close_and_go_home, 3)
def get_trip_server_data(self):
"""Handle the Get trip server data button press."""
selected_device = self.ids.devices_spinner.text
start_date = self.ids.start_date_picker_button.text
end_date = self.ids.end_date_picker_button.text
if selected_device == "Loading devices..." or selected_device == "No devices found":
print("No valid device selected.")
self.ids.result_label.text = "Please select a valid device."
return
if start_date == "Select Date" or end_date == "Select Date":
print("No valid date selected.")
self.ids.result_label.text = "Please select valid start and end dates."
return
# Fetch trip data from the server
print(f"Fetching trip data for device: {selected_device} from {start_date} to {end_date}")
self.ids.result_label.text = f"Fetching trip data for {selected_device} from {start_date} to {end_date}..."
positions = self.fetch_positions_for_selected_day()
self.last_positions = positions # Store for saving
self.update_points_count(len(positions) if positions else 0)
if positions:
print("Positions received:")
for pos in positions:
print(f"{pos['deviceTime']}: {pos['latitude']}, {pos['longitude']}")
else:
print("No positions found or error occurred.")
def fetch_positions_for_selected_day(self):
settings = check_server_settings()
device_name = self.ids.devices_spinner.text
start_date = self.ids.start_date_picker_button.text
end_date = self.ids.end_date_picker_button.text
start_hour = self.ids.start_hour_button.text
end_hour = self.ids.end_hour_button.text
positions, error = fetch_positions_for_selected_day(
settings,
self.device_mapping,
device_name,
start_date,
end_date,
start_hour,
end_hour
)
if error:
self.ids.result_label.text = error
return []
self.ids.result_label.text = f"Retrieved {len(positions)} positions."
return positions
def fetch_devices_async(self):
Thread(target=self._fetch_devices_worker).start()
def _fetch_devices_worker(self):
devices = get_devices_from_server()
self.update_devices_spinner_mainthread(devices)
@mainthread
def update_devices_spinner_mainthread(self, devices):
self.update_devices_spinner(devices)
# trhis screen is used to create a new user
# register screen
class RegisterScreen(Screen):
def create_user(self):
"""Handle user creation."""
username = self.ids.set_username_input.text.strip()
password = self.ids.set_password_input.text.strip()
confirm_password = self.ids.confirm_password_input.text.strip()
email = self.ids.set_email_input.text.strip()
if not username or not password or not confirm_password or not email:
self.ids.result_label.text = "Please fill in all fields."
return
if password != confirm_password:
self.ids.result_label.text = "Passwords do not match."
return
# Check if the username or email already exists
if self.user_exists(username, email):
self.ids.result_label.text = "User or email already exists."
return
# Save user data (encrypted)
user_data = {
"username": username,
"password": password,
"email": email,
}
encrypted_data = encrypt_data(json.dumps(user_data))
try:
with open(CREDENTIALS_FILE, "ab") as file: # Append encrypted data
file.write(encrypted_data + b"\n") # Add a newline for separation
except Exception as e:
self.ids.result_label.text = f"Failed to save user: {str(e)}"
return
self.ids.result_label.text = "User created successfully!"
# Navigate back to the login screen
self.manager.current = "login"
def user_exists(self, username, email):
"""Check if a username or email already exists in the credentials.enc file."""
try:
with open(CREDENTIALS_FILE, "rb") as file:
for line in file:
decrypted_data = decrypt_data(line.strip())
user = json.loads(decrypted_data)
if user["username"] == username or user["email"] == email:
return True
except FileNotFoundError:
pass
except Exception as e:
self.ids.result_label.text = f"Error checking user: {str(e)}"
return False
class CreateAnimationScreen(Screen):
project_name = StringProperty("")
preview_html_path = StringProperty("") # Path to the HTML file for preview
preview_image_path = StringProperty("") # Add this line
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=(None, None), size=(350, 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()
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):
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
positions_path = os.path.join(project_folder, "positions.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
progress.value = 100
label.text = (
f"Entries removable at start: {start_remove}\n"
f"Entries removable at end: {end_remove}"
)
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):
new_positions = positions[start_remove:len(positions)-end_remove if end_remove > 0 else None]
with open(positions_path, "w") as f:
json.dump(new_positions, f, indent=2)
label.text = "File optimized and 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 process_preview(dt):
try:
import folium
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
positions_path = os.path.join(project_folder, "positions.json")
html_path = os.path.join(project_folder, "preview.html")
img_path = os.path.join(project_folder, "preview.png")
if not os.path.exists(positions_path):
label.text = "positions.json not found!"
progress.value = 100
return
with open(positions_path, "r") as f:
positions = json.load(f)
if not positions:
label.text = "No positions to preview."
progress.value = 100
return
coords = [(pos['latitude'], pos['longitude']) for pos in positions]
m = folium.Map(location=coords[0], zoom_start=14)
folium.PolyLine(coords, color="blue", weight=4.5, opacity=1).add_to(m)
folium.Marker(coords[0], tooltip="Start", icon=folium.Icon(color="green")).add_to(m)
folium.Marker(coords[-1], tooltip="End", icon=folium.Icon(color="red")).add_to(m)
m.save(html_path)
# Convert HTML to image
save_folium_map_as_image(html_path, img_path)
# Set the image path for Kivy Image widget
self.preview_image_path = img_path
label.text = "Preview ready!"
progress.value = 100
def close_popup(dt):
popup.dismiss()
Clock.schedule_once(close_popup, 1)
except Exception as e:
label.text = f"Error: {e}"
progress.value = 100
def close_popup(dt):
popup.dismiss()
Clock.schedule_once(close_popup, 2)
Clock.schedule_once(process_preview, 0.5)
def save_folium_map_as_image(html_path, img_path, width=800, height=600, delay=2):
chrome_options = Options()
chrome_options.add_argument("--headless")
chrome_options.add_argument(f"--window-size={width},{height}")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
driver = webdriver.Chrome(options=chrome_options)
try:
driver.get("file://" + os.path.abspath(html_path))
time.sleep(delay) # Wait for map to render
screenshot_path = img_path + ".tmp.png"
driver.save_screenshot(screenshot_path)
driver.quit()
img = Image.open(screenshot_path)
img = img.crop((0, 0, width, height))
img.save(img_path)
os.remove(screenshot_path)
except Exception as e:
print(f"Error saving folium map as image: {e}")
driver.quit()
class HomeScreen(Screen):
def on_pre_enter(self):
"""Load existing projects/trips when the screen is entered."""
self.load_existing_projects()
def load_existing_projects(self):
projects_folder = os.path.join(RESOURCES_FOLDER, "projects")
archive_folder = os.path.join(RESOURCES_FOLDER, "trip_archive")
if not os.path.exists(projects_folder):
os.makedirs(projects_folder)
if not os.path.exists(archive_folder):
os.makedirs(archive_folder)
self.ids.projects_list.clear_widgets()
for project in os.listdir(projects_folder):
row = BoxLayout(
orientation="horizontal",
size_hint_y=None,
height=44,
spacing=6,
padding=(8, 6)
)
from kivy.graphics import Color, Line, RoundedRectangle
with row.canvas.before:
Color(0.11, 0.10, 0.15, 1)
row.bg_rect = RoundedRectangle(pos=row.pos, size=row.size, radius=[6])
Color(0.341, 0.235, 0.980, 1)
row.border_line = Line(rounded_rectangle=[row.x, row.y, row.width, row.height, 6], width=1)
# Use a closure to bind the correct row instance
def make_update_bg_rect(r):
def update_bg_rect(instance, value):
r.bg_rect.pos = r.pos
r.bg_rect.size = r.size
r.border_line.rounded_rectangle = [r.x, r.y, r.width, r.height, 6]
return update_bg_rect
row.bind(pos=make_update_bg_rect(row), size=make_update_bg_rect(row))
project_label = Label(
text=project,
size_hint_x=0.64,
color=(1, 1, 1, 1),
font_size=15,
shorten=True,
shorten_from='right'
)
edit_button = IconButton(
source="resources/images/edit.png",
size_hint_x=0.18,
allow_stretch=True,
keep_ratio=True
)
edit_button.bind(on_press=lambda instance, p=project: self.edit_project(p))
delete_button = IconButton(
source="resources/images/delete.png",
size_hint_x=0.18,
allow_stretch=True,
keep_ratio=True
)
delete_button.bind(on_press=lambda instance, p=project: self.confirm_delete_project(p))
row.add_widget(project_label)
row.add_widget(edit_button)
row.add_widget(delete_button)
self.ids.projects_list.add_widget(row)
def open_project(self, project_name):
"""Handle opening an existing project/trip."""
print(f"Opening project: {project_name}")
self.ids.result_label.text = f"Opened project: {project_name}"
def create_new_project(self):
"""Navigate to the GetTripFromServer screen to create a new project/trip."""
self.manager.current = "get_trip_from_server"
def edit_project(self, project_name):
# Set the project name on the CreateAnimationScreen before switching
create_anim_screen = self.manager.get_screen("create_animation")
create_anim_screen.project_name = project_name
self.manager.current = "create_animation"
# delete or archive project
def confirm_delete_project(self, project_name):
from kivy.uix.popup import Popup
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text=f"Delete project '{project_name}'?\nChoose an option:")
btn_delete = Button(text="Delete Completely", background_color=(1, 0, 0, 1))
btn_archive = Button(text="Archive Trip", background_color=(0.341, 0.235, 0.980, 1))
btn_cancel = Button(text="Cancel")
popup = Popup(title="Delete Project", content=layout, size_hint=(None, None), size=(400, 250), auto_dismiss=False)
layout.add_widget(label)
layout.add_widget(btn_delete)
layout.add_widget(btn_archive)
layout.add_widget(btn_cancel)
def do_delete(instance):
self.delete_project(project_name)
popup.dismiss()
self.load_existing_projects()
def do_archive(instance):
self.archive_project(project_name)
popup.dismiss()
self.load_existing_projects()
btn_delete.bind(on_press=do_delete)
btn_archive.bind(on_press=do_archive)
btn_cancel.bind(on_press=lambda x: popup.dismiss())
popup.open()
def delete_project(self, project_name):
import shutil
folder_path = os.path.join(RESOURCES_FOLDER, "projects", project_name)
if os.path.exists(folder_path):
shutil.rmtree(folder_path)
def archive_project(self, project_name):
import shutil
src_file = os.path.join(RESOURCES_FOLDER, "projects", project_name, "positions.json")
archive_folder = os.path.join(RESOURCES_FOLDER, "trip_archive")
if not os.path.exists(archive_folder):
os.makedirs(archive_folder)
dst_file = os.path.join(archive_folder, f"{project_name}.json")
if os.path.exists(src_file):
shutil.copy2(src_file, dst_file)
# Optionally, delete the project folder after archiving
self.delete_project(project_name)
class IconButton(ButtonBehavior, Image):
pass
class TraccarApp(App):
def build(self):
if not os.path.exists(RESOURCES_FOLDER):

46
proba Normal file
View File

@@ -0,0 +1,46 @@
class CreateAnimationScreen(Screen):
project_name = StringProperty("")
preview_html_path = StringProperty("") # Path to the HTML file for preview
preview_image_path = StringProperty("") # Add this line
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=(None, None), size=(350, 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()

View File

@@ -1 +1 @@
gAAAAABoQqkmrT4fO0Hnm7LjP1_bgBWYGNczjbqwAkW0lO0lS-xro9UspMOcbFu2BpLv_nQD8KT_dNPLdcnrymYeAPCMjUOV9-tXKMefrdbto26cu9gIv2mYXaGIODI7zM6TwPmkHJRu
gAAAAABoQuJn-THhBcB9uQut4cng4vNqljWnzVOe-jvl4j8_nDzq1KiWNF5G2BKJCxy-u2Lf72PE9WMHOA7n2EMYsLzwmF0mi_2me3DnrckEE4kaC4reSowP0AiiKNdYqrZVFcemUf7w

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,169 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.3/dist/leaflet.js"></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.3/dist/leaflet.css"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css"/>
<link rel="stylesheet" href="https://netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-glyphicons.css"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.2.0/css/all.min.css"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.css"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/python-visualization/folium/folium/templates/leaflet.awesome.rotate.min.css"/>
<meta name="viewport" content="width=device-width,
initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<style>
#map_bf67b2d92a9afef5449c1b1b9845da94 {
position: relative;
width: 100.0%;
height: 100.0%;
left: 0.0%;
top: 0.0%;
}
.leaflet-container { font-size: 1rem; }
</style>
<style>html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
</style>
<style>#map {
position:absolute;
top:0;
bottom:0;
right:0;
left:0;
}
</style>
<script>
L_NO_TOUCH = false;
L_DISABLE_3D = false;
</script>
</head>
<body>
<div class="folium-map" id="map_bf67b2d92a9afef5449c1b1b9845da94" ></div>
</body>
<script>
var map_bf67b2d92a9afef5449c1b1b9845da94 = L.map(
"map_bf67b2d92a9afef5449c1b1b9845da94",
{
center: [45.805146666666666, 24.126355555555556],
crs: L.CRS.EPSG3857,
...{
"zoom": 14,
"zoomControl": true,
"preferCanvas": false,
}
}
);
var tile_layer_48f00dde609689cd95b3e5b1020d2d03 = L.tileLayer(
"https://tile.openstreetmap.org/{z}/{x}/{y}.png",
{
"minZoom": 0,
"maxZoom": 19,
"maxNativeZoom": 19,
"noWrap": false,
"attribution": "\u0026copy; \u003ca href=\"https://www.openstreetmap.org/copyright\"\u003eOpenStreetMap\u003c/a\u003e contributors",
"subdomains": "abc",
"detectRetina": false,
"tms": false,
"opacity": 1,
}
);
tile_layer_48f00dde609689cd95b3e5b1020d2d03.addTo(map_bf67b2d92a9afef5449c1b1b9845da94);
var poly_line_ac2b74b1096aa06a1d4ab84860beacdc = L.polyline(
[[45.805146666666666, 24.126355555555556], [45.80562444444445, 24.123990555555554], [45.805820555555556, 24.122884444444445], [45.806001111111115, 24.121864444444444], [45.80658944444445, 24.118647777777777], [45.80706166666667, 24.11584], [45.80744277777778, 24.113130555555554], [45.80744444444444, 24.111027777777778], [45.807554999999994, 24.10904111111111], [45.80765388888889, 24.10791777777778], [45.80775722222222, 24.106204444444444], [45.80775722222222, 24.106204444444444], [45.807792777777784, 24.10529888888889], [45.80769222222222, 24.105220555555558], [45.807494444444444, 24.10537666666667], [45.80721722222222, 24.10552888888889], [45.80721722222222, 24.10552888888889], [45.80452833333334, 24.106312222222222], [45.80452833333334, 24.106312222222222], [45.802245000000006, 24.106793888888888], [45.802245000000006, 24.106793888888888], [45.80039166666667, 24.107621666666667], [45.80039166666667, 24.107621666666667], [45.79863111111111, 24.10826], [45.79706388888889, 24.109215], [45.796372222222224, 24.109560000000002], [45.79611444444444, 24.109526666666667], [45.79596611111111, 24.109244999999998], [45.79575722222222, 24.107441666666666], [45.79575722222222, 24.107441666666666], [45.79544, 24.105129444444444], [45.79544, 24.105129444444444], [45.795164444444445, 24.103232777777777], [45.794825555555555, 24.100786111111113], [45.79484444444444, 24.10045277777778], [45.79482, 24.100100555555557], [45.79452388888888, 24.098648333333333], [45.794362222222226, 24.097596666666668], [45.794362222222226, 24.097596666666668], [45.794362222222226, 24.097596666666668], [45.79418555555556, 24.09649111111111], [45.79419388888889, 24.096272777777777], [45.79433111111111, 24.095743333333335], [45.795445, 24.094136111111112], [45.796870000000006, 24.09261777777778], [45.797534444444445, 24.091910555555554], [45.79878277777778, 24.090588888888888], [45.79978833333333, 24.089429444444445], [45.799776111111115, 24.089080555555554], [45.79944055555555, 24.086607777777775], [45.79913277777778, 24.086008333333332], [45.79909722222222, 24.08582277777778], [45.79911555555555, 24.085697222222223], [45.79911555555555, 24.085697222222223], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79919388888889, 24.08558888888889], [45.79921, 24.085612222222224]],
{"bubblingMouseEvents": true, "color": "blue", "dashArray": null, "dashOffset": null, "fill": false, "fillColor": "blue", "fillOpacity": 0.2, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "noClip": false, "opacity": 1, "smoothFactor": 1.0, "stroke": true, "weight": 4.5}
).addTo(map_bf67b2d92a9afef5449c1b1b9845da94);
var marker_508cb899cfad4984ec8c6bacbc7d4450 = L.marker(
[45.805146666666666, 24.126355555555556],
{
}
).addTo(map_bf67b2d92a9afef5449c1b1b9845da94);
var icon_27f251d0f4b490eac1364fdc7c0e4bcb = L.AwesomeMarkers.icon(
{
"markerColor": "green",
"iconColor": "white",
"icon": "info-sign",
"prefix": "glyphicon",
"extraClasses": "fa-rotate-0",
}
);
marker_508cb899cfad4984ec8c6bacbc7d4450.bindTooltip(
`<div>
Start
</div>`,
{
"sticky": true,
}
);
marker_508cb899cfad4984ec8c6bacbc7d4450.setIcon(icon_27f251d0f4b490eac1364fdc7c0e4bcb);
var marker_fdc9ee2260616a462ff09a9869579e98 = L.marker(
[45.79921, 24.085612222222224],
{
}
).addTo(map_bf67b2d92a9afef5449c1b1b9845da94);
var icon_5ab5f7e75df70e10f7a0380fd99ababf = L.AwesomeMarkers.icon(
{
"markerColor": "red",
"iconColor": "white",
"icon": "info-sign",
"prefix": "glyphicon",
"extraClasses": "fa-rotate-0",
}
);
marker_fdc9ee2260616a462ff09a9869579e98.bindTooltip(
`<div>
End
</div>`,
{
"sticky": true,
}
);
marker_fdc9ee2260616a462ff09a9869579e98.setIcon(icon_5ab5f7e75df70e10f7a0380fd99ababf);
</script>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 KiB

0
screens/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,264 @@
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, ListProperty, 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
)
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
from utils import html_to_image
class CreateAnimationScreen(Screen):
project_name = StringProperty("")
preview_html_path = StringProperty("") # Path to the HTML file for preview
preview_image_path = StringProperty("") # 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):
# Add a dummy query string to force reload
return f"{img_path}?{int(time.time())}"
return "resources/images/track.png"
preview_image_source = AliasProperty(get_preview_image_source, None, bind=['project_name'])
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):
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
positions_path = os.path.join(project_folder, "positions.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
progress.value = 100
label.text = (
f"Entries removable at start: {start_remove}\n"
f"Entries removable at end: {end_remove}"
)
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):
new_positions = positions[start_remove:len(positions)-end_remove if end_remove > 0 else None]
with open(positions_path, "w") as f:
json.dump(new_positions, f, indent=2)
label.text = "File optimized and 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 process_preview(dt):
try:
import folium
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
positions_path = os.path.join(project_folder, "positions.json")
html_path = os.path.join(project_folder, "preview.html")
img_path = os.path.join(project_folder, "preview.png")
if not os.path.exists(positions_path):
label.text = "positions.json not found!"
progress.value = 100
return
with open(positions_path, "r") as f:
positions = json.load(f)
if not positions:
label.text = "No positions to preview."
progress.value = 100
return
coords = [(pos['latitude'], pos['longitude']) for pos in positions]
m = folium.Map(location=coords[0], zoom_start=14)
folium.PolyLine(coords, color="blue", weight=4.5, opacity=1).add_to(m)
folium.Marker(coords[0], tooltip="Start", icon=folium.Icon(color="green")).add_to(m)
folium.Marker(coords[-1], tooltip="End", icon=folium.Icon(color="red")).add_to(m)
m.save(html_path)
# Convert HTML to image
html_to_image(html_path, img_path)
self.property('preview_image_source').dispatch(self)
# Set the image path for Kivy Image widget
self.preview_image_path = img_path
label.text = "Preview ready!"
progress.value = 100
def close_popup(dt):
popup.dismiss()
Clock.schedule_once(close_popup, 1)
except Exception as e:
label.text = f"Error: {e}"
progress.value = 100
def close_popup(dt):
popup.dismiss()
Clock.schedule_once(close_popup, 2)
Clock.schedule_once(process_preview, 0.5)

View File

@@ -0,0 +1,347 @@
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, ListProperty
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
)
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
import webbrowser
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from PIL import Image
import time
from config import RESOURCES_FOLDER, CREDENTIALS_FILE
class GetTripFromServer(Screen):
server_info_text = StringProperty("LOADING DATA...")
server_box_color = ListProperty([0.984, 0.553, 0.078, 1])
device_mapping = {}
def on_pre_enter(self):
self.server_box_color = [0.984, 0.553, 0.078, 1]
self.server_info_text = "LOADING DATA..."
Clock.schedule_once(self.check_server_settings, 1)
def check_server_settings(self, dt):
settings = check_server_settings()
if settings:
server_url = settings["server_url"]
username = settings["username"]
password = settings["password"]
token = settings["token"]
self.server_info_text = f"CHECKING server: {server_url}"
self.server_box_color = [0.984, 0.553, 0.078, 1]
self.ids.devices_spinner.text = "Loading devices..."
Clock.schedule_once(lambda dt: self.test_connection(server_url, username, password, token), 1)
else:
self.server_info_text = "Go to settings and set Traccar Server"
self.server_box_color = [0.909, 0.031, 0.243, 1]
self.ids.devices_spinner.text = "No devices available"
def set_result_message(self, message, color=(1, 1, 1, 1)):
self.ids.result_label.text = message
self.ids.result_label.color = color
def update_devices_spinner(self, devices):
if devices:
self.device_mapping = devices
device_names = list(devices.keys())
self.ids.devices_spinner.values = device_names
self.ids.devices_spinner.text = "Select a device"
else:
self.ids.devices_spinner.text = "No devices found"
self.ids.devices_spinner.values = []
def test_connection(self, server_url, username, password, token):
result = test_connection(server_url, username, password, token)
if result["status"]:
self.server_info_text = f"Connected to {server_url}"
self.server_box_color = [0.008, 0.525, 0.290, 1]
devices = get_devices_from_server()
self.update_devices_spinner(devices)
else:
self.server_info_text = result["message"]
self.server_box_color = [0.909, 0.031, 0.243, 1]
def on_device_selected(self, device_name):
if device_name != "Loading devices..." and device_name != "No devices found":
self.ids.devices_spinner.background_color = (0.008, 0.525, 0.290, 1)
else:
self.ids.devices_spinner.background_color = (1, 1, 1, 1)
def open_date_picker(self, which):
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.popup import Popup
import calendar
today = date.today()
selected = {"year": today.year, "month": today.month}
def update_grid():
grid.clear_widgets()
days_in_month = calendar.monthrange(selected["year"], selected["month"])[1]
for day in range(1, days_in_month + 1):
btn = Button(
text=str(day),
size_hint=(None, None),
size=(38, 38),
background_color=(0.341, 0.235, 0.980, 1),
color=(1, 1, 1, 1),
font_size=15
)
def on_date_selected(instance, day=day):
date_str = f"{selected['year']}-{selected['month']:02d}-{day:02d}"
if which == 'start':
self.ids.start_date_picker_button.text = date_str
else:
self.ids.end_date_picker_button.text = date_str
popup.dismiss()
btn.bind(on_press=on_date_selected)
grid.add_widget(btn)
def prev_month(instance):
if selected["month"] > 1:
selected["month"] -= 1
else:
selected["year"] -= 1
selected["month"] = 12
# Don't allow future months
if (selected["year"], selected["month"]) > (today.year, today.month):
selected["year"], selected["month"] = today.year, today.month
title_label.text = f"Select a Date ({selected['year']}-{selected['month']:02d})"
update_grid()
def next_month(instance):
# Only allow up to current month
if (selected["year"], selected["month"]) < (today.year, today.month):
if selected["month"] < 12:
selected["month"] += 1
else:
selected["year"] += 1
selected["month"] = 1
title_label.text = f"Select a Date ({selected['year']}-{selected['month']:02d})"
update_grid()
def on_cancel(instance):
popup.dismiss()
# Main vertical layout
main_layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
# Month navigation row
nav_layout = BoxLayout(orientation='horizontal', size_hint_y=None, height=40, spacing=10)
prev_btn = Button(text="<", size_hint_x=None, width=40, font_size=18, background_color=(0.341, 0.235, 0.980, 1), color=(1,1,1,1))
next_btn = Button(text=">", size_hint_x=None, width=40, font_size=18, background_color=(0.341, 0.235, 0.980, 1), color=(1,1,1,1))
title_label = Label(
text=f"Select a Date ({selected['year']}-{selected['month']:02d})",
size_hint_x=1,
font_size=18,
color=(1, 1, 1, 1)
)
prev_btn.bind(on_press=prev_month)
next_btn.bind(on_press=next_month)
nav_layout.add_widget(prev_btn)
nav_layout.add_widget(title_label)
nav_layout.add_widget(next_btn)
main_layout.add_widget(nav_layout)
# Grid of days: 6 columns
grid = GridLayout(cols=6, spacing=6, size_hint_y=None)
grid.bind(minimum_height=grid.setter('height'))
main_layout.add_widget(grid)
# Cancel button
cancel_btn = Button(
text="Cancel",
size_hint_y=None,
height=44,
background_color=(0.909, 0.031, 0.243, 1),
color=(1, 1, 1, 1),
font_size=16
)
cancel_btn.bind(on_press=on_cancel)
main_layout.add_widget(cancel_btn)
popup = Popup(
title="",
content=main_layout,
size_hint=(0.95, 0.8),
background_color=(0.11, 0.10, 0.15, 1),
separator_height=0,
auto_dismiss=False
)
update_grid()
popup.open()
def open_hour_picker(self, which):
from kivy.uix.popup import Popup
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.scrollview import ScrollView
from kivy.uix.gridlayout import GridLayout
popup_layout = BoxLayout(orientation='vertical', spacing=8, padding=8)
scroll = ScrollView(size_hint=(1, 1))
grid = GridLayout(cols=1, size_hint_y=None, spacing=4)
grid.bind(minimum_height=grid.setter('height'))
# Add hour buttons
for h in range(24):
hour_str = f"{h:02d}"
btn = Button(
text=hour_str,
size_hint_y=None,
height=44,
font_size=18,
background_color=(0.341, 0.235, 0.980, 1),
color=(1, 1, 1, 1)
)
def set_hour(instance, hour=hour_str):
if which == 'start':
self.ids.start_hour_button.text = hour
else:
self.ids.end_hour_button.text = hour
popup.dismiss()
btn.bind(on_press=set_hour)
grid.add_widget(btn)
scroll.add_widget(grid)
popup_layout.add_widget(scroll)
# Cancel button
cancel_btn = Button(
text="Cancel",
size_hint_y=None,
height=44,
background_color=(0.909, 0.031, 0.243, 1),
color=(1, 1, 1, 1),
font_size=16
)
cancel_btn.bind(on_press=lambda x: popup.dismiss())
popup_layout.add_widget(cancel_btn)
popup = Popup(
title="Select Hour",
content=popup_layout,
size_hint=(0.6, 0.7),
auto_dismiss=False
)
popup.open()
def update_points_count(self, count):
"""Update the label showing the number of points."""
self.ids.points_count_label.text = f"Points: {count}"
def save_route(self):
"""Save the current list of positions as a route in resources/projects/<route_name>/positions.json."""
route_name = self.ids.route_name_input.text.strip()
positions = getattr(self, "last_positions", None)
success, message, file_path = save_route_to_file(route_name, positions)
self.ids.result_label.text = message
if success:
# Reset UI fields
self.ids.devices_spinner.text = "Select a device"
self.ids.start_date_picker_button.text = "Select Start Date"
self.ids.end_date_picker_button.text = "Select End Date"
self.ids.start_hour_button.text = "00"
self.ids.end_hour_button.text = "23"
self.ids.points_count_label.text = "Points: 0"
self.ids.route_name_input.text = ""
# Show popup and schedule reroute to home
popup = Popup(title="Success",
content=Label(text=f"Route '{route_name}' saved!"),
size_hint=(None, None), size=(300, 150),
auto_dismiss=False)
popup.open()
def close_and_go_home(dt):
popup.dismiss()
self.manager.current = "home"
Clock.schedule_once(close_and_go_home, 3)
def get_trip_server_data(self):
"""Handle the Get trip server data button press."""
selected_device = self.ids.devices_spinner.text
start_date = self.ids.start_date_picker_button.text
end_date = self.ids.end_date_picker_button.text
if selected_device == "Loading devices..." or selected_device == "No devices found":
print("No valid device selected.")
self.ids.result_label.text = "Please select a valid device."
return
if start_date == "Select Date" or end_date == "Select Date":
print("No valid date selected.")
self.ids.result_label.text = "Please select valid start and end dates."
return
# Fetch trip data from the server
print(f"Fetching trip data for device: {selected_device} from {start_date} to {end_date}")
self.ids.result_label.text = f"Fetching trip data for {selected_device} from {start_date} to {end_date}..."
positions = self.fetch_positions_for_selected_day()
self.last_positions = positions # Store for saving
self.update_points_count(len(positions) if positions else 0)
if positions:
print("Positions received:")
for pos in positions:
print(f"{pos['deviceTime']}: {pos['latitude']}, {pos['longitude']}")
else:
print("No positions found or error occurred.")
def fetch_positions_for_selected_day(self):
settings = check_server_settings()
device_name = self.ids.devices_spinner.text
start_date = self.ids.start_date_picker_button.text
end_date = self.ids.end_date_picker_button.text
start_hour = self.ids.start_hour_button.text
end_hour = self.ids.end_hour_button.text
positions, error = fetch_positions_for_selected_day(
settings,
self.device_mapping,
device_name,
start_date,
end_date,
start_hour,
end_hour
)
if error:
self.ids.result_label.text = error
return []
self.ids.result_label.text = f"Retrieved {len(positions)} positions."
return positions
def fetch_devices_async(self):
Thread(target=self._fetch_devices_worker).start()
def _fetch_devices_worker(self):
devices = get_devices_from_server()
self.update_devices_spinner_mainthread(devices)
@mainthread
def update_devices_spinner_mainthread(self, devices):
self.update_devices_spinner(devices)

152
screens/home_screen.py Normal file
View File

@@ -0,0 +1,152 @@
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.popup import Popup
from kivy.uix.image import Image
from kivy.uix.behaviors import ButtonBehavior
from kivy.clock import Clock
import os
import json
from config import RESOURCES_FOLDER, CREDENTIALS_FILE
from utils import encrypt_data, decrypt_data, check_server_settings, save_server_settings, test_connection
class IconButton(ButtonBehavior, Image):
pass
class HomeScreen(Screen):
def on_pre_enter(self):
"""Load existing projects/trips when the screen is entered."""
self.load_existing_projects()
def load_existing_projects(self):
projects_folder = os.path.join(RESOURCES_FOLDER, "projects")
archive_folder = os.path.join(RESOURCES_FOLDER, "trip_archive")
if not os.path.exists(projects_folder):
os.makedirs(projects_folder)
if not os.path.exists(archive_folder):
os.makedirs(archive_folder)
self.ids.projects_list.clear_widgets()
for project in os.listdir(projects_folder):
row = BoxLayout(
orientation="horizontal",
size_hint_y=None,
height=44,
spacing=6,
padding=(8, 6)
)
from kivy.graphics import Color, Line, RoundedRectangle
with row.canvas.before:
Color(0.11, 0.10, 0.15, 1)
row.bg_rect = RoundedRectangle(pos=row.pos, size=row.size, radius=[6])
Color(0.341, 0.235, 0.980, 1)
row.border_line = Line(rounded_rectangle=[row.x, row.y, row.width, row.height, 6], width=1)
# Use a closure to bind the correct row instance
def make_update_bg_rect(r):
def update_bg_rect(instance, value):
r.bg_rect.pos = r.pos
r.bg_rect.size = r.size
r.border_line.rounded_rectangle = [r.x, r.y, r.width, r.height, 6]
return update_bg_rect
row.bind(pos=make_update_bg_rect(row), size=make_update_bg_rect(row))
project_label = Label(
text=project,
size_hint_x=0.64,
color=(1, 1, 1, 1),
font_size=15,
shorten=True,
shorten_from='right'
)
edit_button = IconButton(
source="resources/images/edit.png",
size_hint_x=0.18,
allow_stretch=True,
keep_ratio=True
)
edit_button.bind(on_press=lambda instance, p=project: self.edit_project(p))
delete_button = IconButton(
source="resources/images/delete.png",
size_hint_x=0.18,
allow_stretch=True,
keep_ratio=True
)
delete_button.bind(on_press=lambda instance, p=project: self.confirm_delete_project(p))
row.add_widget(project_label)
row.add_widget(edit_button)
row.add_widget(delete_button)
self.ids.projects_list.add_widget(row)
def open_project(self, project_name):
"""Handle opening an existing project/trip."""
print(f"Opening project: {project_name}")
self.ids.result_label.text = f"Opened project: {project_name}"
def create_new_project(self):
"""Navigate to the GetTripFromServer screen to create a new project/trip."""
self.manager.current = "get_trip_from_server"
def edit_project(self, project_name):
# Set the project name on the CreateAnimationScreen before switching
create_anim_screen = self.manager.get_screen("create_animation")
create_anim_screen.project_name = project_name
self.manager.current = "create_animation"
# delete or archive project
def confirm_delete_project(self, project_name):
from kivy.uix.popup import Popup
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
label = Label(text=f"Delete project '{project_name}'?\nChoose an option:")
btn_delete = Button(text="Delete Completely", background_color=(1, 0, 0, 1))
btn_archive = Button(text="Archive Trip", background_color=(0.341, 0.235, 0.980, 1))
btn_cancel = Button(text="Cancel")
popup = Popup(title="Delete Project", content=layout, size_hint=(None, None), size=(400, 250), auto_dismiss=False)
layout.add_widget(label)
layout.add_widget(btn_delete)
layout.add_widget(btn_archive)
layout.add_widget(btn_cancel)
def do_delete(instance):
self.delete_project(project_name)
popup.dismiss()
self.load_existing_projects()
def do_archive(instance):
self.archive_project(project_name)
popup.dismiss()
self.load_existing_projects()
btn_delete.bind(on_press=do_delete)
btn_archive.bind(on_press=do_archive)
btn_cancel.bind(on_press=lambda x: popup.dismiss())
popup.open()
def delete_project(self, project_name):
import shutil
folder_path = os.path.join(RESOURCES_FOLDER, "projects", project_name)
if os.path.exists(folder_path):
shutil.rmtree(folder_path)
def archive_project(self, project_name):
import shutil
src_file = os.path.join(RESOURCES_FOLDER, "projects", project_name, "positions.json")
archive_folder = os.path.join(RESOURCES_FOLDER, "trip_archive")
if not os.path.exists(archive_folder):
os.makedirs(archive_folder)
dst_file = os.path.join(archive_folder, f"{project_name}.json")
if os.path.exists(src_file):
shutil.copy2(src_file, dst_file)
# Optionally, delete the project folder after archiving
self.delete_project(project_name)

29
screens/login_screen.py Normal file
View File

@@ -0,0 +1,29 @@
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.popup import Popup
from kivy.uix.image import Image
from kivy.uix.behaviors import ButtonBehavior
from kivy.clock import Clock
import os
import json
from config import RESOURCES_FOLDER, CREDENTIALS_FILE
from utils import encrypt_data, decrypt_data, check_server_settings, save_server_settings, test_connection
class LoginScreen(Screen):
def login(self):
username = self.ids.username_input.text.strip()
password = self.ids.username_input.text.strip()
if not username or not password:
self.manager.get_screen("home").ids.result_label.text = "Please fill in all fields."
return
credentials = {"username": username, "password": password}
encrypted_data = encrypt_data(json.dumps(credentials))
with open(CREDENTIALS_FILE, "wb") as file:
file.write(encrypted_data)
self.manager.current = "home"

134
screens/settings_screen.py Normal file
View File

@@ -0,0 +1,134 @@
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, ListProperty
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
)
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
import webbrowser
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from PIL import Image
import time
from config import RESOURCES_FOLDER, CREDENTIALS_FILE
class SettingsScreen(Screen):
server_response = "Waiting to test connection..."
def on_pre_enter(self):
settings = check_server_settings()
if settings:
self.ids.server_url_input.text = settings.get("server_url", "")
self.ids.username_input.text = settings.get("username", "")
self.ids.password_input.text = settings.get("password", "")
self.ids.token_input.text = settings.get("token", "")
else:
self.ids.server_url_input.text = ""
self.ids.username_input.text = ""
self.ids.password_input.text = ""
self.ids.token_input.text = ""
def test_connection(self):
server_url = self.ids.server_url_input.text.strip()
username = self.ids.username_input.text.strip()
password = self.ids.password_input.text.strip()
token = self.ids.token_input.text.strip()
result = test_connection(server_url, username, password, token)
self.server_response = result["message"]
self.ids.result_label.text = self.server_response
def save_settings(self):
server_url = self.ids.server_url_input.text.strip()
username = self.ids.username_input.text.strip()
password = self.ids.password_input.text.strip()
token = self.ids.token_input.text.strip()
if not server_url or not username or not password or not token:
self.ids.result_label.text = "Please fill in all fields."
return
settings_data = {
"server_url": server_url,
"username": username,
"password": password,
"token": token,
}
try:
save_server_settings(settings_data)
self.ids.result_label.text = "Settings saved successfully!"
self.manager.current = "home"
except Exception as e:
self.ids.result_label.text = f"Failed to save settings: {str(e)}"
# get trip from server screen
# trhis screen is used to create a new user
# register screen
class RegisterScreen(Screen):
def create_user(self):
"""Handle user creation."""
username = self.ids.set_username_input.text.strip()
password = self.ids.set_password_input.text.strip()
confirm_password = self.ids.confirm_password_input.text.strip()
email = self.ids.set_email_input.text.strip()
if not username or not password or not confirm_password or not email:
self.ids.result_label.text = "Please fill in all fields."
return
if password != confirm_password:
self.ids.result_label.text = "Passwords do not match."
return
# Check if the username or email already exists
if self.user_exists(username, email):
self.ids.result_label.text = "User or email already exists."
return
# Save user data (encrypted)
user_data = {
"username": username,
"password": password,
"email": email,
}
encrypted_data = encrypt_data(json.dumps(user_data))
try:
with open(CREDENTIALS_FILE, "ab") as file: # Append encrypted data
file.write(encrypted_data + b"\n") # Add a newline for separation
except Exception as e:
self.ids.result_label.text = f"Failed to save user: {str(e)}"
return
self.ids.result_label.text = "User created successfully!"
# Navigate back to the login screen
self.manager.current = "login"
def user_exists(self, username, email):
"""Check if a username or email already exists in the credentials.enc file."""
try:
with open(CREDENTIALS_FILE, "rb") as file:
for line in file:
decrypted_data = decrypt_data(line.strip())
user = json.loads(decrypted_data)
if user["username"] == username or user["email"] == email:
return True
except FileNotFoundError:
pass
except Exception as e:
self.ids.result_label.text = f"Error checking user: {str(e)}"
return False

View File

@@ -643,11 +643,11 @@
text_size: self.size
on_press: root.optimize_route_entries()
# Preview frame
# Preview frame (label + button on first row, image on second row)
BoxLayout:
orientation: "horizontal"
orientation: "vertical"
size_hint_y: None
height: 60
height: 300 # Adjust as needed for your image size
padding: [10, 10, 10, 10]
spacing: 10
canvas.before:
@@ -657,22 +657,37 @@
pos: self.pos
size: self.size
Label:
text: "Preview your route"
font_size: 16
color: 1, 1, 1, 1
size_hint_x: 0.7
halign: "left"
valign: "middle"
text_size: self.size
BoxLayout:
orientation: "horizontal"
size_hint_y: None
height: 60
spacing: 10
Button:
text: "Preview"
size_hint_x: 0.3
font_size: 16
background_color: 0.341, 0.235, 0.980, 1
color: 1, 1, 1, 1
on_press: root.preview_route()
Label:
text: "Preview your route"
font_size: 16
color: 1, 1, 1, 1
size_hint_x: 0.7
halign: "left"
valign: "middle"
text_size: self.size
Button:
text: "Preview"
size_hint_x: 0.3
font_size: 16
background_color: 0.341, 0.235, 0.980, 1
color: 1, 1, 1, 1
on_press: root.preview_route()
Image:
id: preview_image
source: root.preview_image_source
allow_stretch: True
keep_ratio: False
size_hint_y: None
height: 220
size_hint_x: 1
Widget:
@@ -685,12 +700,4 @@
background_color: 0.341, 0.235, 0.980, 1
color: 1, 1, 1, 1
font_size: 16
on_press: app.root.current = "home"
Image:
id: preview_image
source: root.preview_image_path if root.preview_image_path else "resources/images/track.png"
allow_stretch: True
keep_ratio: True
size_hint_y: None
height: 220
on_press: app.root.current = "home"

View File

@@ -166,3 +166,46 @@ def fetch_positions_for_selected_day(settings, device_mapping, device_name, star
if error:
return [], error
return positions, None
def html_to_image(html_path, img_path, width=800, height=600, delay=2, driver_path='/usr/bin/chromedriver'):
"""
Convert an HTML file to an image using Selenium and Pillow.
Args:
html_path (str): Path to the HTML file.
img_path (str): Path to save the output image (PNG).
width (int): Width of the browser window.
height (int): Height of the browser window.
delay (int): Seconds to wait for the page to render.
driver_path (str): Path to chromedriver binary.
"""
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
chrome_options = Options()
chrome_options.add_argument("--headless")
chrome_options.add_argument(f"--window-size={width},{height}")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
service = Service(driver_path)
driver = webdriver.Chrome(service=service, options=chrome_options)
try:
driver.get("file://" + os.path.abspath(html_path))
time.sleep(delay) # Wait for the page to render
tmp_img = img_path + ".tmp.png"
driver.save_screenshot(tmp_img)
driver.quit()
img = Image.open(tmp_img)
img = img.crop((0, 0, width, height))
img.save(img_path)
os.remove(tmp_img)
print(f"Image saved to: {img_path}")
except Exception as e:
print(f"Error converting HTML to image: {e}")
driver.quit()

15
webview.py Normal file
View File

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