import streamlit as st import folium import numpy as np from scipy.optimize import linear_sum_assignment from folium import plugins import requests from datetime import datetime from geopy.geocoders import Nominatim def get_route_from_osrm(coord1, coord2): """Get route information from OSRM""" # OSRM expects coordinates in format: longitude,latitude url = f"http://router.project-osrm.org/route/v1/driving/{coord1[1]},{coord1[0]};{coord2[1]},{coord2[0]}" params = { "overview": "full", "geometries": "polyline", "steps": "true", "annotations": "true" } try: response = requests.get(url, params=params) if response.status_code == 200: data = response.json() if data["code"] == "Ok" and len(data["routes"]) > 0: route = data["routes"][0] return { 'distance': route['distance'] / 1000, # Convert to km 'duration': route['duration'] / 60, # Convert to minutes 'geometry': route['geometry'], 'steps': route['legs'][0]['steps'] } except: pass return None def calculate_real_distances(places): """Calculate real driving distances using OSRM""" n = len(places) distances = np.zeros((n, n)) routing_info = {} for i in range(n): for j in range(n): if i != j: origin = places[i][1] # coordinates destination = places[j][1] # coordinates route_data = get_route_from_osrm(origin, destination) if route_data: distances[i,j] = route_data['distance'] routing_info[(i,j)] = { 'distance_km': f"{route_data['distance']:.1f} km", 'duration_mins': f"{route_data['duration']:.0f} mins", 'geometry': route_data['geometry'], 'steps': route_data['steps'] } return distances, routing_info def optimize_route(distances): """Optimize route using Hungarian algorithm""" row_ind, col_ind = linear_sum_assignment(distances) return col_ind def get_place_coordinates(place_name): """Get coordinates using Nominatim geocoding""" try: geolocator = Nominatim(user_agent="my_travel_app") location = geolocator.geocode(place_name) return (location.latitude, location.longitude) if location else None except: return None def create_numbered_marker(number): """Create a custom marker with a number""" return plugins.BeautifyIcon( number=number, border_color='#b4b4b4', border_width=1, text_color='black', background_color='white', inner_icon_style='margin-top:0;' ) def format_instructions(steps): """Format routing instructions from OSRM steps""" instructions = [] for step in steps: if 'maneuver' in step: instruction = step.get('maneuver', {}).get('instruction', '') if instruction: instructions.append(instruction) return instructions def main(): st.title("AI Travel Route Optimizer (with OSRM)") # Sidebar for inputs st.sidebar.header("Travel Parameters") # Input for places places = [] num_places = st.sidebar.number_input("Number of places to visit", min_value=2, max_value=10, value=3) for i in range(num_places): place = st.sidebar.text_input(f"Place {i+1}") if place: coordinates = get_place_coordinates(place) if coordinates: places.append((place, coordinates)) else: st.error(f"Couldn't find coordinates for {place}") # Only proceed if we have all places if len(places) == num_places and num_places >= 2: with st.spinner("Calculating optimal route..."): # Calculate real distances distances, routing_info = calculate_real_distances(places) # Optimize route optimized_indices = optimize_route(distances) # Create map center_lat = sum(coord[0] for _, coord in places) / len(places) center_lon = sum(coord[1] for _, coord in places) / len(places) m = folium.Map(location=[center_lat, center_lon], zoom_start=4) # Create optimized itinerary list optimized_places = [(i+1, places[idx][0]) for i, idx in enumerate(optimized_indices)] # Add markers and route lines for i in range(len(optimized_indices)): current_idx = optimized_indices[i] next_idx = optimized_indices[(i + 1) % len(optimized_indices)] # Add numbered marker place_name, (lat, lon) = places[current_idx] next_place = places[next_idx] # Get routing info for this segment segment_info = routing_info.get((current_idx, next_idx), {}) # Create detailed popup content popup_content = f""" Stop {i+1}: {place_name}
To next stop:
Distance: {segment_info.get('distance_km', 'N/A')}
Duration: {segment_info.get('duration_mins', 'N/A')} """ folium.Marker( [lat, lon], popup=folium.Popup(popup_content, max_width=300), icon=create_numbered_marker(i+1) ).add_to(m) # Draw route line using polyline from OSRM if 'geometry' in segment_info: try: import polyline route_coords = polyline.decode(segment_info['geometry']) folium.PolyLine( route_coords, weight=2, color='blue', opacity=0.8 ).add_to(m) except: # Fallback to straight line if polyline decoding fails folium.PolyLine( [[lat, lon], [next_place[1][0], next_place[1][1]]], weight=2, color='red', opacity=0.8 ).add_to(m) # Display map st.components.v1.html(m._repr_html_(), height=600) # Display optimized itinerary with route information st.header("Optimized Itinerary") total_distance = 0 total_duration = 0 for i in range(len(optimized_indices)): current_idx = optimized_indices[i] next_idx = optimized_indices[(i + 1) % len(optimized_indices)] place_name = places[current_idx][0] segment_info = routing_info.get((current_idx, next_idx), {}) st.write(f"{i+1}. {place_name}") if i < len(optimized_indices) - 1: st.write(f" ↓ ({segment_info.get('distance_km', 'N/A')}, " f"Duration: {segment_info.get('duration_mins', 'N/A')})") # Display turn-by-turn instructions if 'steps' in segment_info: with st.expander(f"Route instructions to next stop"): instructions = format_instructions(segment_info['steps']) for idx, instruction in enumerate(instructions, 1): st.write(f"{idx}. {instruction}") # Add to totals if 'distance_km' in segment_info: total_distance += float(segment_info['distance_km'].split()[0]) if 'duration_mins' in segment_info: total_duration += float(segment_info['duration_mins'].split()[0]) # Display totals st.write("\nSummary:") st.write(f"Total distance: {total_distance:.1f} km") st.write(f"Total estimated duration: {total_duration:.0f} minutes ({total_duration/60:.1f} hours)") if __name__ == "__main__": main()