Files
traccar_animation/screens/pause_edit_screen_legacy.py
2025-07-02 16:41:44 +03:00

1963 lines
75 KiB
Python

import kivy
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.textinput import TextInput
from kivy.uix.filechooser import FileChooserIconView
from kivy.uix.widget import Widget
from kivy.uix.image import Image
from kivy.uix.carousel import Carousel
from kivy.uix.progressbar import ProgressBar
from kivy.graphics import Color, Rectangle, Line
from kivy.uix.scrollview import ScrollView
from kivy.uix.popup import Popup
from kivy.uix.gridlayout import GridLayout
from kivy.properties import StringProperty
from kivy.clock import Clock
import os
import json
import shutil
import threading
from geopy.geocoders import Nominatim
from config import RESOURCES_FOLDER
class PauseEditScreen(Screen):
project_name = StringProperty("")
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.pauses = []
self.on_save_callback = None
self.loading_popup = None
self.carousel = None
def on_pre_enter(self):
"""Called when entering the screen"""
self.show_loading_popup()
# Delay the layout building to show loading popup first
Clock.schedule_once(self.start_loading_process, 0.1)
def show_loading_popup(self):
"""Show loading popup while building the layout"""
layout = BoxLayout(orientation='vertical', spacing=20, padding=20)
# Loading animation/progress bar
progress = ProgressBar(
max=100,
size_hint_y=None,
height=20
)
# Animate the progress bar
def animate_progress(dt):
if progress.value < 95:
progress.value += 5
else:
progress.value = 10 # Reset for continuous animation
Clock.schedule_interval(animate_progress, 0.1)
loading_label = Label(
text="Loading pause information...\nPlease wait",
color=(1, 1, 1, 1),
font_size=16,
halign="center",
text_size=(300, None)
)
layout.add_widget(loading_label)
layout.add_widget(progress)
self.loading_popup = Popup(
title="Loading Pauses",
content=layout,
size_hint=(0.8, 0.3),
auto_dismiss=False
)
self.loading_popup.open()
def start_loading_process(self, dt):
"""Start the loading process in background"""
# Run the heavy loading in a separate thread
thread = threading.Thread(target=self.load_data_background)
thread.daemon = True
thread.start()
def load_data_background(self):
"""Load pause data in background thread"""
try:
# Load pauses
self.load_pauses()
# Pre-process location suggestions to speed up UI
for pause in self.pauses:
lat = pause["location"]["latitude"]
lon = pause["location"]["longitude"]
# Cache the location suggestion
if 'location_suggestion' not in pause:
pause['location_suggestion'] = self.suggest_location_name(lat, lon)
# Schedule UI update on main thread
Clock.schedule_once(self.finish_loading, 0)
except Exception as e:
print(f"Error loading pause data: {e}")
Clock.schedule_once(self.finish_loading, 0)
def finish_loading(self, dt):
"""Finish loading and build the UI"""
try:
self.build_pause_layout()
finally:
# Close loading popup
if self.loading_popup:
self.loading_popup.dismiss()
self.loading_popup = None
def suggest_location_name(self, lat, lon):
"""Simplified and improved location suggestion with focus on practical results"""
print(f"DEBUG: Getting location suggestion for {lat}, {lon}")
try:
geolocator = Nominatim(user_agent="traccar_animation")
# Strategy 1: Direct reverse geocoding with multiple zoom levels
location_result = self._get_reverse_location(geolocator, lat, lon)
if location_result:
print(f"DEBUG: Found location result: {location_result}")
return location_result
# Strategy 2: Nearby search with expanded radius
nearby_result = self._search_nearby_expanded(geolocator, lat, lon)
if nearby_result:
print(f"DEBUG: Found nearby result: {nearby_result}")
return nearby_result
# Strategy 3: Fallback with coordinates
fallback = f"Location {lat:.4f}, {lon:.4f}"
print(f"DEBUG: Using fallback: {fallback}")
return fallback
except Exception as e:
error_msg = f"Location {lat:.5f}, {lon:.5f}"
print(f"DEBUG: Error in location suggestion: {e}, returning: {error_msg}")
return error_msg
def _get_reverse_location(self, geolocator, lat, lon):
"""Get location using reverse geocoding with multiple strategies"""
try:
# Try high-precision search first (zoom level 18)
location = geolocator.reverse(
(lat, lon),
exactly_one=True,
timeout=10,
addressdetails=True,
zoom=18
)
if location and location.raw:
print(f"DEBUG: Raw location data: {location.raw}")
address = location.raw.get('address', {})
# Priority 1: Look for specific places with names
if 'name' in location.raw and location.raw['name']:
name = location.raw['name']
if len(name) > 3 and not name.replace('.', '').replace('-', '').isdigit():
return name
# Priority 2: Look for amenities with names
if 'amenity' in address:
amenity = address['amenity']
# Try to get the actual name of the amenity
if 'name' in address:
return f"{address['name']} ({amenity})"
elif amenity in ['restaurant', 'cafe', 'shop', 'bank', 'hospital', 'school']:
return amenity.title()
# Priority 3: Street address (most reliable for navigation)
street_address = self._extract_street_address(address)
if street_address:
return street_address
# Priority 4: Neighborhood or area
area_name = self._extract_area_name(address)
if area_name:
return area_name
# Priority 5: Parse display name for useful info
display_name = location.raw.get('display_name', '')
if display_name:
parsed = self._parse_display_name(display_name)
if parsed:
return parsed
# Try with different zoom levels if first attempt fails
for zoom in [17, 16, 15]:
try:
location = geolocator.reverse(
(lat, lon),
exactly_one=True,
timeout=8,
addressdetails=True,
zoom=zoom
)
if location and location.raw:
address = location.raw.get('address', {})
street_address = self._extract_street_address(address)
if street_address:
return street_address
area_name = self._extract_area_name(address)
if area_name:
return area_name
except Exception:
continue
except Exception as e:
print(f"DEBUG: Error in reverse location: {e}")
return None
def _extract_street_address(self, address):
"""Extract street address from geocoding address data"""
if not address:
return None
# Try different combinations for street address
road = address.get('road', '')
house_number = address.get('house_number', '')
if road and house_number:
return f"{road} {house_number}"
elif road:
return road
# Try alternative road names
for road_key in ['street', 'pedestrian', 'footway', 'path']:
if road_key in address and address[road_key]:
road_name = address[road_key]
if house_number:
return f"{road_name} {house_number}"
return road_name
return None
def _extract_area_name(self, address):
"""Extract neighborhood, suburb, or area name"""
if not address:
return None
# Priority order for area names
area_keys = [
'neighbourhood', 'suburb', 'district', 'quarter',
'hamlet', 'village', 'town', 'city_district',
'municipality', 'city', 'county'
]
for key in area_keys:
if key in address and address[key]:
area = address[key]
if len(area) > 2 and not area.isdigit():
return area
return None
def _parse_display_name(self, display_name):
"""Parse display name to extract meaningful location"""
if not display_name:
return None
parts = [part.strip() for part in display_name.split(',')]
if parts:
first_part = parts[0]
# Skip if it's just numbers or coordinates
if (len(first_part) > 3 and
not first_part.replace('.', '').replace('-', '').replace(' ', '').isdigit()):
return first_part
return None
def _search_nearby_expanded(self, geolocator, lat, lon):
"""Search for nearby places with expanded radius"""
try:
# Try different radius sizes
for radius in [50, 100, 200]:
results = geolocator.reverse(
(lat, lon),
exactly_one=False,
radius=radius,
timeout=10,
addressdetails=True
)
if results:
for result in results[:5]: # Check first 5 results
if result and result.raw:
address = result.raw.get('address', {})
# Look for named places
if 'name' in result.raw and result.raw['name']:
name = result.raw['name']
if len(name) > 3:
return name
# Look for specific amenities
if 'amenity' in address:
amenity = address['amenity']
if amenity in ['restaurant', 'cafe', 'shop', 'bank', 'hospital', 'school', 'pharmacy']:
if 'name' in address:
return f"{address['name']} ({amenity})"
return amenity.title()
# Look for shops
if 'shop' in address:
shop = address['shop']
if 'name' in address:
return f"{address['name']} ({shop} shop)"
return f"{shop.title()} Shop"
# If we found something, don't try larger radius
if results:
break
except Exception as e:
print(f"DEBUG: Error in nearby search: {e}")
return None
def _search_nearby_pois(self, geolocator, lat, lon):
"""Search for Points of Interest within 100 meters"""
nearby_places = []
try:
# Get all nearby results within 100m
results = geolocator.reverse(
(lat, lon),
exactly_one=False,
radius=100,
timeout=20,
addressdetails=True
)
if not results:
return nearby_places
# Define priority order for place types
priority_place_types = [
'amenity', 'shop', 'tourism', 'attraction', 'leisure',
'building', 'office', 'historic', 'natural', 'landuse'
]
# Specific amenity types we want to prioritize
priority_amenities = [
'restaurant', 'cafe', 'bank', 'hospital', 'pharmacy',
'school', 'university', 'library', 'post_office',
'gas_station', 'parking', 'hotel', 'church', 'mosque',
'synagogue', 'temple', 'police', 'fire_station', 'atm',
'fuel', 'supermarket', 'mall', 'cinema', 'theatre'
]
place_scores = []
for result in results[:15]: # Check up to 15 results for better coverage
if not result or not result.raw:
continue
raw_data = result.raw
address = raw_data.get('address', {})
tags = raw_data.get('extratags', {}) if 'extratags' in raw_data else {}
# Score and extract place names
place_info = self._score_and_extract_place(address, tags, priority_place_types, priority_amenities)
if place_info:
place_scores.append(place_info)
# Sort by score (higher is better) and return unique places
place_scores.sort(key=lambda x: x['score'], reverse=True)
seen_names = set()
for place_info in place_scores:
name = place_info['name']
if name and name not in seen_names and len(name.strip()) > 2:
# Clean up the name
cleaned_name = name.strip()
# Skip generic terms
if cleaned_name.lower() not in ['building', 'house', 'place', 'location', 'area']:
nearby_places.append(cleaned_name)
seen_names.add(cleaned_name)
if len(nearby_places) >= 3: # Limit to top 3
break
except Exception as e:
print(f"Error searching nearby POIs: {e}")
return nearby_places
def _score_and_extract_place(self, address, tags, priority_place_types, priority_amenities):
"""Score and extract place name from address and tags"""
best_name = None
best_score = 0
# Check for high-priority amenities first
if 'amenity' in address:
amenity_type = address['amenity']
if amenity_type in priority_amenities:
# Look for a name in tags or address
name = tags.get('name') or address.get('name') or address.get('amenity')
if name:
return {'name': name, 'score': 100}
# Check other priority place types
for i, place_type in enumerate(priority_place_types):
if place_type in address and address[place_type]:
score = 90 - (i * 5) # Decreasing score based on priority
# Try to get a proper name
name = None
if place_type in tags:
name = tags.get('name')
if not name:
name = address.get('name')
if not name and place_type == 'amenity':
name = address[place_type]
if not name:
name = address[place_type]
if name and score > best_score:
best_name = name
best_score = score
# Check for named places in tags
if 'name' in tags and tags['name']:
if best_score < 80:
return {'name': tags['name'], 'score': 80}
if 'name' in address and address['name']:
if best_score < 75:
return {'name': address['name'], 'score': 75}
if best_name:
return {'name': best_name, 'score': best_score}
return None
def _extract_meaningful_name(self, raw_data):
"""Extract meaningful location name from raw geocoding data"""
address = raw_data.get('address', {})
# High priority location types
high_priority = [
'attraction', 'tourism', 'amenity', 'shop', 'leisure',
'building', 'office', 'historic', 'name'
]
# Check high priority fields
for field in high_priority:
if field in address and address[field]:
value = address[field]
if len(value.strip()) > 2: # Ensure meaningful length
return value
# Try display name parsing
display_name = raw_data.get('display_name', '')
if display_name:
parts = [part.strip() for part in display_name.split(',')]
if parts and len(parts[0]) > 2:
first_part = parts[0]
# Skip if it's just numbers or coordinates
if not first_part.replace('.', '').replace('-', '').replace(' ', '').isdigit():
return first_part
# Street address fallback
if 'road' in address:
road = address['road']
if 'house_number' in address:
return f"{road} {address['house_number']}"
return road
# Area-based fallback
area_keys = ['neighbourhood', 'suburb', 'hamlet', 'village', 'town', 'city']
for key in area_keys:
if key in address and address[key]:
return address[key]
return None
def _alternative_nearby_search(self, geolocator, lat, lon):
"""Alternative search method using bounding box queries"""
try:
# Create a small bounding box around the point (roughly 100m x 100m)
offset = 0.001 # Approximately 100m in degrees
bbox_queries = [
(lat + offset/2, lon - offset/2, lat - offset/2, lon + offset/2), # North-South sweep
(lat - offset/2, lon - offset/2, lat + offset/2, lon + offset/2), # Full box
]
found_places = []
for bbox in bbox_queries:
try:
# Use geocode to search within bounding box
results = geolocator.geocode(
query="",
exactly_one=False,
addressdetails=True,
extratags=True,
bbox=bbox,
timeout=10
)
if results:
for result in results[:5]:
if result and result.raw:
address = result.raw.get('address', {})
# Look for interesting places
place_types = ['amenity', 'shop', 'tourism', 'leisure', 'building']
for place_type in place_types:
if place_type in address:
place_name = address.get('name') or address[place_type]
if place_name and place_name not in found_places:
found_places.append(place_name)
if len(found_places) >= 3:
return found_places
except Exception:
continue
return found_places
except Exception as e:
print(f"Error in alternative search: {e}")
return []
def load_pauses(self):
"""Load pauses from the project folder"""
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
pauses_path = os.path.join(project_folder, "pauses.json")
if os.path.exists(pauses_path):
with open(pauses_path, "r") as f:
self.pauses = json.load(f)
else:
self.pauses = []
def save_pauses(self):
"""Save pauses to the project folder"""
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
pauses_path = os.path.join(project_folder, "pauses.json")
with open(pauses_path, "w") as f:
json.dump(self.pauses, f, indent=2)
def build_pause_layout(self):
"""Build the main pause editing layout"""
self.clear_widgets()
self.load_pauses()
# Main layout with dark background
main_layout = BoxLayout(orientation='vertical', spacing=5, padding=[5, 5, 5, 5])
with main_layout.canvas.before:
Color(0.11, 0.10, 0.15, 1)
main_layout.bg_rect = Rectangle(pos=main_layout.pos, size=main_layout.size)
main_layout.bind(pos=self.update_bg_rect, size=self.update_bg_rect)
# Header with back button
header = BoxLayout(orientation='horizontal', size_hint_y=None, height=40, spacing=10)
back_btn = Button(
text="← Back",
size_hint_x=None,
width=70,
font_size=14,
background_color=(0.341, 0.235, 0.980, 1),
color=(1, 1, 1, 1)
)
back_btn.bind(on_press=self.go_back)
title_label = Label(
text="Edit Pauses",
font_size=16,
color=(1, 1, 1, 1),
halign="center",
bold=True
)
header.add_widget(back_btn)
header.add_widget(title_label)
header.add_widget(Widget(size_hint_x=None, width=70))
main_layout.add_widget(header)
# Scrollable area for pause frames
scroll_content = BoxLayout(orientation='vertical', spacing=10, size_hint_y=None, padding=[5, 5, 5, 5])
scroll_content.bind(minimum_height=scroll_content.setter('height'))
for idx, pause in enumerate(self.pauses):
pause_frame = self.create_pause_frame(idx, pause)
scroll_content.add_widget(pause_frame)
scroll = ScrollView(size_hint=(1, 1))
scroll.add_widget(scroll_content)
main_layout.add_widget(scroll)
# Save all button at bottom
save_all_btn = Button(
text="Save All Changes & Go Back",
size_hint_y=None,
height=45,
font_size=14,
background_color=(0.2, 0.7, 0.2, 1),
color=(1, 1, 1, 1),
bold=True
)
save_all_btn.bind(on_press=self.save_all_and_close)
main_layout.add_widget(save_all_btn)
self.add_widget(main_layout)
def update_bg_rect(self, instance, value):
"""Update background rectangle"""
instance.bg_rect.pos = instance.pos
instance.bg_rect.size = instance.size
def create_pause_frame(self, idx, pause):
"""Create a frame for a single pause"""
# Main frame with border
frame = BoxLayout(
orientation='vertical',
spacing=8,
padding=[8, 8, 8, 8],
size_hint_y=None,
height=340 # Increased height for image previews
)
with frame.canvas.before:
Color(0.18, 0.18, 0.22, 1) # Frame background
frame.bg_rect = Rectangle(pos=frame.pos, size=frame.size)
Color(0.4, 0.6, 1.0, 1) # Frame border
frame.border_line = Line(rectangle=(frame.x, frame.y, frame.width, frame.height), width=2)
def update_frame(instance, value, frame_widget=frame):
frame_widget.bg_rect.pos = frame_widget.pos
frame_widget.bg_rect.size = frame_widget.size
frame_widget.border_line.rectangle = (frame_widget.x, frame_widget.y, frame_widget.width, frame_widget.height)
frame.bind(pos=update_frame, size=update_frame)
# 1. Pause number label (centered)
pause_number_label = Label(
text=f"[b]PAUSE {idx + 1}[/b]",
markup=True,
font_size=16,
color=(1, 1, 1, 1),
size_hint_y=None,
height=30,
halign="center",
valign="middle"
)
pause_number_label.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None)))
frame.add_widget(pause_number_label)
# 2. Location suggestion (left aligned)
suggested_place = self.suggest_location_name(pause["location"]["latitude"], pause["location"]["longitude"])
location_label = Label(
text=f"Location: {suggested_place}",
font_size=12,
color=(0.8, 0.9, 1, 1),
size_hint_y=None,
height=25,
halign="left",
valign="middle"
)
location_label.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None)))
frame.add_widget(location_label)
# 3. Custom name entry and save button
name_layout = BoxLayout(orientation='horizontal', spacing=5, size_hint_y=None, height=35)
name_input = TextInput(
text=pause.get('name', ''),
hint_text="Enter custom location name...",
multiline=False,
background_color=(0.25, 0.25, 0.3, 1),
foreground_color=(1, 1, 1, 1),
font_size=12,
padding=[8, 8, 8, 8]
)
save_name_btn = Button(
text="Save Name",
size_hint_x=None,
width=80,
font_size=11,
background_color=(0.341, 0.235, 0.980, 1),
color=(1, 1, 1, 1)
)
save_name_btn.bind(on_press=lambda x: self.save_pause_name(pause, name_input))
name_layout.add_widget(name_input)
name_layout.add_widget(save_name_btn)
frame.add_widget(name_layout)
# 4. Photos area
photos_area = self.create_photos_area(idx, pause)
frame.add_widget(photos_area)
# 5. Save pause info button
save_pause_btn = Button(
text="Save Pause Info",
size_hint_y=None,
height=30,
font_size=12,
background_color=(0.2, 0.6, 0.8, 1),
color=(1, 1, 1, 1)
)
save_pause_btn.bind(on_press=lambda x: self.save_individual_pause(idx))
frame.add_widget(save_pause_btn)
return frame
def create_photos_area(self, pause_idx, pause):
"""Create the photos area for a pause"""
photos_layout = BoxLayout(orientation='vertical', spacing=5, size_hint_y=None, height=180) # Increased height for image previews
# Photos header with add button
photos_header = BoxLayout(orientation='horizontal', size_hint_y=None, height=30)
photos_title = Label(
text="Photos:",
font_size=12,
color=(1, 1, 1, 1),
size_hint_x=0.5,
halign="left",
valign="middle"
)
photos_title.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None)))
add_photos_btn = Button(
text="Add Photos",
size_hint_x=0.5,
font_size=11,
background_color=(0.2, 0.7, 0.2, 1),
color=(1, 1, 1, 1)
)
add_photos_btn.bind(on_press=lambda x: self.add_photos(pause_idx))
photos_header.add_widget(photos_title)
photos_header.add_widget(add_photos_btn)
photos_layout.add_widget(photos_header)
# Photos list area
photos_scroll_content = BoxLayout(orientation='vertical', spacing=2, size_hint_y=None, padding=[0, 0, 0, 0])
photos_scroll_content.bind(minimum_height=photos_scroll_content.setter('height'))
# Get photos for this pause
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
pause_img_folder = os.path.join(project_folder, f"pause_{pause_idx+1}")
os.makedirs(pause_img_folder, exist_ok=True)
img_list = [f for f in os.listdir(pause_img_folder) if os.path.isfile(os.path.join(pause_img_folder, f))]
if img_list:
for img_file in img_list:
photo_item = self.create_photo_item(pause_idx, img_file, photos_scroll_content)
photos_scroll_content.add_widget(photo_item)
else:
no_photos_label = Label(
text="No photos added yet",
font_size=10,
color=(0.6, 0.6, 0.6, 1),
size_hint_y=None,
height=20,
halign="center"
)
photos_scroll_content.add_widget(no_photos_label)
photos_scroll = ScrollView(size_hint=(1, 1))
photos_scroll.add_widget(photos_scroll_content)
photos_layout.add_widget(photos_scroll)
return photos_layout
def create_photo_item(self, pause_idx, img_file, parent_layout):
"""Create a single photo item with image preview and delete button"""
# Main container with border
photo_item = BoxLayout(orientation='horizontal', spacing=5, size_hint_y=None, height=60, padding=[2, 2, 2, 2])
# Add border and background to photo item
with photo_item.canvas.before:
Color(0.25, 0.25, 0.30, 1) # Background
photo_item.bg_rect = Rectangle(pos=photo_item.pos, size=photo_item.size)
Color(0.4, 0.4, 0.5, 1) # Border
photo_item.border_line = Line(rectangle=(photo_item.x, photo_item.y, photo_item.width, photo_item.height), width=1)
def update_photo_item(instance, value, item=photo_item):
item.bg_rect.pos = item.pos
item.bg_rect.size = item.size
item.border_line.rectangle = (item.x, item.y, item.width, item.height)
photo_item.bind(pos=update_photo_item, size=update_photo_item)
# Get full path to the image
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
pause_img_folder = os.path.join(project_folder, f"pause_{pause_idx+1}")
img_path = os.path.join(pause_img_folder, img_file)
# Image thumbnail container
image_container = BoxLayout(size_hint_x=None, width=55, padding=[2, 2, 2, 2])
try:
photo_image = Image(
source=img_path,
size_hint=(1, 1),
allow_stretch=True,
keep_ratio=True
)
except Exception:
# Fallback to a placeholder if image can't be loaded
photo_image = Widget(size_hint=(1, 1))
with photo_image.canvas:
Color(0.3, 0.3, 0.3, 1)
Rectangle(pos=photo_image.pos, size=photo_image.size)
Color(1, 1, 1, 1)
# Add text "No Preview"
image_container.add_widget(photo_image)
# Photo info layout (filename and details)
info_layout = BoxLayout(orientation='vertical', spacing=2, size_hint_x=0.55, padding=[5, 2, 5, 2])
# Filename label (truncate if too long)
display_name = img_file if len(img_file) <= 20 else f"{img_file[:17]}..."
filename_label = Label(
text=display_name,
font_size=9,
color=(1, 1, 1, 1),
halign="left",
valign="top",
size_hint_y=0.6,
bold=True
)
filename_label.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None)))
# File size and type info
try:
file_size = os.path.getsize(img_path)
if file_size < 1024:
size_text = f"{file_size} B"
elif file_size < 1024*1024:
size_text = f"{file_size/1024:.1f} KB"
else:
size_text = f"{file_size/(1024*1024):.1f} MB"
# Get file extension
file_ext = os.path.splitext(img_file)[1].upper().replace('.', '')
info_text = f"{file_ext}{size_text}"
except:
info_text = "Unknown format"
size_label = Label(
text=info_text,
font_size=7,
color=(0.8, 0.8, 0.8, 1),
halign="left",
valign="bottom",
size_hint_y=0.4
)
size_label.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None)))
info_layout.add_widget(filename_label)
info_layout.add_widget(size_label)
# Button layout for actions
button_layout = BoxLayout(orientation='vertical', spacing=2, size_hint_x=0.28, padding=[2, 2, 2, 2])
# View button to show full image
view_btn = Button(
text="👁 View",
font_size=8,
background_color=(0.2, 0.6, 0.8, 1),
color=(1, 1, 1, 1),
size_hint_y=0.5
)
view_btn.bind(on_press=lambda x: self.view_full_image(img_path, img_file))
# Delete button
delete_btn = Button(
text="🗑 Del",
font_size=8,
background_color=(0.8, 0.2, 0.2, 1),
color=(1, 1, 1, 1),
size_hint_y=0.5
)
delete_btn.bind(on_press=lambda x: self.delete_single_photo(pause_idx, img_file, photo_item, parent_layout))
button_layout.add_widget(view_btn)
button_layout.add_widget(delete_btn)
# Add all components to photo item
photo_item.add_widget(image_container)
photo_item.add_widget(info_layout)
photo_item.add_widget(button_layout)
return photo_item
def save_pause_name(self, pause, name_input):
"""Save the custom name for a pause"""
pause['name'] = name_input.text
def save_individual_pause(self, pause_idx):
"""Save individual pause info"""
self.save_pauses()
# Show confirmation
self.show_message("Pause Saved", f"Pause {pause_idx + 1} information saved successfully!")
def add_photos(self, pause_idx):
"""Open file browser to add photos"""
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
pause_img_folder = os.path.join(project_folder, f"pause_{pause_idx+1}")
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
with layout.canvas.before:
Color(0.13, 0.13, 0.16, 1)
layout.bg_rect = Rectangle(pos=layout.pos, size=layout.size)
layout.bind(
pos=lambda inst, val: setattr(layout.bg_rect, 'pos', inst.pos),
size=lambda inst, val: setattr(layout.bg_rect, 'size', inst.size)
)
title_label = Label(
text=f"Select photos for Pause {pause_idx + 1}:",
color=(1, 1, 1, 1),
font_size=14,
size_hint_y=None,
height=30
)
filechooser = FileChooserIconView(
filters=['*.png', '*.jpg', '*.jpeg', '*.gif', '*.bmp'],
path=os.path.expanduser('~'),
multiselect=True
)
btn_layout = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=40)
add_btn = Button(
text="Add Selected",
background_color=(0.2, 0.7, 0.2, 1),
color=(1, 1, 1, 1),
font_size=12
)
cancel_btn = Button(
text="Cancel",
background_color=(0.6, 0.3, 0.3, 1),
color=(1, 1, 1, 1),
font_size=12
)
btn_layout.add_widget(cancel_btn)
btn_layout.add_widget(add_btn)
layout.add_widget(title_label)
layout.add_widget(filechooser)
layout.add_widget(btn_layout)
popup = Popup(
title="Add Photos",
content=layout,
size_hint=(0.95, 0.9),
auto_dismiss=False
)
def add_selected_files(instance):
if filechooser.selection:
for file_path in filechooser.selection:
if os.path.isfile(file_path):
filename = os.path.basename(file_path)
dest_path = os.path.join(pause_img_folder, filename)
if not os.path.exists(dest_path):
shutil.copy2(file_path, dest_path)
self.refresh_photos_display()
popup.dismiss()
else:
popup.dismiss()
add_btn.bind(on_press=add_selected_files)
cancel_btn.bind(on_press=lambda x: popup.dismiss())
popup.open()
def delete_single_photo(self, pause_idx, img_file, photo_item, parent_layout):
"""Delete a single photo with confirmation"""
def confirm_delete():
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
pause_img_folder = os.path.join(project_folder, f"pause_{pause_idx+1}")
file_path = os.path.join(pause_img_folder, img_file)
if os.path.exists(file_path):
os.remove(file_path)
parent_layout.remove_widget(photo_item)
parent_layout.height = max(20, len(parent_layout.children) * 62) # Updated for new photo item height
self.show_confirmation(
f"Delete Photo",
f"Are you sure you want to delete '{img_file}'?",
confirm_delete
)
def refresh_photos_display(self):
"""Refresh the entire display to show updated photos"""
self.build_pause_layout()
def show_message(self, title, message):
"""Show a simple message popup"""
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
msg_label = Label(
text=message,
color=(1, 1, 1, 1),
font_size=12,
halign="center"
)
msg_label.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None)))
ok_btn = Button(
text="OK",
size_hint_y=None,
height=35,
background_color=(0.341, 0.235, 0.980, 1),
color=(1, 1, 1, 1)
)
layout.add_widget(msg_label)
layout.add_widget(ok_btn)
popup = Popup(title=title, content=layout, size_hint=(0.8, 0.4))
ok_btn.bind(on_press=lambda x: popup.dismiss())
popup.open()
def show_confirmation(self, title, message, confirm_callback):
"""Show a confirmation dialog"""
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
msg_label = Label(
text=message,
color=(1, 1, 1, 1),
font_size=12,
halign="center"
)
msg_label.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None)))
btn_layout = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=35)
cancel_btn = Button(
text="Cancel",
background_color=(0.6, 0.3, 0.3, 1),
color=(1, 1, 1, 1)
)
confirm_btn = Button(
text="Confirm",
background_color=(0.8, 0.2, 0.2, 1),
color=(1, 1, 1, 1)
)
btn_layout.add_widget(cancel_btn)
btn_layout.add_widget(confirm_btn)
layout.add_widget(msg_label)
layout.add_widget(btn_layout)
popup = Popup(title=title, content=layout, size_hint=(0.8, 0.4), auto_dismiss=False)
cancel_btn.bind(on_press=lambda x: popup.dismiss())
confirm_btn.bind(on_press=lambda x: (confirm_callback(), popup.dismiss()))
popup.open()
def save_all_and_close(self, instance):
"""Save all pauses and return to previous screen"""
self.save_pauses()
if self.on_save_callback:
self.on_save_callback()
self.go_back()
def go_back(self, instance=None):
"""Return to the previous screen"""
self.manager.current = "create_animation"
def set_project_and_callback(self, project_name, callback=None):
"""Set the project name and callback for this screen"""
self.project_name = project_name
self.on_save_callback = callback
def view_full_image(self, img_path, img_file):
"""Show full image in a popup"""
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
with layout.canvas.before:
Color(0.05, 0.05, 0.08, 1)
layout.bg_rect = Rectangle(pos=layout.pos, size=layout.size)
layout.bind(
pos=lambda inst, val: setattr(layout.bg_rect, 'pos', inst.pos),
size=lambda inst, val: setattr(layout.bg_rect, 'size', inst.size)
)
# Image display
try:
full_image = Image(
source=img_path,
allow_stretch=True,
keep_ratio=True
)
except Exception:
full_image = Label(
text="Unable to load image",
color=(1, 1, 1, 1),
font_size=16
)
# Close button
close_btn = Button(
text="Close",
size_hint_y=None,
height=40,
background_color=(0.341, 0.235, 0.980, 1),
color=(1, 1, 1, 1),
font_size=14
)
layout.add_widget(full_image)
layout.add_widget(close_btn)
popup = Popup(
title=f"Photo: {img_file}",
content=layout,
size_hint=(0.95, 0.95),
auto_dismiss=False
)
close_btn.bind(on_press=lambda x: popup.dismiss())
popup.open()
def find_nearby_pois(self, lat, lon, radius=100):
"""Find nearby points of interest within specified radius"""
try:
geolocator = Nominatim(user_agent="traccar_animation")
nearby_places = []
# Try different search approaches
search_strategies = [
# Strategy 1: Direct reverse geocoding with details
{"exactly_one": False, "addressdetails": True, "extratags": True},
# Strategy 2: Single result with extended details
{"exactly_one": True, "addressdetails": True, "extratags": True, "zoom": 18},
]
for strategy in search_strategies:
try:
results = geolocator.reverse(
(lat, lon),
radius=radius,
timeout=10,
**strategy
)
# Handle both single result and list of results
if not isinstance(results, list):
results = [results] if results else []
for result in results[:5]: # Check up to 5 results
if not result or not result.raw:
continue
# Extract place information
address = result.raw.get('address', {})
extratags = result.raw.get('extratags', {})
# Look for interesting places
place_candidates = []
# Check address fields for places
poi_fields = [
'amenity', 'shop', 'tourism', 'leisure', 'building',
'attraction', 'restaurant', 'cafe', 'hotel', 'hospital',
'school', 'bank', 'pharmacy', 'supermarket', 'museum'
]
for field in poi_fields:
if field in address and address[field]:
place_candidates.append(address[field])
# Check extratags for additional place info
if extratags:
for key in ['name', 'brand', 'operator']:
if key in extratags and extratags[key]:
place_candidates.append(extratags[key])
# Add valid candidates to nearby places
for candidate in place_candidates:
if (candidate and
candidate not in nearby_places and
len(candidate) > 2 and
not candidate.isdigit()):
nearby_places.append(candidate)
if len(nearby_places) >= 3:
break
if nearby_places:
break
except Exception:
continue
return nearby_places[:2] # Return top 2 POIs
except Exception:
return []
def debug_location_suggestion(self, lat, lon):
"""Debug method to test location suggestions with detailed output"""
print(f"\n=== DEBUGGING LOCATION SUGGESTION ===")
print(f"Coordinates: {lat}, {lon}")
try:
from geopy.geocoders import Nominatim
geolocator = Nominatim(user_agent="traccar_animation")
# Test basic reverse geocoding
print("\n1. Basic reverse geocoding:")
location = geolocator.reverse((lat, lon), exactly_one=True, timeout=10, addressdetails=True)
if location:
print(f" Found: {location.address}")
if location.raw:
print(f" Raw address data: {location.raw.get('address', {})}")
print(f" Display name: {location.raw.get('display_name', 'N/A')}")
else:
print(" No location found")
# Test with different zoom levels
print("\n2. Testing different zoom levels:")
for zoom in [18, 17, 16, 15]:
try:
location = geolocator.reverse((lat, lon), exactly_one=True, timeout=8, addressdetails=True, zoom=zoom)
if location and location.raw:
address = location.raw.get('address', {})
print(f" Zoom {zoom}: {address}")
except Exception as e:
print(f" Zoom {zoom}: Error - {e}")
# Test nearby search
print("\n3. Nearby search:")
for radius in [50, 100, 200]:
try:
results = geolocator.reverse((lat, lon), exactly_one=False, radius=radius, timeout=10, addressdetails=True)
if results:
print(f" Radius {radius}m: Found {len(results)} results")
for i, result in enumerate(results[:3]):
if result and result.raw:
address = result.raw.get('address', {})
print(f" Result {i+1}: {address}")
else:
print(f" Radius {radius}m: No results")
except Exception as e:
print(f" Radius {radius}m: Error - {e}")
# Test final suggestion
print("\n4. Final suggestion:")
final_result = self.suggest_location_name(lat, lon)
print(f" Result: {final_result}")
except Exception as e:
print(f"DEBUG ERROR: {e}")
print("=== END DEBUG ===\n")
def load_pauses(self):
"""Load pauses from the project folder"""
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
pauses_path = os.path.join(project_folder, "pauses.json")
if os.path.exists(pauses_path):
with open(pauses_path, "r") as f:
self.pauses = json.load(f)
else:
self.pauses = []
def save_pauses(self):
"""Save pauses to the project folder"""
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
pauses_path = os.path.join(project_folder, "pauses.json")
with open(pauses_path, "w") as f:
json.dump(self.pauses, f, indent=2)
def build_pause_layout(self):
"""Build the main pause editing layout"""
self.clear_widgets()
self.load_pauses()
# Main layout with dark background
main_layout = BoxLayout(orientation='vertical', spacing=5, padding=[5, 5, 5, 5])
with main_layout.canvas.before:
Color(0.11, 0.10, 0.15, 1)
main_layout.bg_rect = Rectangle(pos=main_layout.pos, size=main_layout.size)
main_layout.bind(pos=self.update_bg_rect, size=self.update_bg_rect)
# Header with back button
header = BoxLayout(orientation='horizontal', size_hint_y=None, height=40, spacing=10)
back_btn = Button(
text="← Back",
size_hint_x=None,
width=70,
font_size=14,
background_color=(0.341, 0.235, 0.980, 1),
color=(1, 1, 1, 1)
)
back_btn.bind(on_press=self.go_back)
title_label = Label(
text="Edit Pauses",
font_size=16,
color=(1, 1, 1, 1),
halign="center",
bold=True
)
header.add_widget(back_btn)
header.add_widget(title_label)
header.add_widget(Widget(size_hint_x=None, width=70))
main_layout.add_widget(header)
# Scrollable area for pause frames
scroll_content = BoxLayout(orientation='vertical', spacing=10, size_hint_y=None, padding=[5, 5, 5, 5])
scroll_content.bind(minimum_height=scroll_content.setter('height'))
for idx, pause in enumerate(self.pauses):
pause_frame = self.create_pause_frame(idx, pause)
scroll_content.add_widget(pause_frame)
scroll = ScrollView(size_hint=(1, 1))
scroll.add_widget(scroll_content)
main_layout.add_widget(scroll)
# Save all button at bottom
save_all_btn = Button(
text="Save All Changes & Go Back",
size_hint_y=None,
height=45,
font_size=14,
background_color=(0.2, 0.7, 0.2, 1),
color=(1, 1, 1, 1),
bold=True
)
save_all_btn.bind(on_press=self.save_all_and_close)
main_layout.add_widget(save_all_btn)
self.add_widget(main_layout)
def update_bg_rect(self, instance, value):
"""Update background rectangle"""
instance.bg_rect.pos = instance.pos
instance.bg_rect.size = instance.size
def create_pause_frame(self, idx, pause):
"""Create a frame for a single pause"""
# Main frame with border
frame = BoxLayout(
orientation='vertical',
spacing=8,
padding=[8, 8, 8, 8],
size_hint_y=None,
height=340 # Increased height for image previews
)
with frame.canvas.before:
Color(0.18, 0.18, 0.22, 1) # Frame background
frame.bg_rect = Rectangle(pos=frame.pos, size=frame.size)
Color(0.4, 0.6, 1.0, 1) # Frame border
frame.border_line = Line(rectangle=(frame.x, frame.y, frame.width, frame.height), width=2)
def update_frame(instance, value, frame_widget=frame):
frame_widget.bg_rect.pos = frame_widget.pos
frame_widget.bg_rect.size = frame_widget.size
frame_widget.border_line.rectangle = (frame_widget.x, frame_widget.y, frame_widget.width, frame_widget.height)
frame.bind(pos=update_frame, size=update_frame)
# 1. Pause number label (centered)
pause_number_label = Label(
text=f"[b]PAUSE {idx + 1}[/b]",
markup=True,
font_size=16,
color=(1, 1, 1, 1),
size_hint_y=None,
height=30,
halign="center",
valign="middle"
)
pause_number_label.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None)))
frame.add_widget(pause_number_label)
# 2. Location suggestion (left aligned)
suggested_place = self.suggest_location_name(pause["location"]["latitude"], pause["location"]["longitude"])
location_label = Label(
text=f"Location: {suggested_place}",
font_size=12,
color=(0.8, 0.9, 1, 1),
size_hint_y=None,
height=25,
halign="left",
valign="middle"
)
location_label.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None)))
frame.add_widget(location_label)
# 3. Custom name entry and save button
name_layout = BoxLayout(orientation='horizontal', spacing=5, size_hint_y=None, height=35)
name_input = TextInput(
text=pause.get('name', ''),
hint_text="Enter custom location name...",
multiline=False,
background_color=(0.25, 0.25, 0.3, 1),
foreground_color=(1, 1, 1, 1),
font_size=12,
padding=[8, 8, 8, 8]
)
save_name_btn = Button(
text="Save Name",
size_hint_x=None,
width=80,
font_size=11,
background_color=(0.341, 0.235, 0.980, 1),
color=(1, 1, 1, 1)
)
save_name_btn.bind(on_press=lambda x: self.save_pause_name(pause, name_input))
name_layout.add_widget(name_input)
name_layout.add_widget(save_name_btn)
frame.add_widget(name_layout)
# 4. Photos area
photos_area = self.create_photos_area(idx, pause)
frame.add_widget(photos_area)
# 5. Save pause info button
save_pause_btn = Button(
text="Save Pause Info",
size_hint_y=None,
height=30,
font_size=12,
background_color=(0.2, 0.6, 0.8, 1),
color=(1, 1, 1, 1)
)
save_pause_btn.bind(on_press=lambda x: self.save_individual_pause(idx))
frame.add_widget(save_pause_btn)
return frame
def create_photos_area(self, pause_idx, pause):
"""Create the photos area for a pause"""
photos_layout = BoxLayout(orientation='vertical', spacing=5, size_hint_y=None, height=180) # Increased height for image previews
# Photos header with add button
photos_header = BoxLayout(orientation='horizontal', size_hint_y=None, height=30)
photos_title = Label(
text="Photos:",
font_size=12,
color=(1, 1, 1, 1),
size_hint_x=0.5,
halign="left",
valign="middle"
)
photos_title.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None)))
add_photos_btn = Button(
text="Add Photos",
size_hint_x=0.5,
font_size=11,
background_color=(0.2, 0.7, 0.2, 1),
color=(1, 1, 1, 1)
)
add_photos_btn.bind(on_press=lambda x: self.add_photos(pause_idx))
photos_header.add_widget(photos_title)
photos_header.add_widget(add_photos_btn)
photos_layout.add_widget(photos_header)
# Photos list area
photos_scroll_content = BoxLayout(orientation='vertical', spacing=2, size_hint_y=None, padding=[0, 0, 0, 0])
photos_scroll_content.bind(minimum_height=photos_scroll_content.setter('height'))
# Get photos for this pause
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
pause_img_folder = os.path.join(project_folder, f"pause_{pause_idx+1}")
os.makedirs(pause_img_folder, exist_ok=True)
img_list = [f for f in os.listdir(pause_img_folder) if os.path.isfile(os.path.join(pause_img_folder, f))]
if img_list:
for img_file in img_list:
photo_item = self.create_photo_item(pause_idx, img_file, photos_scroll_content)
photos_scroll_content.add_widget(photo_item)
else:
no_photos_label = Label(
text="No photos added yet",
font_size=10,
color=(0.6, 0.6, 0.6, 1),
size_hint_y=None,
height=20,
halign="center"
)
photos_scroll_content.add_widget(no_photos_label)
photos_scroll = ScrollView(size_hint=(1, 1))
photos_scroll.add_widget(photos_scroll_content)
photos_layout.add_widget(photos_scroll)
return photos_layout
def create_photo_item(self, pause_idx, img_file, parent_layout):
"""Create a single photo item with image preview and delete button"""
# Main container with border
photo_item = BoxLayout(orientation='horizontal', spacing=5, size_hint_y=None, height=60, padding=[2, 2, 2, 2])
# Add border and background to photo item
with photo_item.canvas.before:
Color(0.25, 0.25, 0.30, 1) # Background
photo_item.bg_rect = Rectangle(pos=photo_item.pos, size=photo_item.size)
Color(0.4, 0.4, 0.5, 1) # Border
photo_item.border_line = Line(rectangle=(photo_item.x, photo_item.y, photo_item.width, photo_item.height), width=1)
def update_photo_item(instance, value, item=photo_item):
item.bg_rect.pos = item.pos
item.bg_rect.size = item.size
item.border_line.rectangle = (item.x, item.y, item.width, item.height)
photo_item.bind(pos=update_photo_item, size=update_photo_item)
# Get full path to the image
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
pause_img_folder = os.path.join(project_folder, f"pause_{pause_idx+1}")
img_path = os.path.join(pause_img_folder, img_file)
# Image thumbnail container
image_container = BoxLayout(size_hint_x=None, width=55, padding=[2, 2, 2, 2])
try:
photo_image = Image(
source=img_path,
size_hint=(1, 1),
allow_stretch=True,
keep_ratio=True
)
except Exception:
# Fallback to a placeholder if image can't be loaded
photo_image = Widget(size_hint=(1, 1))
with photo_image.canvas:
Color(0.3, 0.3, 0.3, 1)
Rectangle(pos=photo_image.pos, size=photo_image.size)
Color(1, 1, 1, 1)
# Add text "No Preview"
image_container.add_widget(photo_image)
# Photo info layout (filename and details)
info_layout = BoxLayout(orientation='vertical', spacing=2, size_hint_x=0.55, padding=[5, 2, 5, 2])
# Filename label (truncate if too long)
display_name = img_file if len(img_file) <= 20 else f"{img_file[:17]}..."
filename_label = Label(
text=display_name,
font_size=9,
color=(1, 1, 1, 1),
halign="left",
valign="top",
size_hint_y=0.6,
bold=True
)
filename_label.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None)))
# File size and type info
try:
file_size = os.path.getsize(img_path)
if file_size < 1024:
size_text = f"{file_size} B"
elif file_size < 1024*1024:
size_text = f"{file_size/1024:.1f} KB"
else:
size_text = f"{file_size/(1024*1024):.1f} MB"
# Get file extension
file_ext = os.path.splitext(img_file)[1].upper().replace('.', '')
info_text = f"{file_ext}{size_text}"
except:
info_text = "Unknown format"
size_label = Label(
text=info_text,
font_size=7,
color=(0.8, 0.8, 0.8, 1),
halign="left",
valign="bottom",
size_hint_y=0.4
)
size_label.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None)))
info_layout.add_widget(filename_label)
info_layout.add_widget(size_label)
# Button layout for actions
button_layout = BoxLayout(orientation='vertical', spacing=2, size_hint_x=0.28, padding=[2, 2, 2, 2])
# View button to show full image
view_btn = Button(
text="👁 View",
font_size=8,
background_color=(0.2, 0.6, 0.8, 1),
color=(1, 1, 1, 1),
size_hint_y=0.5
)
view_btn.bind(on_press=lambda x: self.view_full_image(img_path, img_file))
# Delete button
delete_btn = Button(
text="🗑 Del",
font_size=8,
background_color=(0.8, 0.2, 0.2, 1),
color=(1, 1, 1, 1),
size_hint_y=0.5
)
delete_btn.bind(on_press=lambda x: self.delete_single_photo(pause_idx, img_file, photo_item, parent_layout))
button_layout.add_widget(view_btn)
button_layout.add_widget(delete_btn)
# Add all components to photo item
photo_item.add_widget(image_container)
photo_item.add_widget(info_layout)
photo_item.add_widget(button_layout)
return photo_item
def save_pause_name(self, pause, name_input):
"""Save the custom name for a pause"""
pause['name'] = name_input.text
def save_individual_pause(self, pause_idx):
"""Save individual pause info"""
self.save_pauses()
# Show confirmation
self.show_message("Pause Saved", f"Pause {pause_idx + 1} information saved successfully!")
def add_photos(self, pause_idx):
"""Open file browser to add photos"""
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
pause_img_folder = os.path.join(project_folder, f"pause_{pause_idx+1}")
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
with layout.canvas.before:
Color(0.13, 0.13, 0.16, 1)
layout.bg_rect = Rectangle(pos=layout.pos, size=layout.size)
layout.bind(
pos=lambda inst, val: setattr(layout.bg_rect, 'pos', inst.pos),
size=lambda inst, val: setattr(layout.bg_rect, 'size', inst.size)
)
title_label = Label(
text=f"Select photos for Pause {pause_idx + 1}:",
color=(1, 1, 1, 1),
font_size=14,
size_hint_y=None,
height=30
)
filechooser = FileChooserIconView(
filters=['*.png', '*.jpg', '*.jpeg', '*.gif', '*.bmp'],
path=os.path.expanduser('~'),
multiselect=True
)
btn_layout = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=40)
add_btn = Button(
text="Add Selected",
background_color=(0.2, 0.7, 0.2, 1),
color=(1, 1, 1, 1),
font_size=12
)
cancel_btn = Button(
text="Cancel",
background_color=(0.6, 0.3, 0.3, 1),
color=(1, 1, 1, 1),
font_size=12
)
btn_layout.add_widget(cancel_btn)
btn_layout.add_widget(add_btn)
layout.add_widget(title_label)
layout.add_widget(filechooser)
layout.add_widget(btn_layout)
popup = Popup(
title="Add Photos",
content=layout,
size_hint=(0.95, 0.9),
auto_dismiss=False
)
def add_selected_files(instance):
if filechooser.selection:
for file_path in filechooser.selection:
if os.path.isfile(file_path):
filename = os.path.basename(file_path)
dest_path = os.path.join(pause_img_folder, filename)
if not os.path.exists(dest_path):
shutil.copy2(file_path, dest_path)
self.refresh_photos_display()
popup.dismiss()
else:
popup.dismiss()
add_btn.bind(on_press=add_selected_files)
cancel_btn.bind(on_press=lambda x: popup.dismiss())
popup.open()
def delete_single_photo(self, pause_idx, img_file, photo_item, parent_layout):
"""Delete a single photo with confirmation"""
def confirm_delete():
project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name)
pause_img_folder = os.path.join(project_folder, f"pause_{pause_idx+1}")
file_path = os.path.join(pause_img_folder, img_file)
if os.path.exists(file_path):
os.remove(file_path)
parent_layout.remove_widget(photo_item)
parent_layout.height = max(20, len(parent_layout.children) * 62) # Updated for new photo item height
self.show_confirmation(
f"Delete Photo",
f"Are you sure you want to delete '{img_file}'?",
confirm_delete
)
def refresh_photos_display(self):
"""Refresh the entire display to show updated photos"""
self.build_pause_layout()
def show_message(self, title, message):
"""Show a simple message popup"""
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
msg_label = Label(
text=message,
color=(1, 1, 1, 1),
font_size=12,
halign="center"
)
msg_label.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None)))
ok_btn = Button(
text="OK",
size_hint_y=None,
height=35,
background_color=(0.341, 0.235, 0.980, 1),
color=(1, 1, 1, 1)
)
layout.add_widget(msg_label)
layout.add_widget(ok_btn)
popup = Popup(title=title, content=layout, size_hint=(0.8, 0.4))
ok_btn.bind(on_press=lambda x: popup.dismiss())
popup.open()
def show_confirmation(self, title, message, confirm_callback):
"""Show a confirmation dialog"""
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
msg_label = Label(
text=message,
color=(1, 1, 1, 1),
font_size=12,
halign="center"
)
msg_label.bind(width=lambda instance, value: setattr(instance, 'text_size', (value, None)))
btn_layout = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=35)
cancel_btn = Button(
text="Cancel",
background_color=(0.6, 0.3, 0.3, 1),
color=(1, 1, 1, 1)
)
confirm_btn = Button(
text="Confirm",
background_color=(0.8, 0.2, 0.2, 1),
color=(1, 1, 1, 1)
)
btn_layout.add_widget(cancel_btn)
btn_layout.add_widget(confirm_btn)
layout.add_widget(msg_label)
layout.add_widget(btn_layout)
popup = Popup(title=title, content=layout, size_hint=(0.8, 0.4), auto_dismiss=False)
cancel_btn.bind(on_press=lambda x: popup.dismiss())
confirm_btn.bind(on_press=lambda x: (confirm_callback(), popup.dismiss()))
popup.open()
def save_all_and_close(self, instance):
"""Save all pauses and return to previous screen"""
self.save_pauses()
if self.on_save_callback:
self.on_save_callback()
self.go_back()
def go_back(self, instance=None):
"""Return to the previous screen"""
self.manager.current = "create_animation"
def set_project_and_callback(self, project_name, callback=None):
"""Set the project name and callback for this screen"""
self.project_name = project_name
self.on_save_callback = callback
def view_full_image(self, img_path, img_file):
"""Show full image in a popup"""
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
with layout.canvas.before:
Color(0.05, 0.05, 0.08, 1)
layout.bg_rect = Rectangle(pos=layout.pos, size=layout.size)
layout.bind(
pos=lambda inst, val: setattr(layout.bg_rect, 'pos', inst.pos),
size=lambda inst, val: setattr(layout.bg_rect, 'size', inst.size)
)
# Image display
try:
full_image = Image(
source=img_path,
allow_stretch=True,
keep_ratio=True
)
except Exception:
full_image = Label(
text="Unable to load image",
color=(1, 1, 1, 1),
font_size=16
)
# Close button
close_btn = Button(
text="Close",
size_hint_y=None,
height=40,
background_color=(0.341, 0.235, 0.980, 1),
color=(1, 1, 1, 1),
font_size=14
)
layout.add_widget(full_image)
layout.add_widget(close_btn)
popup = Popup(
title=f"Photo: {img_file}",
content=layout,
size_hint=(0.95, 0.95),
auto_dismiss=False
)
close_btn.bind(on_press=lambda x: popup.dismiss())
popup.open()
def find_nearby_pois(self, lat, lon, radius=100):
"""Find nearby points of interest within specified radius"""
try:
geolocator = Nominatim(user_agent="traccar_animation")
nearby_places = []
# Try different search approaches
search_strategies = [
# Strategy 1: Direct reverse geocoding with details
{"exactly_one": False, "addressdetails": True, "extratags": True},
# Strategy 2: Single result with extended details
{"exactly_one": True, "addressdetails": True, "extratags": True, "zoom": 18},
]
for strategy in search_strategies:
try:
results = geolocator.reverse(
(lat, lon),
radius=radius,
timeout=10,
**strategy
)
# Handle both single result and list of results
if not isinstance(results, list):
results = [results] if results else []
for result in results[:5]: # Check up to 5 results
if not result or not result.raw:
continue
# Extract place information
address = result.raw.get('address', {})
extratags = result.raw.get('extratags', {})
# Look for interesting places
place_candidates = []
# Check address fields for places
poi_fields = [
'amenity', 'shop', 'tourism', 'leisure', 'building',
'attraction', 'restaurant', 'cafe', 'hotel', 'hospital',
'school', 'bank', 'pharmacy', 'supermarket', 'museum'
]
for field in poi_fields:
if field in address and address[field]:
place_candidates.append(address[field])
# Check extratags for additional place info
if extratags:
for key in ['name', 'brand', 'operator']:
if key in extratags and extratags[key]:
place_candidates.append(extratags[key])
# Add valid candidates to nearby places
for candidate in place_candidates:
if (candidate and
candidate not in nearby_places and
len(candidate) > 2 and
not candidate.isdigit()):
nearby_places.append(candidate)
if len(nearby_places) >= 3:
break
if nearby_places:
break
except Exception:
continue
return nearby_places[:2] # Return top 2 POIs
except Exception:
return []