Spaces:
Sleeping
Sleeping
| 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 | |
| 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 | |
| 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='© <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() |