File size: 10,862 Bytes
faea948
 
 
 
59c425e
4687ad4
 
 
2a063fd
 
 
faea948
2a063fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4687ad4
 
 
 
 
 
 
 
 
2a063fd
 
 
4687ad4
 
 
 
 
 
 
 
 
 
 
 
2a063fd
 
4687ad4
 
2a063fd
4687ad4
 
faea948
4687ad4
faea948
2a063fd
 
 
 
faea948
 
 
2a063fd
 
faea948
4687ad4
faea948
4687ad4
 
 
 
 
 
 
 
2a063fd
 
 
4687ad4
 
faea948
2a063fd
 
4e34654
faea948
4e34654
 
 
2a063fd
4e34654
 
 
 
 
4687ad4
faea948
2a063fd
faea948
2a063fd
 
 
 
 
faea948
2a063fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
faea948
2a063fd
faea948
2a063fd
 
 
 
 
 
4687ad4
2a063fd
 
4687ad4
2a063fd
 
 
 
4687ad4
2a063fd
 
 
4687ad4
2a063fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4e34654
2a063fd
 
 
 
 
4687ad4
2a063fd
 
 
 
 
 
4687ad4
2a063fd
4e34654
 
 
4687ad4
4e34654
4687ad4
 
2a063fd
4687ad4
4e34654
2a063fd
 
4687ad4
 
2a063fd
4687ad4
 
 
2a063fd
 
faea948
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
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
import polyline
from typing import List, Tuple, Dict, Optional
import time

# Utility Functions
def create_numbered_marker(number: int) -> folium.features.DivIcon:
    """Create a numbered marker icon for the map"""
    return folium.features.DivIcon(
        html=f'<div style="background-color: white; border-radius: 50%; width: 25px; height: 25px; '
             f'display: flex; align-items: center; justify-content: center; border: 2px solid blue; '
             f'font-weight: bold;">{number}</div>'
    )

def get_place_coordinates(place_name: str) -> Optional[Tuple[float, float]]:
    """Get coordinates for a place using Nominatim geocoding"""
    try:
        geolocator = Nominatim(user_agent="travel_optimizer")
        location = geolocator.geocode(place_name)
        if location:
            return (location.latitude, location.longitude)
    except Exception as e:
        st.error(f"Error geocoding {place_name}: {str(e)}")
    return None

def format_instructions(steps: List[Dict]) -> List[str]:
    """Format OSRM route steps into readable instructions"""
    instructions = []
    for step in steps:
        if 'maneuver' in step:
            instruction = step.get('maneuver', {}).get('instruction', '')
            if instruction:
                # Add distance information if available
                if 'distance' in step:
                    distance_km = step['distance'] / 1000
                    if distance_km >= 1:
                        instruction += f" ({distance_km:.1f} km)"
                    else:
                        instruction += f" ({step['distance']:.0f} m)"
                instructions.append(instruction)
    return instructions

def get_route_from_osrm(coord1: Tuple[float, float], coord2: Tuple[float, float]) -> Optional[Dict]:
    """Get route information from OSRM"""
    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"
    }
    
    # Add rate limiting
    time.sleep(1)  # Respect OSRM usage guidelines
    
    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 Exception as e:
        st.warning(f"Error getting route: {str(e)}")
    return None

def calculate_real_distances(places: List[Tuple[str, Tuple[float, float]]]) -> Tuple[np.ndarray, Dict]:
    """Calculate real driving distances using OSRM"""
    n = len(places)
    distances = np.zeros((n, n))
    routing_info = {}
    
    progress_bar = st.progress(0)
    total_calcs = n * (n-1)
    current_calc = 0
    
    for i in range(n):
        for j in range(n):
            if i != j:
                origin = places[i][1]
                destination = places[j][1]
                
                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']
                    }
                
                current_calc += 1
                progress_bar.progress(current_calc / total_calcs)
    
    return distances, routing_info

def optimize_route(distances: np.ndarray) -> List[int]:
    """Optimize route using modified Hungarian algorithm for TSP"""
    n = len(distances)
    row_ind, col_ind = linear_sum_assignment(distances)
    
    # Convert assignment to tour
    tour = []
    current = 0
    for _ in range(n):
        tour.append(current)
        current = col_ind[current]
    
    return tour

