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 []