File size: 11,240 Bytes
faea948
 
 
2a063fd
 
fb17aa5
 
 
43efb9f
faea948
2a063fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4687ad4
 
 
 
 
 
 
 
 
2a063fd
 
 
4687ad4
 
 
 
 
 
 
 
 
 
 
 
2a063fd
 
4687ad4
 
fb17aa5
 
4687ad4
faea948
4687ad4
faea948
2a063fd
 
 
 
faea948
 
 
2a063fd
 
faea948
4687ad4
faea948
4687ad4
 
 
 
 
 
 
 
2a063fd
 
 
4687ad4
 
faea948
fb17aa5
 
4e34654
fb17aa5
 
 
4e34654
fb17aa5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4e34654
fb17aa5
4687ad4
b1621cd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
faea948
2a063fd
 
 
 
 
 
b1621cd
2a063fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b1621cd
2a063fd
b1621cd
2a063fd
 
 
fb17aa5
 
4687ad4
fb17aa5
 
4687ad4
b1621cd
 
fb17aa5
 
b1621cd
 
 
 
2a063fd
b1621cd
2a063fd
b1621cd
2a063fd
b1621cd
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
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
import streamlit as st
import folium
import numpy as np
from typing import List, Tuple, Dict, Optional
import time
from collections import deque
import requests
import polyline
from geopy.geocoders import Nominatim

# 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_distance_matrix(places: List[Tuple[str, Tuple[float, float]]]) -> Tuple[np.ndarray, Dict]:
    """Calculate distance matrix and routing information 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 dijkstra(distances: np.ndarray, start_idx: int) -> Tuple[List[int], Dict[int, float]]:
    """Implement Dijkstra's algorithm to find the optimal route"""
    n = len(distances)
    visited = [False] * n
    distances_to = [float('inf')] * n
    prev_node = [-1] * n
    
    distances_to[start_idx] = 0
    queue = deque([start_idx])
    
    while queue:
        current_node = queue.popleft()
        visited[current_node] = True
        
        for neighbor in range(n):
            if not visited[neighbor] and distances[current_node, neighbor] > 0:
                new_distance = distances_to[current_node] + distances[current_node, neighbor]
                if new_distance < distances_to[neighbor]:
                    distances_to[neighbor] = new_distance
                    prev_node[neighbor] = current_node
                    queue.append(neighbor)
    
    # Reconstruct the optimal route
    optimal_order = []
    node = n - 1
    while prev_node[node] != -1:
        optimal_order.insert(0, node)
        node = prev_node[node]
    optimal_order.insert(0, start_idx)
    
    return optimal_order, distances_to

def add_markers_and_route(m: folium.Map, places: List[Tuple[str, Tuple[float, float]]], optimal_order: List[int], routing_info: Dict):
    """Add markers and route lines to the map"""
    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])
    
    return total_distance, total_duration

def display_itinerary_summary(places: List[Tuple[str, Tuple[float, float]]], optimal_order: List[int], routing_info: Dict):
    """Display the optimized itinerary summary"""
    # Display summary first
    st.markdown("### πŸ“Š Summary")
    total_distance, total_duration = add_markers_and_route(folium.Map(), places, optimal_order, routing_info)
    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}")

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")
        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) >= 2:
        col1, col2 = st.columns([2, 1])
        with col1:
            with st.spinner("πŸš— Calculating optimal route..."):
                # Calculate distance matrix
                distances, routing_info = calculate_distance_matrix(places)
                
                # Get optimized route order using Dijkstra's algorithm
                optimal_order, _ = dijkstra(distances, 0)
                
                # Update the route if the user changes the input
                if st.button("Recalculate Route"):
                    distances, routing_info = calculate_distance_matrix(places)
                    optimal_order, _ = dijkstra(distances, 0)

                # Create map and display
                m = folium.Map()
                total_distance, total_duration = add_markers_and_route(m, places, optimal_order, routing_info)
                st.components.v1.html(m._repr_html_(), height=600)

        with col2:
            display_itinerary_summary(places, optimal_order, routing_info)
    else:
        st.info("πŸ‘ˆ Please enter at least 2 destinations in the sidebar to get started")

if __name__ == "__main__":
    main()