saved preview picture
This commit is contained in:
BIN
__pycache__/config.cpython-311.pyc
Normal file
BIN
__pycache__/config.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
13
config.py
Normal file
13
config.py
Normal 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
818
main.py
@@ -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
46
proba
Normal 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()
|
||||
@@ -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
169
resources/projects/Ziua 44/preview.html
Normal file
169
resources/projects/Ziua 44/preview.html
Normal 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>
|
||||
BIN
resources/projects/Ziua 44/preview.png
Normal file
BIN
resources/projects/Ziua 44/preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 352 KiB |
51417
resources/projects/many nice days/positions.json
Normal file
51417
resources/projects/many nice days/positions.json
Normal file
File diff suppressed because it is too large
Load Diff
169
resources/projects/many nice days/preview.html
Normal file
169
resources/projects/many nice days/preview.html
Normal file
File diff suppressed because one or more lines are too long
BIN
resources/projects/many nice days/preview.png
Normal file
BIN
resources/projects/many nice days/preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 546 KiB |
0
screens/__init__.py
Normal file
0
screens/__init__.py
Normal file
BIN
screens/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
screens/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
screens/__pycache__/create_animation_screen.cpython-311.pyc
Normal file
BIN
screens/__pycache__/create_animation_screen.cpython-311.pyc
Normal file
Binary file not shown.
BIN
screens/__pycache__/get_trip_from_server.cpython-311.pyc
Normal file
BIN
screens/__pycache__/get_trip_from_server.cpython-311.pyc
Normal file
Binary file not shown.
BIN
screens/__pycache__/home_screen.cpython-311.pyc
Normal file
BIN
screens/__pycache__/home_screen.cpython-311.pyc
Normal file
Binary file not shown.
BIN
screens/__pycache__/login_screen.cpython-311.pyc
Normal file
BIN
screens/__pycache__/login_screen.cpython-311.pyc
Normal file
Binary file not shown.
BIN
screens/__pycache__/settings_screen.cpython-311.pyc
Normal file
BIN
screens/__pycache__/settings_screen.cpython-311.pyc
Normal file
Binary file not shown.
264
screens/create_animation_screen.py
Normal file
264
screens/create_animation_screen.py
Normal 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)
|
||||
|
||||
347
screens/get_trip_from_server.py
Normal file
347
screens/get_trip_from_server.py
Normal 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
152
screens/home_screen.py
Normal 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
29
screens/login_screen.py
Normal 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
134
screens/settings_screen.py
Normal 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
|
||||
61
traccar.kv
61
traccar.kv
@@ -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"
|
||||
43
utils.py
43
utils.py
@@ -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
15
webview.py
Normal 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()
|
||||
Reference in New Issue
Block a user