TravelWander / app.py
vincentiusyoshuac's picture
Update app.py
db26f98 verified
import streamlit as st
import folium
from geopy.geocoders import Nominatim
import numpy as np
import requests
import polyline
import time
from functools import lru_cache
from concurrent.futures import ThreadPoolExecutor
def held_karp_tsp_fixed_start(dist_matrix: np.ndarray, return_to_start: bool = True) -> tuple:
"""
Modified Held-Karp algorithm for solving TSP with fixed starting point (index 0)
Returns: (minimum cost, optimal route)
"""
if len(dist_matrix) < 2:
return 0, []
n = len(dist_matrix)
inf = float('inf')
dp = np.full((1 << n, n), inf)
parent = np.full((1 << n, n), -1, dtype=int)
for i in range(1, n):
dp[1 << i | 1][i] = dist_matrix[0][i]
for mask in range(1, 1 << n):
if not (mask & 1):
continue
for curr in range(n):
if not (mask & (1 << curr)):
continue
prev_mask = mask ^ (1 << curr)
if not prev_mask:
continue
for prev in range(n):
if not (prev_mask & (1 << prev)):
continue
candidate = dp[prev_mask][prev] + dist_matrix[prev][curr]
if candidate < dp[mask][curr]:
dp[mask][curr] = candidate
parent[mask][curr] = prev
mask = (1 << n) - 1
if return_to_start:
curr = min(range(1, n), key=lambda x: dp[mask][x] + dist_matrix[x][0] if dp[mask][x] != inf else inf)
if dp[mask][curr] == inf:
return inf, []
final_cost = dp[mask][curr] + dist_matrix[curr][0]
else:
curr = min(range(1, n), key=lambda x: dp[mask][x] if dp[mask][x] != inf else inf)
if dp[mask][curr] == inf:
return inf, []
final_cost = dp[mask][curr]
path = []
while curr != -1:
path.append(curr)
new_mask = mask ^ (1 << curr)
curr = parent[mask][curr]
mask = new_mask
path.append(0)
path.reverse()
if return_to_start:
path.append(0)
return final_cost, path
@st.cache_data
def get_route_osrm(start_coords: tuple, end_coords: tuple) -> tuple:
"""Get route using OSRM public API"""
try:
coords = f"{start_coords[1]},{start_coords[0]};{end_coords[1]},{end_coords[0]}"
url = f"http://router.project-osrm.org/route/v1/driving/{coords}"
params = {
"overview": "full",
"geometries": "polyline",
"annotations": "distance"
}
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]
distance = route["distance"] / 1000
geometry = route["geometry"]
return distance, geometry
else:
st.warning("No route found")
return None, None
else:
st.warning(f"OSRM API error: {response.status_code}")
return None, None
except Exception as e:
st.error(f"Error getting route: {str(e)}")
return None, None
@st.cache_data
def cached_geocoding(city_name: str) -> tuple:
"""Cache geocoding results"""
try:
geolocator = Nominatim(user_agent="tsp_app")
location = geolocator.geocode(city_name, timeout=10)
if location:
return (location.latitude, location.longitude)
return None
except Exception as e:
st.error(f"Error geocoding {city_name}: {str(e)}")
return None
def create_distance_matrix_with_routes(coordinates: list) -> tuple:
"""Create distance matrix and store routes between all points"""
valid_coordinates = [c for c in coordinates if c is not None]
n = len(valid_coordinates)
if n < 2:
return np.array([]), valid_coordinates, {}
dist_matrix = np.zeros((n, n))
routes_dict = {}
def calculate_route(i, j):
if i != j:
time.sleep(0.1)
distance, route = get_route_osrm(valid_coordinates[i], valid_coordinates[j])
if distance is not None:
return i, j, distance, route
return i, j, 0, None
with ThreadPoolExecutor(max_workers=5) as executor:
futures = []
for i in range(n):
for j in range(i + 1, n):
futures.append(executor.submit(calculate_route, i, j))
with st.spinner("Calculating routes..."):
for future in futures:
i, j, distance, route = future.result()
if route is not None:
dist_matrix[i][j] = distance
dist_matrix[j][i] = distance
routes_dict[f"{i}-{j}"] = route
routes_dict[f"{j}-{i}"] = route
return dist_matrix, valid_coordinates, routes_dict
def plot_route_with_roads(map_obj: folium.Map, coordinates: list, route: list,
city_names: list, routes_dict: dict, return_to_start: bool) -> folium.Map:
"""Plot route using actual road paths from OSRM"""
route_group = folium.FeatureGroup(name="Route")
for i in range(len(route)-1):
start_idx = route[i]
end_idx = route[i+1]
route_key = f"{start_idx}-{end_idx}"
if route_key in routes_dict:
decoded_path = polyline.decode(routes_dict[route_key])
segment_label = (f"Return to {city_names[end_idx]}" if i == len(route)-2 and return_to_start
else f"Segment {i+1}: {city_names[start_idx]} β†’ {city_names[end_idx]}")
folium.PolyLine(
decoded_path,
color="blue",
weight=3,
opacity=0.8,
tooltip=segment_label
).add_to(route_group)
for i, point_idx in enumerate(route):
if return_to_start and i == len(route) - 1:
continue
is_start = i == 0
is_end = (i == len(route) - 1) if not return_to_start else (i == len(route) - 2)
icon_color = "green" if is_start else "red" if is_end else "blue"
stop_number = "Start" if is_start else "End" if is_end else f"Stop {i}"
popup_text = f"""
<div style='font-size: 12px'>
<b>City:</b> {city_names[point_idx]}<br>
<b>Stop:</b> {stop_number}
</div>
"""
folium.Marker(
location=coordinates[point_idx],
popup=folium.Popup(popup_text, max_width=200),
tooltip=f'{stop_number}: {city_names[point_idx]}',
icon=folium.Icon(color=icon_color, icon='info-sign')
).add_to(route_group)
route_group.add_to(map_obj)
folium.TileLayer(
tiles='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
attr='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
).add_to(map_obj)
return map_obj
def main():
st.set_page_config(page_title="TravelWander", layout="wide")
st.title("🌍 TravelWander")
st.markdown("""
Temukan rute optimal berkendara antar lokasi. Masukkan nama lokasi dibawah dan klik 'Optimize Route' untuk melihat hasilnya.
Kota 1 akan menjadi titik awal perjalanan.
""")
col1, col2 = st.columns([1, 2])
with col1:
st.subheader("πŸ“ Masukkan Lokasi")
trip_type = st.radio(
"Tipe Perjalanan",
["Closed Loop (Kembali ke titik awal)", "Single Trip (Tidak kembali ke titik awal)"],
help="Pilih apakah Anda ingin kembali ke lokasi awal atau tidak"
)
return_to_start = trip_type.startswith("Closed Loop")
city_count = st.number_input("Jumlah lokasi", min_value=2, max_value=10, value=3, step=1,
help="Maksimum 10 lokasi direkomendasikan karena batasan API")
if 'city_inputs' not in st.session_state:
st.session_state.city_inputs = [''] * city_count
if len(st.session_state.city_inputs) != city_count:
st.session_state.city_inputs = st.session_state.city_inputs[:city_count] + [''] * max(0, city_count - len(st.session_state.city_inputs))
city_names = []
city_coords = []
for i in range(city_count):
label = "Kota 1 (Titik Awal)" if i == 0 else f"Kota {i+1}"
city_name = st.text_input(
label,
value=st.session_state.city_inputs[i],
key=f"city_{i}"
)
st.session_state.city_inputs[i] = city_name
if city_name:
coords = cached_geocoding(city_name)
if coords:
city_names.append(city_name)
city_coords.append(coords)
else:
st.warning(f"⚠️ Tidak dapat menemukan koordinat untuk '{city_name}'")
with col2:
if not city_coords:
map_center = [-2.548926, 118.014863] # Center of Indonesia
zoom_start = 5
else:
map_center = city_coords[0]
zoom_start = 5
map_obj = folium.Map(location=map_center, zoom_start=zoom_start)
if st.button("πŸ”„ Optimize Route", key="optimize"):
if len(city_coords) < 2:
st.error("❌ Masukkan minimal 2 lokasi yang valid")
else:
with st.spinner("Menghitung rute optimal..."):
start_time = time.time()
dist_matrix, valid_coordinates, routes_dict = create_distance_matrix_with_routes(city_coords)
min_cost, optimal_route = held_karp_tsp_fixed_start(dist_matrix, return_to_start)
end_time = time.time()
if min_cost == float('inf'):
st.error("❌ Tidak dapat menemukan rute yang valid")
else:
st.success(f"βœ… Rute dihitung dalam {end_time - start_time:.2f} detik")
st.write(f"πŸ›£οΈ Total jarak berkendara: {min_cost:.2f} km")
st.write("πŸ“ Rute optimal:")
route_cities = [city_names[i] for i in optimal_route]
if return_to_start:
route_cities = route_cities[:-1]
route_text = " β†’ ".join(route_cities)
st.code(route_text)
map_obj = plot_route_with_roads(map_obj, valid_coordinates, optimal_route,
city_names, routes_dict, return_to_start)
st.components.v1.html(folium.Map._repr_html_(map_obj), height=600)
if __name__ == "__main__":
main()