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()