import json import matplotlib.pyplot as plt import polyline from PIL import Image from io import BytesIO from geopy import distance from openrouteservice.exceptions import ApiError from ors_client import get_ors_client, is_ors_configured from route_utils import ( generate_route_image_file, load_image_with_title, geocode_address, calculate_route_distance_km, calculate_route_time_minutes, get_poi_data ) def get_coords_from_address(address: str) -> str: """ Converts a street address into latitude and longitude coordinates. Args: address (str): The address to search for (e.g., "Eiffel Tower, Paris"). Returns: str: A formatted string with the coordinates "Lat: XX.XXXX, Lon: YY.YYYY" or an error message if the address is not found. """ try: coords = geocode_address(address) if coords: lat, lon = coords return f"Lat: {lat}, Lon: {lon}" else: return "Address not found. Please try being more specific. E.g., '1600 Amphitheatre Parkway, Mountain View, CA'" except Exception as e: print(f"An error occurred: {e}") return "An error occurred while trying to contact the geocoding service." def calculate_direct_distance(lat1: float, lon1: float, lat2: float, lon2: float, unit: str = "km") -> str: """ Calculates the distance between two points on the Earth's surface using the Haversine formula. Args: lat1 (float): Latitude of the first point. lon1 (float): Longitude of the first point. lat2 (float): Latitude of the second point. lon2 (float): Longitude of the second point. unit (str, optional): Unit of measurement for the distance. Default is "km". Returns: str: The distance between the two points in kilometers. """ print("calculate_distance", lat1, lon1, lat2, lon2, unit) if unit == "km": return str(round(distance.distance((lat1, lon1), (lat2, lon2)).km, 2)) else: return str(round(distance.distance((lat1, lon1), (lat2, lon2)).miles, 2)) # This is now a standard, synchronous function def get_route_data(start_lat: float, start_lon: float, end_lat: float, end_lon: float, mode: str) -> str: """ Fetches optimized route data from the OpenRouteService API for a given start/end point and travel mode. This is the primary data-gathering tool. Its output is a compact JSON string meant to be used by other tools. Args: start_lat (float): Latitude of the starting point. start_lon (float): Longitude of the starting point. end_lat (float): Latitude of the ending point. end_lon (float): Longitude of the ending point. mode (str): The mode of transportation (e.g., "car", "walk", "bike"). Returns: A compact JSON string containing optimized route details with decoded coordinates. """ profile_map = { "car": "driving-car", "walk": "foot-walking", "bike": "cycling-road" } if mode not in profile_map: return json.dumps({"error": "Invalid mode. Please use 'car', 'walk', or 'bike'."}) if not is_ors_configured(): return json.dumps({"error": "ORS API key not configured"}) client_ors = get_ors_client() coords = ((start_lon, start_lat), (end_lon, end_lat)) try: routes = client_ors.directions(coordinates=coords, profile=profile_map[mode], geometry='true') route_data = routes['routes'][0] # Decode full polyline geometry for image generation decoded_coords = polyline.decode(route_data['geometry']) # Generate route image with full detail image_path = generate_route_image_file(decoded_coords, start_lat, start_lon, end_lat, end_lon) # Return optimized JSON with image path optimized_data = { "summary": route_data['summary'], "map_image_path": image_path, "start_point": [start_lat, start_lon], "end_point": [end_lat, end_lon] } return json.dumps(optimized_data) except ApiError as e: return json.dumps({"error": f"Could not find a route. API Error: {e}"}) except Exception as e: return json.dumps({"error": f"An unexpected error occurred: {e}"}) def extract_route_time(route_json: str) -> str: """ Extract travel time from route data. Args: route_json (str): JSON string containing route data Returns: Time in human format like '15 min' or '1 h 23 min' """ data = json.loads(route_json) if "error" in data: return data["error"] minutes = calculate_route_time_minutes(data) if minutes is None: return "Error calculating time" if minutes < 60: return f"{minutes} min" else: hours = minutes // 60 mins = minutes % 60 return f"{hours} h {mins} min" def extract_route_distance(route_json: str) -> str: """ Extract distance from route data. Args: route_json (str): JSON string containing route data Returns: Distance like '5.2 km' """ data = json.loads(route_json) if "error" in data: return data["error"] km = calculate_route_distance_km(data) if km is None: return "Error calculating distance" return f"{km} km" def generate_route_image(route_json: str, custom_title: str = None) -> Image.Image: """ Extract route image from JSON data and optionally add custom title. Args: route_json (str): JSON string containing route data custom_title (str): Optional title to add to the image Returns: PIL.Image.Image: The generated route image """ data = json.loads(route_json) if "error" in data: # Create error image fig, ax = plt.subplots(1, 1, figsize=(8, 6)) ax.text(0.5, 0.5, f"Error: {data['error']}", ha='center', va='center', fontsize=12, color='red') ax.axis('off') buf = BytesIO() fig.savefig(buf, format='png', dpi=150, bbox_inches='tight') buf.seek(0) img = Image.open(buf) plt.close(fig) return img # Load image from path and add title if provided image_path = data['map_image_path'] return load_image_with_title(image_path, custom_title) def get_points_of_interest(lat: float, lon: float, radius_km: float = 10.0, categories: list = None) -> str: """ Find points of interest near given coordinates. Increase radius if not found (10km is default), try with more categories if not found any poi Args: lat (float): Latitude of the center point lon (float): Longitude of the center point radius_km (float): Search radius in kilometers (default 10.0), is recommended to be 10km categories (str): Comma-separated POI categories (e.g., "sustenance,tourism"), use many categories to find more poi Returns: str: Formatted text with POI information """ radius_m = int(radius_km * 1000) category_list = categories if categories else None poi_data = get_poi_data(lat, lon, radius_m, category_list) if "error" in poi_data: return f"Error: {poi_data['error']}" if "features" not in poi_data or not poi_data["features"]: return f"No points of interest found within {radius_km} km of the location." pois = poi_data["features"] result = f"Found {len(pois)} points of interest within {radius_km} km:\n\n" for poi in pois: props = poi.get("properties", {}) geom = poi.get("geometry", {}).get("coordinates", []) name = props.get("osm_tags", {}).get("name", "Unnamed location") category = props.get("category_ids", {}) category_name = next(iter(category.values())) if category else "Unknown" if geom and len(geom) >= 2: poi_lon, poi_lat = geom[0], geom[1] result += f"• {name} ({category_name})\n" result += f" Location: {poi_lat:.4f}, {poi_lon:.4f}\n" # Add additional details if available tags = props.get("osm_tags", {}) if "addr:street" in tags: result += f" Address: {tags.get('addr:street', '')}\n" if "phone" in tags: result += f" Phone: {tags['phone']}\n" if "website" in tags: result += f" Website: {tags['website']}\n" result += "\n" return result.strip()