import streamlit as st import pandas as pd import numpy as np from datetime import datetime, timedelta import scipy.spatial.distance as dist import openpyxl from openpyxl.styles import Font, PatternFill, Alignment, Border, Side import io # ตั้งค่าหน้าแอป st.set_page_config(page_title="Optimal Route Planner", page_icon="📍", layout="wide") st.title("📍 ระบบวางแผนเส้นทางโลจิสติกส์อัจฉริยะ (Route Optimizer)") st.subheader("จัดลำดับเส้นทางที่ดีที่สุดตามพิกัดภูมิศาสตร์ พร้อมคำนวณเวลาเดินทางและส่งออก Excel") # ส่วนรับข้อมูลพื้นฐาน (Sidebar) with st.sidebar: st.header("⚙️ ตั้งค่าการเดินทาง") # พิกัดเริ่มต้น (Default: พระราม 3) start_lat = st.number_input("ละติจูดจุดเริ่มต้น (Lat)", value=13.677113, format="%.6f") start_lon = st.number_input("ลองจิจูดจุดเริ่มต้น (Lon)", value=100.547028, format="%.6f") # เวลาเริ่มเดินทาง start_time_str = st.time_input("เวลาออกเดินทาง", datetime.strptime("08:30", "%H:%M").time()) # ความเร็วเฉลี่ย avg_speed = st.slider("ความเร็วเฉลี่ยในการขับขี่ (กม./ชม.)", min_value=30, max_value=120, value=80, step=5) # เวลาที่ใช้ในแต่ละจุด stop_duration = st.number_input("เวลาแวะพักในแต่ละจุด (นาที)", value=15) # อัปโหลดไฟล์พิกัด uploaded_file = st.file_uploader("อัปโหลดไฟล์พิกัดปลายทาง (CSV)", type=["csv"]) # ฟังก์ชันคำนวณระยะทาง Haversine (ระยะทางตรงผิวโลก) def haversine_distance(lat1, lon1, lat2, lon2): R = 6371.0 # รัศมีโลก (กม.) phi1, phi2 = np.radians(lat1), np.radians(lat2) dphi = np.radians(lat2 - lat1) dlam = np.radians(lon2 - lon1) a = np.sin(dphi/2.0)**2 + np.cos(phi1) * np.cos(phi2) * np.sin(dlam/2.0)**2 return 2 * R * np.arcsin(np.sqrt(a)) # ฟังก์ชันแก้ปัญหา TSP (หาเส้นทางสั้นที่สุด) ด้วยวิธี Nearest Neighbor def solve_tsp(start_coords, points_df): points = points_df[['no', 'lat', 'lon']].values.tolist() curr_lat, curr_lon = start_coords unvisited = points.copy() optimized_path = [] while unvisited: # เรียงตามระยะทางจากจุดปัจจุบันที่อยู่ใกล้ที่สุด unvisited.sort(key=lambda x: haversine_distance(curr_lat, curr_lon, x[1], x[2])) next_pt = unvisited.pop(0) optimized_path.append(next_pt) curr_lat, curr_lon = next_pt[1], next_pt[2] return optimized_path if uploaded_file is not None: # อ่านไฟล์ CSV df = pd.read_csv(uploaded_file) # ตรวจสอบโครงสร้างไฟล์ if not all(col in df.columns for col in ['no', 'lat', 'lon']): st.error("❌ ไฟล์ CSV ต้องประกอบด้วยคอลัมน์ 'no', 'lat', 'lon' เท่านั้น") else: st.success("📊 อ่านข้อมูลจุดพิกัดสำเร็จ!") # ค้นหาเส้นทางที่ดีที่สุด optimized_points = solve_tsp((start_lat, start_lon), df) # คำนวณระยะทางจริงและการหน่วงเวลา (เพิ่ม Road Factor 1.3 เพื่อจำลองถนนจริงที่ไม่ใช่ทางตรง) road_factor = 1.3 # คำนวณข้อมูลจุดต่อจุด itinerary = [] current_time = datetime.combine(datetime.today(), start_time_str) curr_lat, curr_lon = start_lat, start_lon # ใส่จุดเริ่มต้นลงในตาราง itinerary.append({ "ลำดับ": "จุดเริ่ม", "No. ใน CSV": "-", "พิกัด (Lat, Lon)": f"{start_lat:.6f}, {start_lon:.6f}", "เวลาถึงโดยประมาณ": "-", "เวลาออกจากจุด (นาที)": current_time.strftime("%H:%M น."), "ระยะทางไปจุดถัดไป (กม.)": 0.0, "เวลาขับรถไปจุดถัดไป (นาที)": 0 }) total_distance = 0.0 total_drive_time_mins = 0 # วนลูปจุดแวะพักทั้งหมด for idx, pt in enumerate(optimized_points): no, lat, lon = pt # หาระยะทางจากจุดปัจจุบันไปยังจุดนี้ dist_km = haversine_distance(curr_lat, curr_lon, lat, lon) * road_factor drive_time_mins = int((dist_km / avg_speed) * 60) # อัปเดตเวลารวมและสะสมเวลาเดินทางบนถนน total_distance += dist_km total_drive_time_mins += drive_time_mins # เวลาที่ขับมาถึงจุดนี้ current_time += timedelta(minutes=drive_time_mins) arr_time_str = current_time.strftime("%H:%M น.") # เวลาที่จะออกจากจุดนี้หลังจากพักทำภารกิจ current_time += timedelta(minutes=stop_duration) dep_time_str = current_time.strftime("%H:%M น.") # อัปเดตข้อมูลจุดก่อนหน้าให้ชี้ระยะทางมาที่จุดนี้ itinerary[-1]["ระยะทางไปจุดถัดไป (กม.)"] = round(dist_km, 1) itinerary[-1]["เวลาขับรถไปจุดถัดไป (นาที)"] = drive_time_mins itinerary.append({ "ลำดับ": idx + 1, "No. ใน CSV": int(no), "พิกัด (Lat, Lon)": f"{lat:.6f}, {lon:.6f}", "เวลาถึงโดยประมาณ": arr_time_str, "เวลาออกจากจุด (นาที)": dep_time_str, "ระยะทางไปจุดถัดไป (กม.)": 0.0, "เวลาขับรถไปจุดถัดไป (นาที)": 0 }) curr_lat, curr_lon = lat, lon # กลับเข้าจุดเริ่มต้น final_dist = haversine_distance(curr_lat, curr_lon, start_lat, start_lon) * road_factor final_drive_mins = int((final_dist / avg_speed) * 60) total_distance += final_dist total_drive_time_mins += final_drive_mins itinerary[-1]["ระยะทางไปจุดถัดไป (กม.)"] = round(final_dist, 1) itinerary[-1]["เวลาขับรถไปจุดถัดไป (นาที)"] = final_drive_mins current_time += timedelta(minutes=final_drive_mins) itinerary.append({ "ลำดับ": "จุดจบ", "No. ใน CSV": "-", "พิกัด (Lat, Lon)": f"{start_lat:.6f}, {start_lon:.6f}", "เวลาถึงโดยประมาณ": current_time.strftime("%H:%M น."), "เวลาออกจากจุด (นาที)": "-", "ระยะทางไปจุดถัดไป (กม.)": "-", "เวลาขับรถไปจุดถัดไป (นาที)": "-" }) # แสดงผลแดชบอร์ดด้านบน col1, col2, col3 = st.columns(3) with col1: st.metric("🛣️ ระยะทางรวมทั้งหมด", f"{total_distance:.1f} กม.") with col2: st.metric("⏳ เวลาอยู่บนรถจริง", f"{total_drive_time_mins // 60} ชม. {total_drive_time_mins % 60} นาที") with col3: total_trip_mins = total_drive_time_mins + (len(optimized_points) * stop_duration) st.metric("⏱️ เวลารวมทั้งภารกิจ (รวมจอด)", f"{total_trip_mins // 60} ชม. {total_trip_mins % 60} นาที") # แสดงผลตารางเปรียบเทียบในรูปแบบ Table บนหน้าเว็บ itinerary_df = pd.DataFrame(itinerary) st.dataframe(itinerary_df, use_container_width=True) # ฟังก์ชันสร้างไฟล์ Excel (.xlsx) ที่จัดรูปแบบสวยงาม def generate_excel(): output = io.BytesIO() wb = openpyxl.Workbook() ws = wb.active ws.title = "แผนการเดินทาง" ws.views.sheetView[0].showGridLines = True # การดีไซน์สีและฟอนต์ navy_fill = PatternFill(start_color="1B365D", end_color="1B365D", fill_type="solid") light_blue_fill = PatternFill(start_color="F2F4F8", end_color="F2F4F8", fill_type="solid") font_header = Font(name="Calibri", size=11, bold=True, color="FFFFFF") font_bold = Font(name="Calibri", size=11, bold=True) font_regular = Font(name="Calibri", size=11) thin_border = Border( left=Side(style='thin', color='D3D3D3'), right=Side(style='thin', color='D3D3D3'), top=Side(style='thin', color='D3D3D3'), bottom=Side(style='thin', color='D3D3D3') ) # ส่วนหัวรายงาน ws['A1'] = "แผนงานจัดเส้นทางและตารางเวลาเดินทางที่เหมาะสมที่สุด" ws['A1'].font = Font(name="Calibri", size=14, bold=True, color="1B365D") ws['A2'] = f"คำนวณที่ความเร็วพื้นฐาน {avg_speed} กม./ชม. | เวลาออกเดินทาง {start_time_str.strftime('%H:%M')} น." ws['A2'].font = Font(name="Calibri", size=11, italic=True) # เขียน Headers ลงแผ่นงาน headers = list(itinerary_df.columns) for col_idx, text in enumerate(headers, 1): cell = ws.cell(row=4, column=col_idx, value=text) cell.font = font_header cell.fill = navy_fill cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) # ใส่ข้อมูลลงแผ่นงาน for row_idx, row_data in enumerate(itinerary, 5): for col_idx, (k, v) in enumerate(row_data.items(), 1): cell = ws.cell(row=row_idx, column=col_idx, value=v) cell.font = font_regular cell.border = thin_border # แบ่งสีแถวสลับและจัดตำแหน่งข้อความ if row_idx % 2 == 0: cell.fill = light_blue_fill if col_idx in [1, 2, 3, 4, 5]: cell.alignment = Alignment(horizontal="center", vertical="center") else: cell.alignment = Alignment(horizontal="right", vertical="center") # ข้อมูลรวมท้ายตาราง sum_row = len(itinerary) + 5 ws.cell(row=sum_row, column=1, value="สรุปภาพรวม").font = font_bold summary_text = f"ระยะทางรวม: {total_distance:.1f} กม. | เวลาเดินทางบนรถรวม: {total_drive_time_mins // 60} ชม. {total_drive_time_mins % 60} นาที" ws.cell(row=sum_row, column=4, value=summary_text).font = font_bold ws.merge_cells(start_row=sum_row, start_column=4, end_row=sum_row, end_column=7) # ปรับความกว้างคอลัมน์อัตโนมัติ for col in ws.columns: max_len = max(len(str(cell.value or '')) for cell in col) col_letter = openpyxl.utils.get_column_letter(col[0].column) ws.column_dimensions[col_letter].width = max(max_len + 3, 12) wb.save(output) return output.getvalue() # ปุ่มสำหรับดาวน์โหลด Excel excel_data = generate_excel() st.download_button( label="📥 ดาวน์โหลดตารางแผนการเดินทางเป็น Excel (.xlsx)", data=excel_data, file_name=f"Route_Plan_{datetime.now().strftime('%Y%m%d')}.xlsx", mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ) else: st.info("💡 กรุณาอัปโหลดไฟล์ CSV พิกัดปลายทางที่เมนูด้านซ้ายเพื่อเริ่มต้นจัดเส้นทาง")