vincentiusyoshuac commited on
Commit
0e240e7
Β·
verified Β·
1 Parent(s): 0dd6310

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +222 -126
app.py CHANGED
@@ -1,143 +1,239 @@
1
  import streamlit as st
2
  import folium
3
  from geopy.geocoders import Nominatim
4
- from geopy.distance import geodesic
5
  from itertools import combinations
6
  import numpy as np
7
-
8
- # Algoritma Held-Karp untuk TSP
9
- def held_karp_tsp(dist_matrix):
10
- if len(dist_matrix) < 2:
11
- return 0, []
12
-
13
- n = len(dist_matrix)
14
- inf = float('inf')
15
- memo = {}
16
-
17
- # Initialize memo table for subsets of size 2
18
- for i in range(1, n):
19
- memo[(1 << i, i)] = dist_matrix[0][i]
20
-
21
- # Fill memo table for larger subsets
22
- for subset_size in range(2, n):
23
- for subset in combinations(range(1, n), subset_size):
24
- bits = 0
25
- for bit in subset:
26
- bits |= 1 << bit
27
- for next_city in subset:
28
- prev_bits = bits & ~(1 << next_city)
29
- if (prev_bits, next_city) in memo:
30
- min_dist = memo[(prev_bits, next_city)] + dist_matrix[next_city][0]
31
- memo[(bits, next_city)] = min_dist
32
-
33
- # Find optimal tour
34
- bits = (2 ** n - 1) - 1
35
- min_tour_cost = inf
36
- last_city = None
37
- for k in range(1, n):
38
- if (bits, k) in memo:
39
- tour_cost = memo[(bits, k)] + dist_matrix[k][0]
40
- if tour_cost < min_tour_cost:
41
- min_tour_cost = tour_cost
42
- last_city = k
43
-
44
- # Backtrack to find the full tour
45
- tour = [0]
46
- bits = (2 ** n - 1) - 1
47
- for _ in range(n - 1):
48
- if last_city is not None:
49
- tour.append(last_city)
50
- bits &= ~(1 << last_city)
51
- if bits == 0:
52
- break
53
- next_city = min(
54
- [(memo[(bits, k)] + dist_matrix[k][last_city], k) for k in range(n) if (bits, k) in memo],
55
- key=lambda x: x[0],
56
- )[1]
57
- last_city = next_city
58
  else:
59
- break
60
-
61
- if len(tour) == n + 1:
62
- return min_tour_cost, tour
63
- else:
64
- return inf, []
65
-
66
- # Mendapatkan koordinat kota dari nama
67
- def get_coordinates(city_name):
68
- geolocator = Nominatim(user_agent="tsp_app")
69
- location = geolocator.geocode(city_name)
70
- if location:
71
- return (location.latitude, location.longitude)
72
- else:
 
 
 
 
73
  return None
74
 
75
- # Menghitung jarak antar semua pasangan kota
76
- def create_distance_matrix(coordinates):
 
 
77
  valid_coordinates = [c for c in coordinates if c is not None]
78
  n = len(valid_coordinates)
 
79
  if n < 2:
80
- return np.array([]), valid_coordinates
 
81
  dist_matrix = np.zeros((n, n))
82
- for i in range(n):
83
- for j in range(i + 1, n):
84
- dist_matrix[i][j] = geodesic(valid_coordinates[i], valid_coordinates[j]).kilometers
85
- dist_matrix[j][i] = dist_matrix[i][j]
86
- return dist_matrix, valid_coordinates
87
-
88
- # Fungsi untuk menampilkan peta rute
89
- def plot_route(map_obj, coordinates, route):
90
- for i in range(len(route) - 1):
91
- start = coordinates[route[i]]
92
- end = coordinates[route[i + 1]]
93
- folium.Marker(location=start, tooltip=f'City {route[i] + 1}').add_to(map_obj)
94
- folium.Marker(location=end, tooltip=f'City {route[i + 1] + 1}').add_to(map_obj)
95
- folium.PolyLine([start, end], color="blue", weight=2.5, opacity=1).add_to(map_obj)
96
-
97
- # Add markers for start and end points
98
- folium.Marker(location=coordinates[0], tooltip='Start', icon=folium.Icon(color="green")).add_to(map_obj)
99
- folium.Marker(location=coordinates[route[-2]], tooltip='End', icon=folium.Icon(color="red")).add_to(map_obj)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  return map_obj
101
 