def main():
    st.set_page_config(page_title="AI Travel Route Optimizer", layout="wide")
    
    st.title("🌍 AI Travel Route Optimizer")
    st.markdown("""
    This app helps you find the optimal route between multiple destinations using real driving distances.
    Enter your destinations in the sidebar and get a customized route with turn-by-turn directions.
    """)
    
    # Sidebar for inputs
    with st.sidebar:
        st.header("πŸ“ Destinations")
        
        # Input for places
        num_places = st.number_input("Number of destinations", min_value=2, max_value=10, value=3)
        
        places = []
        for i in range(num_places):
            place = st.text_input(f"Destination {i+1}")
            if place:
                with st.spinner(f"Finding coordinates for {place}..."):
                    coordinates = get_place_coordinates(place)
                    if coordinates:
                        places.append((place, coordinates))
                        st.success(f"βœ“ Found coordinates for {place}")
                    else:
                        st.error(f"❌ Couldn't find coordinates for {place}")
    
    # Main content area
    if len(places) == num_places and num_places >= 2:
        col1, col2 = st.columns([2, 1])
        
        with col1:
            with st.spinner("πŸš— Calculating optimal route..."):
                # Calculate real distances
                distances, routing_info = calculate_real_distances(places)
                
                # Get optimized route order
                optimal_order = 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)
                
                # Add markers and route lines
                total_distance = 0
                total_duration = 0
                
                for i in range(len(optimal_order)):
                    current_idx = optimal_order[i]
                    next_idx = optimal_order[(i + 1) % len(optimal_order)]
                    
                    current_place, (lat, lon) = places[current_idx]
                    segment_info = routing_info.get((current_idx, next_idx), {})
                    
                    # Add marker
                    popup_content = f"""
                    <b>Stop {i+1}: {current_place}</b><br>
                    """
                    if i < len(optimal_order) - 1:
                        popup_content += f"""
                        To next stop:<br>
                        Distance: {segment_info.get('distance_km', 'N/A')}<br>
                        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
                    if i < len(optimal_order) - 1:
                        if 'geometry' in segment_info:
                            try:
                                route_coords = polyline.decode(segment_info['geometry'])
                                folium.PolyLine(
                                    route_coords,
                                    weight=2,
                                    color='blue',
                                    opacity=0.8
                                ).add_to(m)
                            except Exception as e:
                                st.warning(f"Error drawing route: {str(e)}")
                                next_place = places[next_idx][1]
                                folium.PolyLine(
                                    [[lat, lon], [next_place[0], next_place[1]]],
                                    weight=2,
                                    color='red',
                                    opacity=0.8
                                ).add_to(m)
                        
                        # 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 map
                st.components.v1.html(m._repr_html_(), height=600)
        
        with col2:
            st.header("πŸ“ Optimized Itinerary")
            
            # Display summary first
            st.markdown("### πŸ“Š Summary")
            st.info(f"""
            πŸ›£οΈ Total distance: **{total_distance:.1f} km**  
            ⏱️ Total duration: **{total_duration:.0f} mins** ({total_duration/60:.1f} hours)
            """)
            
            st.markdown("### πŸ—ΊοΈ Route Details")
            for i in range(len(optimal_order)):
                current_idx = optimal_order[i]
                next_idx = optimal_order[(i + 1) % len(optimal_order)]
                
                current_place = places[current_idx][0]
                segment_info = routing_info.get((current_idx, next_idx), {})
                
                st.markdown(f"**Stop {i+1}: {current_place}**")
                
                if i < len(optimal_order) - 1:
                    st.markdown(f"↓ *{segment_info.get('distance_km', 'N/A')}, "
                              f"Duration: {segment_info.get('duration_mins', 'N/A')}*")
                    
                    if 'steps' in segment_info:
                        with st.expander("πŸ“ Turn-by-turn directions"):
                            instructions = format_instructions(segment_info['steps'])
                            for idx, instruction in enumerate(instructions, 1):
                                st.write(f"{idx}. {instruction}")
    else:
        st.info("πŸ‘ˆ Please enter all destinations in the sidebar to get started")

if __name__ == "__main__":
    main()