1963 lines
75 KiB
Python
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 []
|