102
- # Streamlit UI
103
- st.title("Traveling Salesman Problem Solver")
104
-
105
- # Create map
106
- map_obj = folium.Map(location=[0, 0], zoom_start=2)
107
-
108
- # Input kota
109
- st.subheader("Pilih Kota")
110
- city_count = st.number_input("Jumlah kota", min_value=2, step=1)
111
- city_names = []
112
- city_coords = []
113
- for i in range(int(city_count)):
114
- city_name = st.text_input(f"Kota {i+1}", key=f"city_{i}")
115
- city_coords.append(get_coordinates(city_name))
116
- if city_coords[-1] is None:
117
- st.warning(f"Kota '{city_name}' tidak ditemukan. Harap periksa ejaan dan coba lagi.")
118
- else:
119
- city_names.append(city_name)
120
-
121
- if st.button("Optimasi Rute"):
122
- dist_matrix, valid_coordinates = create_distance_matrix(city_coords)
123
-
124
- if len(valid_coordinates) < 2:
125
- st.error("Tidak cukup kota yang valid untuk dioptimalkan.")
126
- else:
127
- # Hitung rute optimal
128
- min_cost, optimal_route = held_karp_tsp(dist_matrix)
129
 
130
- # Tampilkan hasil
131
- if min_cost == float('inf'):
132
- st.write("Tidak ada rute optimal yang dapat ditemukan.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  else:
134
- st.write(f"Biaya total minimum: {min_cost:.2f} km")
135
- st.write("Rute optimal:", " -> ".join([city_names[i] for i in optimal_route]))
136
-
137
- # Buat peta
138
- map_obj = folium.Map(location=valid_coordinates[0], zoom_start=5)
139
- plot_route(map_obj, valid_coordinates, optimal_route)
140
 
141
- # Render map
142
- st.markdown("### Rute Optimal")
143
- st_folium = st.components.v1.html(folium.Map._repr_html_(map_obj), width=800, height=600)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
  import folium
3
  from geopy.geocoders import Nominatim
 
4
  from itertools import combinations
5
  import numpy as np
6
+ import requests
7
+ import polyline
8
+ import time
9
+ from functools import lru_cache
10
+ import json
11
+ from concurrent.futures import ThreadPoolExecutor
12
+
13
+ @st.cache_data
14
+ def get_route_osrm(start_coords: tuple, end_coords: tuple) -> tuple:
15
+ """
16
+ Get route using OSRM public API
17
+ Returns: (distance in km, encoded polyline of the route)
18
+ """
19
+ try:
20
+ # Format coordinates for OSRM (lon,lat format)
21
+ coords = f"{start_coords[1]},{start_coords[0]};{end_coords[1]},{end_coords[0]}"
22
+
23
+ # OSRM public API endpoint
24
+ url = f"http://router.project-osrm.org/route/v1/driving/{coords}"
25
+ params = {
26
+ "overview": "full",
27
+ "geometries": "polyline",
28
+ "annotations": "distance"
29
+ }
30
+
31
+ response = requests.get(url, params=params)
32
+
33
+ if response.status_code == 200:
34
+ data = response.json()
35
+ if data["code"] == "Ok" and len(data["routes"]) > 0:
36
+ route = data["routes"][0]
37
+ distance = route["distance"] / 1000 # Convert to km
38
+ geometry = route["geometry"] # Already encoded polyline
39
+ return distance, geometry
40
+ else:
41
+ st.warning("No route found")
42
+ return None, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  else:
44
+ st.warning(f"OSRM API error: {response.status_code}")
45
+ return None, None
46
+
47
+ except Exception as e:
48
+ st.error(f"Error getting route: {str(e)}")
49
+ return None, None
50
+
51
+ @st.cache_data
52
+ def cached_geocoding(city_name: str) -> tuple:
53
+ """Cache geocoding results"""
54
+ try:
55
+ geolocator = Nominatim(user_agent="tsp_app")
56
+ location = geolocator.geocode(city_name, timeout=10)
57
+ if location:
58
+ return (location.latitude, location.longitude)
59
+ return None
60
+ except Exception as e:
61
+ st.error(f"Error geocoding {city_name}: {str(e)}")
62
  return None
63
 
64
+ def create_distance_matrix_with_routes(coordinates: list) -> tuple:
65
+ """
66
+ Create distance matrix and store routes between all points using OSRM
67
+ """
68
  valid_coordinates = [c for c in coordinates if c is not None]
69
  n = len(valid_coordinates)
70
+
71
  if n < 2:
72
+ return np.array([]), valid_coordinates, {}
73
+
74
  dist_matrix = np.zeros((n, n))
75
+ routes_dict = {} # Store encoded polylines for routes
76
+
77
+ def calculate_route(i, j):
78
+ if i != j:
79
+ # Add delay to avoid hitting rate limits
80
+ time.sleep(0.1)
81
+ distance, route = get_route_osrm(valid_coordinates[i], valid_coordinates[j])
82
+ if distance is not None:
83
+ return i, j, distance, route
84
+ return i, j, 0, None
85
+
86
+ with ThreadPoolExecutor(max_workers=5) as executor: # Limit concurrent requests
87
+ futures = []
88
+ for i in range(n):
89
+ for j in range(i + 1, n):
90
+ futures.append(executor.submit(calculate_route, i, j))
91
+
92
+ with st.spinner("Calculating routes..."):
93
+ for future in futures:
94
+ i, j, distance, route = future.result()
95
+ if route is not None:
96
+ dist_matrix[i][j] = distance
97
+ dist_matrix[j][i] = distance
98
+ routes_dict[f"{i}-{j}"] = route
99
+ routes_dict[f"{j}-{i}"] = route
100
+
101
+ return dist_matrix, valid_coordinates, routes_dict
102
+
103
+ def plot_route_with_roads(map_obj: folium.Map, coordinates: list, route: list,
104
+ city_names: list, routes_dict: dict) -> folium.Map:
105
+ """
106
+ Plot route using actual road paths from OSRM
107
+ """
108
+ route_group = folium.FeatureGroup(name="Route")
109
+
110
+ # Plot road segments between consecutive points
111
+ total_distance = 0
112
+ for i in range(len(route)-1):
113
+ start_idx = route[i]
114
+ end_idx = route[i+1]
115
+ route_key = f"{start_idx}-{end_idx}"
116
+
117
+ if route_key in routes_dict:
118
+ # Decode and plot the polyline
119
+ decoded_path = polyline.decode(routes_dict[route_key])
120
+ folium.PolyLine(
121
+ decoded_path,
122
+ color="blue",
123
+ weight=3,
124
+ opacity=0.8,
125
+ tooltip=f"Segment {i+1}: {city_names[start_idx]} β†’ {city_names[end_idx]}"
126
+ ).add_to(route_group)
127
+
128
+ # Add markers with custom icons
129
+ for i, point_idx in enumerate(route):
130
+ icon_color = "green" if i == 0 else "red" if i == len(route)-1 else "blue"
131
+ popup_text = f"""
132
+ <div style='font-size: 12px'>
133
+ <b>City:</b> {city_names[point_idx]}<br>
134
+ <b>Stop:</b> {i + 1} of {len(route)}
135
+ </div>
136
+ """
137
+ folium.Marker(
138
+ location=coordinates[point_idx],
139
+ popup=folium.Popup(popup_text, max_width=200),
140
+ tooltip=f'Stop {i + 1}: {city_names[point_idx]}',
141
+ icon=folium.Icon(color=icon_color, icon='info-sign')
142
+ ).add_to(route_group)
143
+
144
+ route_group.add_to(map_obj)
145
+
146
+ # Add OpenStreetMap tile layer
147
+ folium.TileLayer(
148
+ tiles='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
149
+ attr='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
150
+ ).add_to(map_obj)
151
+
152
  return map_obj
153
 
154
+ def main():
155
+ st.set_page_config(page_title="TSP Solver with OSRM", layout="wide")
156
+
157
+ st.title("🌍 TSP Route Optimizer")
158
+ st.markdown("""
159
+ Find the optimal driving route between multiple cities using OSRM.
160
+ Enter city names below and click 'Optimize Route' to see the results.
161
+ """)
162
+
163
+ col1, col2 = st.columns([1, 2])
164
+
165
+ with col1:
166
+ st.subheader("πŸ“ Enter Cities")
167
+ city_count = st.number_input("Number of cities", min_value=2, max_value=10, value=3, step=1,
168
+ help="Maximum 10 cities recommended due to API limits")
169
+
170
+ if 'city_inputs' not in st.session_state:
171
+ st.session_state.city_inputs = [''] * city_count
172
+
173
+ if len(st.session_state.city_inputs) != city_count:
174
+ st.session_state.city_inputs = st.session_state.city_inputs[:city_count] + [''] * max(0, city_count - len(st.session_state.city_inputs))
175
+
176
+ city_names = []
177
+ city_coords = []
 
 
 
178
 
179
+ for i in range(city_count):
180
+ city_name = st.text_input(
181
+ f"City {i+1}",
182
+ value=st.session_state.city_inputs[i],
183
+ key=f"city_{i}"
184
+ )
185
+ st.session_state.city_inputs[i] = city_name
186
+
187
+ if city_name:
188
+ coords = cached_geocoding(city_name)
189
+ if coords:
190
+ city_names.append(city_name)
191
+ city_coords.append(coords)
192
+ else:
193
+ st.warning(f"⚠️ Could not find coordinates for '{city_name}'")
194
+
195
+ with col2:
196
+ if not city_coords:
197
+ map_center = [-2.548926, 118.014863] # Center of Indonesia
198
+ zoom_start = 5
199
  else:
200
+ map_center = city_coords[0]
201
+ zoom_start = 5
 
 
 
 
202
 
203
+ map_obj = folium.Map(location=map_center, zoom_start=zoom_start)
204
+
205
+ if st.button("πŸ”„ Optimize Route", key="optimize"):
206
+ if len(city_coords) < 2:
207
+ st.error("❌ Please enter at least 2 valid cities")
208
+ else:
209
+ with st.spinner("Calculating optimal route..."):
210
+ start_time = time.time()
211
+
212
+ # Get distance matrix and routes
213
+ dist_matrix, valid_coordinates, routes_dict = create_distance_matrix_with_routes(city_coords)
214
+
215
+ # Calculate optimal route
216
+ from held_karp_tsp import held_karp_tsp # Menggunakan fungsi yang sudah ada
217
+ min_cost, optimal_route = held_karp_tsp(dist_matrix)
218
+
219
+ end_time = time.time()
220
+
221
+ if min_cost == float('inf'):
222
+ st.error("❌ Could not find a valid route")
223
+ else:
224
+ # Display results
225
+ st.success(f"βœ… Route calculated in {end_time - start_time:.2f} seconds")
226
+ st.write(f"πŸ›£οΈ Total driving distance: {min_cost:.2f} km")
227
+ st.write("πŸ“ Optimal route:")
228
+ route_text = " β†’ ".join([city_names[i] for i in optimal_route])
229
+ st.code(route_text)
230
+
231
+ # Update map with route
232
+ map_obj = plot_route_with_roads(map_obj, valid_coordinates, optimal_route,
233
+ city_names, routes_dict)
234
+
235
+ # Display map
236
+ st.components.v1.html(folium.Map._repr_html_(map_obj), height=600)
237
+
238
+ if __name__ == "__main__":
239
+ main()