Jing997 commited on
Commit
dc171c8
·
1 Parent(s): 944f6e3

add pages src

Browse files
src/pages/__init__.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # Import all page functions to make them available from this package
2
+ from src.pages._home_page import home_page
3
+ from src.pages._about_page import about_page
4
+ from src.pages._contact_page import contact_page
5
+ from src.pages._map_page import map_page
6
+ from src.pages._optimize_page import optimize_page
7
+
8
+ __all__ = ['home_page', 'about_page', 'contact_page', 'map_page', 'optimize_page']
src/pages/_about_page.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+
3
+ def about_page():
4
+ """
5
+ Render the about page
6
+ """
7
+ st.title("About This Project")
8
+
9
+ st.write("""
10
+ ## Project Overview
11
+
12
+ This project is a **Delivery Route Optimization** tool built using Streamlit. It aims to optimize delivery
13
+ routes for a fleet of vehicles while considering constraints such as delivery time windows, vehicle capacity,
14
+ and traffic conditions.
15
+
16
+ """)
17
+
18
+ # Project overview from about page
19
+ st.write("""
20
+ This project is a **Delivery Route Optimization** tool that provides an interactive web interface
21
+ for solving complex logistics challenges. It uses advanced algorithms to determine the most efficient
22
+ delivery routes while balancing various constraints and business priorities.
23
+ """)
24
+
25
+ # Key features in columns
26
+ st.subheader("Key Features")
27
+
28
+ col1, col2 = st.columns(2)
29
+
30
+ with col1:
31
+ st.markdown("""
32
+ #### Route Optimization
33
+ - Solves the **Vehicle Routing Problem (VRP)** to determine efficient routes
34
+ - Incorporates constraints like time windows and vehicle capacity
35
+ - Prioritizes deliveries based on importance and urgency
36
+
37
+ #### Map Visualization
38
+ - Displays optimized routes on an interactive map
39
+ - Highlights delivery stops and depot locations
40
+ - Provides detailed route information and statistics
41
+ """)
42
+
43
+ with col2:
44
+ st.markdown("""
45
+ #### Calendar View
46
+ - Calendar-based schedule for deliveries
47
+ - Shows delivery timeline and workload distribution
48
+ - Helps manage delivery schedules efficiently
49
+
50
+ #### Interactive Dashboard
51
+ - Real-time delivery status monitoring
52
+ - Data filtering and visualization options
53
+ - Customizable optimization parameters
54
+ """)
55
+
56
+ # Tools and technologies in an expander
57
+ with st.expander("Tools and Technologies"):
58
+ col1, col2, col3 = st.columns(3)
59
+
60
+ with col1:
61
+ st.markdown("""
62
+ #### Core Technologies
63
+ - **Python** - Main programming language
64
+ - **Streamlit** - Interactive web interface
65
+ - **Google OR-Tools** - Optimization engine
66
+ """)
67
+
68
+ with col2:
69
+ st.markdown("""
70
+ #### Data Visualization
71
+ - **Folium** - Interactive maps
72
+ - **Plotly** - Charts and timelines
73
+ - **Pandas** - Data processing
74
+ """)
75
+
76
+ with col3:
77
+ st.markdown("""
78
+ #### Routing Services
79
+ - **OSRM** - Road distances calculation
80
+ - **TimeMatrix** - Travel time estimation
81
+ - **Geocoding** - Location services
82
+ """)
83
+
84
+ # Navigation guidance
85
+ st.header("Getting Started")
86
+ st.write("""
87
+ Use the sidebar navigation to explore the application:
88
+
89
+ - **Map**: Visualize delivery locations and vehicle depots
90
+ - **Optimizer**: Create optimized delivery routes
91
+ - **About**: Learn more about this application
92
+ - **Contact**: Get in touch with the team
93
+ """)
94
+
95
+ # Make sure the function can be executed standalone
96
+ if __name__ == "__main__":
97
+ about_page()
src/pages/_contact_page.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+
3
+ def contact_page():
4
+ """
5
+ Render the contact page
6
+ """
7
+ st.title("Contact")
8
+
9
+ st.write("""
10
+ ### Get in Touch
11
+
12
+ For questions, feedback, or suggestions about this application, please feel free to reach out.
13
+
14
+ **Email**: [email protected]
15
+
16
+ ### Repository
17
+
18
+ This project is open-source. Find the code on GitHub:
19
+ [streamlit-schedular-app](https://github.com/yourusername/streamlit-schedular-app)
20
+
21
+ ### License
22
+
23
+ This project is licensed under the MIT License. See the LICENSE file for more details.
24
+ """)
25
+
26
+ # Make sure the function can be executed standalone
27
+ if __name__ == "__main__":
28
+ contact_page()
src/pages/_home_page.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import os
4
+ from pathlib import Path
5
+
6
+ def home_page():
7
+ """
8
+ Render the combined home and about page
9
+ """
10
+ st.title("Delivery Route Optimization")
11
+
12
+ st.write("""
13
+ Welcome to the Delivery Route Optimization application! This tool helps logistics teams
14
+ optimize delivery routes for a fleet of vehicles while considering constraints such as delivery time windows,
15
+ vehicle capacity, and traffic conditions.
16
+
17
+ Use the navigation sidebar to explore different features of this application.
18
+ """)
19
+
20
+ # Quick stats from data at the top
21
+ try:
22
+ # Get data paths
23
+ root_dir = Path(__file__).resolve().parent.parent.parent
24
+ delivery_path = os.path.join(root_dir, 'data', 'delivery-data', 'delivery_data.csv')
25
+ vehicle_path = os.path.join(root_dir, 'data', 'vehicle-data', 'vehicle_data.csv')
26
+
27
+ if os.path.exists(delivery_path) and os.path.exists(vehicle_path):
28
+ # Load data for stats
29
+ delivery_data = pd.read_csv(delivery_path)
30
+ vehicle_data = pd.read_csv(vehicle_path)
31
+
32
+ # Display stats
33
+ st.subheader("Current Statistics")
34
+ col1, col2, col3 = st.columns(3)
35
+ with col1:
36
+ st.metric("Total Deliveries", len(delivery_data))
37
+ with col2:
38
+ st.metric("Total Vehicles", len(vehicle_data))
39
+ with col3:
40
+ pending = delivery_data[delivery_data['status'] == 'Pending'] if 'status' in delivery_data.columns else []
41
+ st.metric("Pending Deliveries", len(pending))
42
+
43
+ # Add more detailed stats in an expander
44
+ with st.expander("View More Statistics"):
45
+ # Status breakdown
46
+ if 'status' in delivery_data.columns:
47
+ st.write("#### Delivery Status Breakdown")
48
+ status_counts = delivery_data['status'].value_counts().reset_index()
49
+ status_counts.columns = ['Status', 'Count']
50
+ status_chart = st.bar_chart(status_counts.set_index('Status'))
51
+
52
+ # Priority breakdown
53
+ if 'priority' in delivery_data.columns:
54
+ st.write("#### Delivery Priority Breakdown")
55
+ priority_counts = delivery_data['priority'].value_counts().reset_index()
56
+ priority_counts.columns = ['Priority', 'Count']
57
+ priority_chart = st.bar_chart(priority_counts.set_index('Priority'))
58
+ else:
59
+ st.info("Please generate data first to see statistics")
60
+ st.code("python src/utils/generate_all_data.py")
61
+ except Exception as e:
62
+ st.info("Generate data first to see statistics")
63
+ st.code("python src/utils/generate_all_data.py")
64
+
65
+ # Add the image
66
+ img_path = Path(__file__).resolve().parent.parent.parent / "img" / "delivery-route-network.jpg"
67
+ if os.path.exists(img_path):
68
+ st.image(str(img_path), caption="Delivery Route Network")
69
+
70
+
71
+ # Make sure the function can be executed standalone
72
+ if __name__ == "__main__":
73
+ st.set_page_config(page_title="Home - Delivery Route Optimization", page_icon="🚚", layout="wide")
74
+ home_page()
src/pages/_map_page.py ADDED
@@ -0,0 +1,457 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import folium
3
+ from streamlit_folium import folium_static
4
+ import pandas as pd
5
+ import os
6
+ import plotly.figure_factory as ff
7
+ import plotly.express as px
8
+ from pathlib import Path
9
+ from datetime import datetime, timedelta
10
+ import numpy as np
11
+
12
+ def map_page():
13
+ """
14
+ Render the map visualization page with delivery and depot locations.
15
+ Can be called from app.py to display within the main application.
16
+ """
17
+ st.title("Delivery Route Map")
18
+ st.write("""
19
+ This page visualizes the delivery locations and vehicle depots on an interactive map.
20
+ Use the filters in the sidebar to customize the view.
21
+ """)
22
+
23
+ # Initialize session state variables for filters
24
+ if 'map_filters' not in st.session_state:
25
+ st.session_state.map_filters = {
26
+ 'selected_dates': ["All"],
27
+ 'priority_filter': [],
28
+ 'status_filter': [],
29
+ 'date_range': [None, None],
30
+ 'show_calendar': True,
31
+ 'show_map': True,
32
+ 'show_data_table': False,
33
+ 'cluster_markers': True
34
+ }
35
+
36
+ # Create filters in sidebar
37
+ with st.sidebar:
38
+ st.header("Map Filters")
39
+
40
+ # Show/hide options - use session state values as defaults
41
+ show_deliveries = st.checkbox(
42
+ "Show Deliveries",
43
+ value=st.session_state.map_filters.get('show_deliveries', True),
44
+ key="show_deliveries_checkbox"
45
+ )
46
+ st.session_state.map_filters['show_deliveries'] = show_deliveries
47
+
48
+ show_depots = st.checkbox(
49
+ "Show Depots",
50
+ value=st.session_state.map_filters.get('show_depots', True),
51
+ key="show_depots_checkbox"
52
+ )
53
+ st.session_state.map_filters['show_depots'] = show_depots
54
+
55
+ # Show/hide data table
56
+ show_data_table = st.checkbox(
57
+ "Show Data Table",
58
+ value=st.session_state.map_filters.get('show_data_table', False),
59
+ key="show_data_table_checkbox"
60
+ )
61
+ st.session_state.map_filters['show_data_table'] = show_data_table
62
+
63
+ # Choose visualization tabs
64
+ show_calendar = st.checkbox(
65
+ "Show Calendar View",
66
+ value=st.session_state.map_filters.get('show_calendar', True),
67
+ key="show_calendar_checkbox"
68
+ )
69
+ st.session_state.map_filters['show_calendar'] = show_calendar
70
+
71
+ # Try to load data
72
+ try:
73
+ # Get data paths
74
+ root_dir = Path(__file__).resolve().parent.parent.parent # Go up to project root level
75
+ delivery_path = os.path.join(root_dir, 'data', 'delivery-data', 'delivery_data.csv') # Fixed directory name with underscore
76
+ vehicle_path = os.path.join(root_dir, 'data', 'vehicle-data', 'vehicle_data.csv') # Fixed directory name with underscore
77
+
78
+ # Check if files exist
79
+ if not os.path.exists(delivery_path):
80
+ # Try with hyphen instead of underscore
81
+ delivery_path = os.path.join(root_dir, 'data', 'delivery-data', 'delivery_data.csv')
82
+ if not os.path.exists(delivery_path):
83
+ st.warning(f"Delivery data file not found at: {delivery_path}")
84
+ st.info("Please generate data first with: python src/utils/generate_all_data.py")
85
+ return
86
+
87
+ if not os.path.exists(vehicle_path):
88
+ # Try with hyphen instead of underscore
89
+ vehicle_path = os.path.join(root_dir, 'data', 'vehicle-data', 'vehicle_data.csv')
90
+ if not os.path.exists(vehicle_path):
91
+ st.warning(f"Vehicle data file not found at: {vehicle_path}")
92
+ st.info("Please generate data first with: python src/utils/generate_all_data.py")
93
+ return
94
+
95
+ # Load data
96
+ delivery_data = pd.read_csv(delivery_path)
97
+ vehicle_data = pd.read_csv(vehicle_path)
98
+
99
+ # Ensure delivery_date is properly formatted as datetime
100
+ if 'delivery_date' in delivery_data.columns:
101
+ delivery_data['delivery_date'] = pd.to_datetime(delivery_data['delivery_date'])
102
+
103
+ # Add more filters if data is available - CONVERT TO MULTI-SELECT
104
+ if 'priority' in delivery_data.columns:
105
+ with st.sidebar:
106
+ all_priorities = sorted(delivery_data['priority'].unique().tolist())
107
+ selected_priorities = st.multiselect(
108
+ "Filter by Priority",
109
+ options=all_priorities,
110
+ default=st.session_state.map_filters.get('priority_filter', all_priorities),
111
+ key="priority_multiselect"
112
+ )
113
+ st.session_state.map_filters['priority_filter'] = selected_priorities
114
+
115
+ if selected_priorities:
116
+ delivery_data = delivery_data[delivery_data['priority'].isin(selected_priorities)]
117
+
118
+ if 'status' in delivery_data.columns:
119
+ with st.sidebar:
120
+ all_statuses = sorted(delivery_data['status'].unique().tolist())
121
+ selected_statuses = st.multiselect(
122
+ "Filter by Status",
123
+ options=all_statuses,
124
+ default=st.session_state.map_filters.get('status_filter', all_statuses),
125
+ key="status_multiselect"
126
+ )
127
+ st.session_state.map_filters['status_filter'] = selected_statuses
128
+
129
+ if selected_statuses:
130
+ delivery_data = delivery_data[delivery_data['status'].isin(selected_statuses)]
131
+
132
+ if 'delivery_date' in delivery_data.columns:
133
+ with st.sidebar:
134
+ # Get the min/max dates from the ORIGINAL unfiltered data
135
+ # Load original data to get proper date range
136
+ original_data = pd.read_csv(delivery_path)
137
+ if 'delivery_date' in original_data.columns:
138
+ original_data['delivery_date'] = pd.to_datetime(original_data['delivery_date'])
139
+
140
+ min_date = original_data['delivery_date'].min().date()
141
+ max_date = original_data['delivery_date'].max().date()
142
+
143
+ # Get saved values from session state
144
+ saved_start_date = st.session_state.map_filters.get('date_range', [None, None])[0]
145
+ saved_end_date = st.session_state.map_filters.get('date_range', [None, None])[1]
146
+
147
+ # Validate saved dates - ensure they're within allowed range
148
+ if saved_start_date and saved_start_date < min_date:
149
+ saved_start_date = min_date
150
+ if saved_end_date and saved_end_date > max_date:
151
+ saved_end_date = max_date
152
+
153
+ # Set default values with proper validation
154
+ default_start_date = saved_start_date if saved_start_date else min_date
155
+ default_end_date = saved_end_date if saved_end_date else min(min_date + timedelta(days=7), max_date)
156
+
157
+ # Add date range picker
158
+ try:
159
+ date_range = st.date_input(
160
+ "Date Range",
161
+ value=(default_start_date, default_end_date),
162
+ min_value=min_date,
163
+ max_value=max_date,
164
+ key="date_range_input"
165
+ )
166
+
167
+ # Update session state with new date range
168
+ if len(date_range) == 2:
169
+ st.session_state.map_filters['date_range'] = list(date_range)
170
+ start_date, end_date = date_range
171
+ mask = (delivery_data['delivery_date'].dt.date >= start_date) & (delivery_data['delivery_date'].dt.date <= end_date)
172
+ delivery_data = delivery_data[mask]
173
+ except Exception as e:
174
+ # If there's any error with the date range, reset it
175
+ st.error(f"Error with date range: {e}")
176
+ st.session_state.map_filters['date_range'] = [min_date, max_date]
177
+ date_range = (min_date, max_date)
178
+ mask = (delivery_data['delivery_date'].dt.date >= min_date) & (delivery_data['delivery_date'].dt.date <= max_date)
179
+ delivery_data = delivery_data[mask]
180
+
181
+ # MOVED STATISTICS TO THE TOP
182
+ st.subheader("Delivery Overview")
183
+ col1, col2, col3 = st.columns(3)
184
+
185
+ with col1:
186
+ st.metric("Deliveries Shown", len(delivery_data))
187
+
188
+ with col2:
189
+ if 'weight_kg' in delivery_data.columns:
190
+ total_weight = delivery_data['weight_kg'].sum()
191
+ st.metric("Total Weight", f"{total_weight:.2f} kg")
192
+
193
+ with col3:
194
+ if 'status' in delivery_data.columns:
195
+ pending = len(delivery_data[delivery_data['status'] == 'Pending'])
196
+ st.metric("Pending Deliveries", pending)
197
+
198
+ # Status count columns - dynamic based on available statuses
199
+ if 'status' in delivery_data.columns:
200
+ status_counts = delivery_data['status'].value_counts()
201
+ # Create a varying number of columns based on unique statuses
202
+ status_cols = st.columns(len(status_counts))
203
+
204
+ for i, (status, count) in enumerate(status_counts.items()):
205
+ with status_cols[i]:
206
+ # Choose color based on status
207
+ delta_color = "normal"
208
+ if status == "Delivered":
209
+ delta_color = "off"
210
+ elif status == "In Transit":
211
+ delta_color = "normal"
212
+ elif status == "Pending":
213
+ delta_color = "inverse" # Red
214
+
215
+ # Calculate percentage
216
+ percentage = round((count / len(delivery_data)) * 100, 1)
217
+ st.metric(
218
+ f"{status}",
219
+ count,
220
+ f"{percentage}% of total",
221
+ delta_color=delta_color
222
+ )
223
+
224
+ # Create map
225
+ singapore_coords = [1.3521, 103.8198] # Center of Singapore
226
+ m = folium.Map(location=singapore_coords, zoom_start=12)
227
+
228
+ # Add delivery markers
229
+ if show_deliveries:
230
+ for _, row in delivery_data.iterrows():
231
+ # Create popup content
232
+ popup_content = f"<b>ID:</b> {row['delivery_id']}<br>"
233
+
234
+ if 'customer_name' in row:
235
+ popup_content += f"<b>Customer:</b> {row['customer_name']}<br>"
236
+
237
+ if 'address' in row:
238
+ popup_content += f"<b>Address:</b> {row['address']}<br>"
239
+
240
+ if 'time_window' in row:
241
+ popup_content += f"<b>Time Window:</b> {row['time_window']}<br>"
242
+
243
+ if 'priority' in row:
244
+ popup_content += f"<b>Priority:</b> {row['priority']}<br>"
245
+
246
+ if 'delivery_date' in row:
247
+ popup_content += f"<b>Date:</b> {row['delivery_date'].strftime('%b %d, %Y')}<br>"
248
+
249
+ if 'status' in row:
250
+ popup_content += f"<b>Status:</b> {row['status']}<br>"
251
+
252
+ # Choose marker color based on priority
253
+ color = 'blue'
254
+ if 'priority' in row:
255
+ if row['priority'] == 'High':
256
+ color = 'red'
257
+ elif row['priority'] == 'Medium':
258
+ color = 'orange'
259
+
260
+ # Add marker to map
261
+ folium.Marker(
262
+ [row['latitude'], row['longitude']],
263
+ popup=folium.Popup(popup_content, max_width=300),
264
+ tooltip=f"Delivery {row['delivery_id']}",
265
+ icon=folium.Icon(color=color)
266
+ ).add_to(m)
267
+
268
+ # Add depot markers
269
+ if show_depots:
270
+ for _, row in vehicle_data.iterrows():
271
+ # Create popup content
272
+ popup_content = f"<b>Vehicle ID:</b> {row['vehicle_id']}<br>"
273
+
274
+ if 'vehicle_type' in row:
275
+ popup_content += f"<b>Type:</b> {row['vehicle_type']}<br>"
276
+
277
+ if 'driver_name' in row:
278
+ popup_content += f"<b>Driver:</b> {row['driver_name']}<br>"
279
+
280
+ # Add marker to map
281
+ folium.Marker(
282
+ [row['depot_latitude'], row['depot_longitude']],
283
+ popup=folium.Popup(popup_content, max_width=300),
284
+ tooltip=f"Depot: {row['vehicle_id']}",
285
+ icon=folium.Icon(color='green', icon='home', prefix='fa')
286
+ ).add_to(m)
287
+
288
+ # Display the map
289
+ folium_static(m, width=800, height=500)
290
+
291
+ # Display calendar visualization if selected
292
+ if show_calendar and 'delivery_date' in delivery_data.columns and 'time_window' in delivery_data.columns:
293
+ st.subheader("Delivery Schedule Calendar")
294
+
295
+ # Process data for calendar view
296
+ calendar_data = delivery_data.copy()
297
+
298
+ # Extract start and end times from time_window
299
+ calendar_data[['start_time', 'end_time']] = calendar_data['time_window'].str.split('-', expand=True)
300
+
301
+ # Create start and end datetime for each delivery
302
+ calendar_data['Start'] = pd.to_datetime(
303
+ calendar_data['delivery_date'].dt.strftime('%Y-%m-%d') + ' ' + calendar_data['start_time']
304
+ )
305
+ calendar_data['Finish'] = pd.to_datetime(
306
+ calendar_data['delivery_date'].dt.strftime('%Y-%m-%d') + ' ' + calendar_data['end_time']
307
+ )
308
+
309
+ # Create task column for Gantt chart
310
+ calendar_data['Task'] = calendar_data['delivery_id'] + ': ' + calendar_data['customer_name']
311
+
312
+ # Create color mapping for priority
313
+ if 'priority' in calendar_data.columns:
314
+ color_map = {'High': 'rgb(255, 0, 0)', 'Medium': 'rgb(255, 165, 0)', 'Low': 'rgb(0, 0, 255)'}
315
+ calendar_data['Color'] = calendar_data['priority'].map(color_map)
316
+ else:
317
+ calendar_data['Color'] = 'rgb(0, 0, 255)' # Default blue
318
+
319
+ # Get all available dates and add ƒmulti-select filter
320
+ all_dates = sorted(calendar_data['delivery_date'].dt.date.unique())
321
+
322
+ # Format dates for display in the dropdown
323
+ date_options = {date.strftime('%b %d, %Y'): date for date in all_dates}
324
+
325
+ # Get default selection from session state
326
+ default_selections = st.session_state.map_filters.get('calendar_selected_dates', [])
327
+
328
+ # Validate default selections - only keep dates that exist in current options
329
+ valid_default_selections = [date_str for date_str in default_selections if date_str in date_options.keys()]
330
+
331
+ # If no valid selections remain, default to first date (if available)
332
+ if not valid_default_selections and date_options:
333
+ valid_default_selections = [list(date_options.keys())[0]]
334
+
335
+ # Add multiselect for date filtering with validated defaults
336
+ selected_date_strings = st.multiselect(
337
+ "Select dates to display",
338
+ options=list(date_options.keys()),
339
+ default=valid_default_selections,
340
+ key="calendar_date_selector"
341
+ )
342
+
343
+ # Save selections to session state
344
+ st.session_state.map_filters['calendar_selected_dates'] = selected_date_strings
345
+
346
+ # Convert selected strings back to date objects
347
+ selected_dates = [date_options[date_str] for date_str in selected_date_strings]
348
+
349
+ if not selected_dates:
350
+ st.info("Please select at least one date to view the delivery schedule.")
351
+ else:
352
+ # Filter calendar data to only include selected dates
353
+ filtered_calendar = calendar_data[calendar_data['delivery_date'].dt.date.isin(selected_dates)]
354
+
355
+ # Group tasks by date for better visualization
356
+ date_groups = filtered_calendar.groupby(filtered_calendar['delivery_date'].dt.date)
357
+
358
+ # Create tabs only for the selected dates
359
+ date_tabs = st.tabs([date.strftime('%b %d, %Y') for date in selected_dates])
360
+
361
+ for i, (date, tab) in enumerate(zip(selected_dates, date_tabs)):
362
+ with tab:
363
+ # Filter data for this date
364
+ day_data = filtered_calendar[filtered_calendar['delivery_date'].dt.date == date]
365
+
366
+ if len(day_data) > 0:
367
+ # Create figure
368
+ fig = px.timeline(
369
+ day_data,
370
+ x_start="Start",
371
+ x_end="Finish",
372
+ y="Task",
373
+ color="priority" if 'priority' in day_data.columns else None,
374
+ color_discrete_map={"High": "red", "Medium": "orange", "Low": "blue"},
375
+ hover_data=["customer_name", "address", "weight_kg", "status"]
376
+ )
377
+
378
+ # Update layout
379
+ fig.update_layout(
380
+ title=f"Deliveries scheduled for {date.strftime('%b %d, %Y')}",
381
+ xaxis_title="Time of Day",
382
+ yaxis_title="Delivery",
383
+ height=max(300, 50 * len(day_data)),
384
+ yaxis={'categoryorder':'category ascending'}
385
+ )
386
+
387
+ # Display figure
388
+ st.plotly_chart(fig, use_container_width=True)
389
+
390
+ # Show summary
391
+ col1, col2, col3 = st.columns(3)
392
+ with col1:
393
+ st.metric("Total Deliveries", len(day_data))
394
+ with col2:
395
+ if 'weight_kg' in day_data.columns:
396
+ st.metric("Total Weight", f"{day_data['weight_kg'].sum():.2f} kg")
397
+ with col3:
398
+ if 'priority' in day_data.columns and 'High' in day_data['priority'].values:
399
+ st.metric("High Priority", len(day_data[day_data['priority'] == 'High']))
400
+
401
+ # NEW - Add delivery status breakdown for this day
402
+ if 'status' in day_data.columns:
403
+ st.write("##### Deliveries by Status")
404
+ status_counts = day_data['status'].value_counts()
405
+ status_cols = st.columns(min(4, len(status_counts)))
406
+
407
+ for i, (status, count) in enumerate(status_counts.items()):
408
+ col_idx = i % len(status_cols)
409
+ with status_cols[col_idx]:
410
+ st.metric(status, count)
411
+ else:
412
+ st.info(f"No deliveries scheduled for {date.strftime('%b %d, %Y')}")
413
+
414
+ # Display raw data table if selected
415
+ if show_data_table:
416
+ st.subheader("Delivery Data")
417
+
418
+ # Create a copy for display
419
+ display_df = delivery_data.copy()
420
+
421
+ # Convert delivery_date back to string for display
422
+ if 'delivery_date' in display_df.columns:
423
+ display_df['delivery_date'] = display_df['delivery_date'].dt.strftime('%b %d, %Y')
424
+
425
+ # Compute which deliveries are urgent (next 7 days)
426
+ if 'delivery_date' in delivery_data.columns:
427
+ today = datetime.now().date()
428
+ next_week = today + timedelta(days=7)
429
+
430
+ # Function to highlight rows based on delivery status and urgency
431
+ def highlight_rows(row):
432
+ delivery_date = pd.to_datetime(row['delivery_date']).date() if 'delivery_date' in row else None
433
+
434
+ # Check status first - highlight delivered rows in green
435
+ if 'status' in row and row['status'] == 'Delivered':
436
+ return ['background-color: rgba(0, 255, 0, 0.1)'] * len(row)
437
+ # Then check for urgent high-priority deliveries - highlight in red
438
+ elif delivery_date and delivery_date <= next_week and delivery_date >= today and row['priority'] == 'High':
439
+ return ['background-color: rgba(255, 0, 0, 0.1)'] * len(row)
440
+ else:
441
+ return [''] * len(row)
442
+
443
+ # Display styled dataframe
444
+ st.dataframe(display_df.style.apply(highlight_rows, axis=1))
445
+ else:
446
+ st.dataframe(display_df)
447
+
448
+ except Exception as e:
449
+ st.error(f"Error loading data: {str(e)}")
450
+ st.info("Please generate the data first by running: python src/utils/generate_all_data.py")
451
+ st.write("Error details:", e) # Detailed error for debugging
452
+
453
+ # Make the function executable when file is run directly
454
+ if __name__ == "__main__":
455
+ # This is for debugging/testing the function independently
456
+ st.set_page_config(page_title="Map View - Delivery Route Optimization", page_icon="🗺️", layout="wide")
457
+ map_page()
src/pages/_optimize_page.py ADDED
@@ -0,0 +1,1735 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import numpy as np
4
+ import folium
5
+ from streamlit_folium import folium_static
6
+ import os
7
+ from pathlib import Path
8
+ from datetime import datetime, timedelta
9
+ import matplotlib.pyplot as plt
10
+ import random
11
+ import time
12
+ from ortools.constraint_solver import routing_enums_pb2
13
+ from ortools.constraint_solver import pywrapcp
14
+ import folium.plugins
15
+ from folium.features import DivIcon
16
+ import requests
17
+ import plotly.express as px
18
+
19
+ def clear_optimization_results():
20
+ """Clear optimization results when parameters change"""
21
+ if 'optimization_result' in st.session_state:
22
+ st.session_state.optimization_result = None
23
+
24
+ def optimize_page():
25
+ """
26
+ Render the optimization page with controls for route optimization
27
+ """
28
+ st.title("Delivery Route Optimization")
29
+
30
+ # Initialize session state variables
31
+ if 'optimization_result' not in st.session_state:
32
+ st.session_state.optimization_result = None
33
+ if 'optimization_params' not in st.session_state:
34
+ st.session_state.optimization_params = {
35
+ 'priority_weight': 0.3,
36
+ 'time_window_weight': 0.5,
37
+ 'balance_weight': 0.2,
38
+ 'max_vehicles': 5,
39
+ 'selected_dates': ["All"]
40
+ }
41
+ if 'calendar_display_dates' not in st.session_state:
42
+ st.session_state.calendar_display_dates = None
43
+ # Add this new session state variable to store calculated road routes
44
+ if 'calculated_road_routes' not in st.session_state:
45
+ st.session_state.calculated_road_routes = {}
46
+
47
+ # Load data
48
+ data = load_all_data()
49
+ if not data:
50
+ return
51
+
52
+ delivery_data, vehicle_data, distance_matrix, time_matrix, locations = data
53
+
54
+ # Optimization parameters
55
+ st.sidebar.header("Optimization Parameters")
56
+
57
+ # Date selection for deliveries
58
+ if 'delivery_date' in delivery_data.columns:
59
+ available_dates = sorted(delivery_data['delivery_date'].unique())
60
+ date_options = ["All"] + list(available_dates)
61
+
62
+ # Store current value before selection
63
+ current_selected_dates = st.session_state.optimization_params['selected_dates']
64
+
65
+ selected_dates = st.sidebar.multiselect(
66
+ "Select Delivery Dates",
67
+ options=date_options,
68
+ default=current_selected_dates,
69
+ key="delivery_date_selector"
70
+ )
71
+
72
+ # Check if selection changed
73
+ if selected_dates != current_selected_dates:
74
+ clear_optimization_results()
75
+ st.session_state.optimization_params['selected_dates'] = selected_dates
76
+
77
+ # Handle filtering based on selection
78
+ if "All" not in selected_dates:
79
+ if selected_dates: # If specific dates were selected
80
+ delivery_data = delivery_data[delivery_data['delivery_date'].isin(selected_dates)]
81
+ elif available_dates: # No dates selected, show warning
82
+ st.sidebar.warning("No dates selected. Please select at least one delivery date.")
83
+ return
84
+ # If "All" is selected, keep all dates - no filtering needed
85
+
86
+ # Priority weighting
87
+ current_priority = st.session_state.optimization_params['priority_weight']
88
+ priority_weight = st.sidebar.slider(
89
+ "Priority Importance",
90
+ min_value=0.0,
91
+ max_value=1.0,
92
+ value=current_priority,
93
+ help="Higher values give more importance to high-priority deliveries",
94
+ key="priority_weight",
95
+ on_change=clear_optimization_results
96
+ )
97
+
98
+ # Time window importance
99
+ current_time_window = st.session_state.optimization_params['time_window_weight']
100
+ time_window_weight = st.sidebar.slider(
101
+ "Time Window Importance",
102
+ min_value=0.0,
103
+ max_value=1.0,
104
+ value=current_time_window,
105
+ help="Higher values enforce stricter adherence to delivery time windows",
106
+ key="time_window_weight",
107
+ on_change=clear_optimization_results
108
+ )
109
+
110
+ # Distance vs load balancing
111
+ current_balance = st.session_state.optimization_params['balance_weight']
112
+ balance_weight = st.sidebar.slider(
113
+ "Load Balancing vs Distance",
114
+ min_value=0.0,
115
+ max_value=1.0,
116
+ value=current_balance,
117
+ help="Higher values prioritize even distribution of deliveries across vehicles over total distance",
118
+ key="balance_weight",
119
+ on_change=clear_optimization_results
120
+ )
121
+
122
+ # Max vehicles to use
123
+ available_vehicles = vehicle_data[vehicle_data['status'] == 'Available']
124
+ current_max_vehicles = st.session_state.optimization_params['max_vehicles']
125
+ max_vehicles = st.sidebar.slider(
126
+ "Maximum Vehicles to Use",
127
+ min_value=1,
128
+ max_value=len(available_vehicles),
129
+ value=min(current_max_vehicles, len(available_vehicles)),
130
+ key="max_vehicles",
131
+ on_change=clear_optimization_results
132
+ )
133
+
134
+ # Add minimum time window compliance slider
135
+ min_time_window_compliance = st.sidebar.slider(
136
+ "Minimum Time Window Compliance (%)",
137
+ min_value=0,
138
+ max_value=100,
139
+ value=75,
140
+ help="Minimum percentage of deliveries that must be within their time window",
141
+ key="min_time_window_compliance",
142
+ on_change=clear_optimization_results
143
+ )
144
+
145
+ # Update session state with new parameter values
146
+ st.session_state.optimization_params['priority_weight'] = priority_weight
147
+ st.session_state.optimization_params['time_window_weight'] = time_window_weight
148
+ st.session_state.optimization_params['balance_weight'] = balance_weight
149
+ st.session_state.optimization_params['max_vehicles'] = max_vehicles
150
+
151
+ # # Add a notification when parameters have changed and results need regenerating
152
+ # if ('optimization_result' not in st.session_state or st.session_state.optimization_result is None):
153
+ # st.warning("⚠️ Optimization parameters have changed. Please click 'Generate Optimal Routes' to update results.")
154
+
155
+ # Main optimization section
156
+ col1, col2 = st.columns([2, 1])
157
+
158
+ with col1:
159
+ st.subheader("Delivery Route Optimizer")
160
+
161
+ # Filter out completed deliveries for statistics
162
+ if 'status' in delivery_data.columns:
163
+ pending_deliveries = delivery_data[delivery_data['status'] != 'Delivered']
164
+ completed_count = len(delivery_data) - len(pending_deliveries)
165
+ else:
166
+ pending_deliveries = delivery_data
167
+ completed_count = 0
168
+
169
+ st.write(f"Optimizing routes for {len(pending_deliveries)} pending deliveries using up to {max_vehicles} vehicles")
170
+
171
+ # Statistics
172
+ st.write("#### Delivery Statistics")
173
+ total_count = len(delivery_data)
174
+ pending_count = len(pending_deliveries)
175
+
176
+ col1a, col1b = st.columns(2)
177
+ with col1a:
178
+ st.metric("Total Deliveries", total_count)
179
+ with col1b:
180
+ st.metric("Pending Deliveries", pending_count,
181
+ delta=f"-{completed_count}" if completed_count > 0 else None,
182
+ delta_color="inverse" if completed_count > 0 else "normal")
183
+
184
+ if 'priority' in delivery_data.columns:
185
+ # Show priority breakdown for pending deliveries only
186
+ priority_counts = pending_deliveries['priority'].value_counts()
187
+
188
+ # Display priority counts in a more visual way
189
+ st.write("##### Priority Breakdown")
190
+ priority_cols = st.columns(min(3, len(priority_counts)))
191
+
192
+ for i, (priority, count) in enumerate(priority_counts.items()):
193
+ col_idx = i % len(priority_cols)
194
+ with priority_cols[col_idx]:
195
+ st.metric(f"{priority}", count)
196
+
197
+ if 'weight_kg' in delivery_data.columns:
198
+ # Calculate weight only for pending deliveries
199
+ total_weight = pending_deliveries['weight_kg'].sum()
200
+ st.metric("Total Weight (Pending)", f"{total_weight:.2f} kg")
201
+
202
+ with col2:
203
+ st.write("#### Vehicle Availability")
204
+ st.write(f"Available Vehicles: {len(available_vehicles)}")
205
+
206
+ # Show vehicle capacity
207
+ if 'max_weight_kg' in vehicle_data.columns:
208
+ total_capacity = available_vehicles['max_weight_kg'].sum()
209
+ st.write(f"Total Capacity: {total_capacity:.2f} kg")
210
+
211
+ # Check if we have enough capacity
212
+ if 'weight_kg' in delivery_data.columns:
213
+ if total_capacity < total_weight:
214
+ st.warning("⚠️ Insufficient vehicle capacity for all deliveries")
215
+ else:
216
+ st.success("✅ Sufficient vehicle capacity")
217
+
218
+ # Run optimization button
219
+ run_optimization_btn = st.button("Generate Optimal Routes")
220
+
221
+ # Check if we should display results (either have results in session or button was clicked)
222
+ if run_optimization_btn or st.session_state.optimization_result is not None:
223
+ if run_optimization_btn:
224
+ # Run new optimization
225
+ with st.spinner("Calculating optimal routes..."):
226
+ start_time = time.time()
227
+
228
+ # Filter out completed deliveries before optimization
229
+ if 'status' in delivery_data.columns:
230
+ pending_deliveries = delivery_data[delivery_data['status'] != 'Delivered']
231
+ else:
232
+ pending_deliveries = delivery_data
233
+
234
+ # Prepare data for optimization - USE PENDING DELIVERIES ONLY
235
+ optimization_result = run_optimization(
236
+ delivery_data=pending_deliveries,
237
+ vehicle_data=available_vehicles.iloc[:max_vehicles],
238
+ distance_matrix=distance_matrix,
239
+ time_matrix=time_matrix,
240
+ locations=locations,
241
+ priority_weight=priority_weight,
242
+ time_window_weight=time_window_weight,
243
+ balance_weight=balance_weight,
244
+ min_time_window_compliance=min_time_window_compliance/100.0 # Convert to decimal
245
+ )
246
+
247
+ end_time = time.time()
248
+ st.success(f"Optimization completed in {end_time - start_time:.2f} seconds")
249
+
250
+ # Store results in session state
251
+ st.session_state.optimization_result = optimization_result
252
+ else:
253
+ # Use existing results
254
+ optimization_result = st.session_state.optimization_result
255
+
256
+ # Filter pending deliveries before displaying results
257
+ if 'status' in delivery_data.columns:
258
+ pending_deliveries = delivery_data[delivery_data['status'] != 'Delivered']
259
+ else:
260
+ pending_deliveries = delivery_data
261
+
262
+ # Display results with filtered pending deliveries
263
+ display_optimization_results(
264
+ optimization_result=optimization_result,
265
+ delivery_data=pending_deliveries, # ← CHANGED: Use pending_deliveries instead
266
+ vehicle_data=available_vehicles.iloc[:max_vehicles],
267
+ distance_matrix=distance_matrix,
268
+ time_matrix=time_matrix,
269
+ locations=locations
270
+ )
271
+
272
+ def load_all_data():
273
+ """
274
+ Load all necessary data for optimization
275
+
276
+ Returns:
277
+ tuple of (delivery_data, vehicle_data, distance_matrix, time_matrix, locations)
278
+ """
279
+ # Get data paths
280
+ root_dir = Path(__file__).resolve().parent.parent.parent
281
+ delivery_path = os.path.join(root_dir, 'data', 'delivery-data', 'delivery_data.csv')
282
+ vehicle_path = os.path.join(root_dir, 'data', 'vehicle-data', 'vehicle_data.csv')
283
+ distance_matrix_path = os.path.join(root_dir, 'data', 'time-matrix', 'distance_matrix.csv')
284
+ time_matrix_path = os.path.join(root_dir, 'data', 'time-matrix', 'base_time_matrix.csv')
285
+ locations_path = os.path.join(root_dir, 'data', 'time-matrix', 'locations.csv')
286
+
287
+ # Check if files exist
288
+ missing_files = []
289
+ for path, name in [
290
+ (delivery_path, "delivery data"),
291
+ (vehicle_path, "vehicle data"),
292
+ (distance_matrix_path, "distance matrix"),
293
+ (time_matrix_path, "time matrix"),
294
+ (locations_path, "locations data")
295
+ ]:
296
+ if not os.path.exists(path):
297
+ missing_files.append(name)
298
+
299
+ if missing_files:
300
+ st.error(f"Missing required data: {', '.join(missing_files)}")
301
+ st.info("Please generate all data first by running: python src/utils/generate_all_data.py")
302
+ return None
303
+
304
+ # Load data
305
+ delivery_data = pd.read_csv(delivery_path)
306
+ vehicle_data = pd.read_csv(vehicle_path)
307
+ distance_matrix = pd.read_csv(distance_matrix_path, index_col=0)
308
+ time_matrix = pd.read_csv(time_matrix_path, index_col=0)
309
+ locations = pd.read_csv(locations_path)
310
+
311
+ return delivery_data, vehicle_data, distance_matrix, time_matrix, locations
312
+
313
+ def run_optimization(delivery_data, vehicle_data, distance_matrix, time_matrix, locations,
314
+ priority_weight, time_window_weight, balance_weight, min_time_window_compliance=0.75):
315
+ """
316
+ Run the route optimization algorithm using Google OR-Tools
317
+
318
+ Parameters:
319
+ delivery_data (pd.DataFrame): DataFrame containing delivery information
320
+ vehicle_data (pd.DataFrame): DataFrame containing vehicle information
321
+ distance_matrix (pd.DataFrame): Distance matrix between locations
322
+ time_matrix (pd.DataFrame): Time matrix between locations
323
+ locations (pd.DataFrame): DataFrame with location details
324
+ priority_weight (float): Weight for delivery priority in optimization (α)
325
+ time_window_weight (float): Weight for time window adherence (β)
326
+ balance_weight (float): Weight for balancing load across vehicles (γ)
327
+ min_time_window_compliance (float): Minimum required time window compliance (δ)
328
+
329
+ Returns:
330
+ dict: Optimization results
331
+ """
332
+ st.write("Setting up optimization model with OR-Tools...")
333
+
334
+ # Extract required data for optimization
335
+ num_vehicles = len(vehicle_data)
336
+ num_deliveries = len(delivery_data)
337
+
338
+ # Create a list of all locations (depots + delivery points)
339
+ all_locations = []
340
+ delivery_locations = []
341
+ depot_locations = []
342
+ vehicle_capacities = []
343
+
344
+ # First, add depot locations (one per vehicle)
345
+ for i, (_, vehicle) in enumerate(vehicle_data.iterrows()):
346
+ depot_loc = {
347
+ 'id': vehicle['vehicle_id'],
348
+ 'type': 'depot',
349
+ 'index': i, # Important for mapping to OR-Tools indices
350
+ 'latitude': vehicle['depot_latitude'],
351
+ 'longitude': vehicle['depot_longitude'],
352
+ 'vehicle_index': i
353
+ }
354
+ depot_locations.append(depot_loc)
355
+ all_locations.append(depot_loc)
356
+
357
+ # Add vehicle capacity
358
+ if 'max_weight_kg' in vehicle:
359
+ vehicle_capacities.append(int(vehicle['max_weight_kg'] * 100)) # Convert to integers (OR-Tools works better with integers)
360
+ else:
361
+ vehicle_capacities.append(1000) # Default capacity of 10kg (1000 in scaled units)
362
+
363
+ # Then add delivery locations
364
+ for i, (_, delivery) in enumerate(delivery_data.iterrows()):
365
+ # Determine priority factor (will be used in the objective function)
366
+ priority_factor = 1.0
367
+ if 'priority' in delivery:
368
+ if delivery['priority'] == 'High':
369
+ priority_factor = 0.5 # Higher priority = lower cost
370
+ elif delivery['priority'] == 'Low':
371
+ priority_factor = 2.0 # Lower priority = higher cost
372
+
373
+ # Calculate delivery demand (weight)
374
+ demand = int(delivery.get('weight_kg', 1.0) * 100) # Convert to integers
375
+
376
+ delivery_loc = {
377
+ 'id': delivery['delivery_id'],
378
+ 'type': 'delivery',
379
+ 'index': num_vehicles + i, # Important for mapping to OR-Tools indices
380
+ 'latitude': delivery['latitude'],
381
+ 'longitude': delivery['longitude'],
382
+ 'priority': delivery.get('priority', 'Medium'),
383
+ 'priority_factor': priority_factor,
384
+ 'weight_kg': delivery.get('weight_kg', 1.0),
385
+ 'demand': demand,
386
+ 'time_window': delivery.get('time_window', '09:00-17:00'),
387
+ 'customer_name': delivery.get('customer_name', 'Unknown')
388
+ }
389
+ delivery_locations.append(delivery_loc)
390
+ all_locations.append(delivery_loc)
391
+
392
+ # Create distance and time matrices for OR-Tools
393
+ dist_matrix = np.zeros((len(all_locations), len(all_locations)))
394
+ time_matrix_mins = np.zeros((len(all_locations), len(all_locations)))
395
+
396
+ # Use the provided distance_matrix if it's the right size, otherwise compute distances
397
+ if isinstance(distance_matrix, pd.DataFrame) and len(distance_matrix) == len(all_locations):
398
+ # Convert dataframe to numpy array
399
+ dist_matrix = distance_matrix.values
400
+ time_matrix_mins = time_matrix.values
401
+ else:
402
+ # Compute simple Euclidean distances (this is a fallback)
403
+ for i in range(len(all_locations)):
404
+ for j in range(len(all_locations)):
405
+ if i == j:
406
+ continue
407
+
408
+ # Approximate distance in km (very rough)
409
+ lat1, lon1 = all_locations[i]['latitude'], all_locations[i]['longitude']
410
+ lat2, lon2 = all_locations[j]['latitude'], all_locations[j]['longitude']
411
+
412
+ # Simple Euclidean distance (for demo purposes)
413
+ dist = ((lat1 - lat2) ** 2 + (lon1 - lon2) ** 2) ** 0.5 * 111 # Convert to km
414
+ dist_matrix[i, j] = dist
415
+ time_matrix_mins[i, j] = dist * 2 # Rough estimate: 30km/h -> 2 mins per km
416
+
417
+ # Prepare demand array (0 for depots, actual demand for deliveries)
418
+ demands = [0] * num_vehicles + [d['demand'] for d in delivery_locations]
419
+
420
+ # Calculate total weight of all deliveries
421
+ total_delivery_weight = sum(d['demand'] for d in delivery_locations)
422
+
423
+ # OR-Tools setup
424
+ # Create the routing index manager
425
+ manager = pywrapcp.RoutingIndexManager(
426
+ len(all_locations), # Number of nodes (depots + deliveries)
427
+ num_vehicles, # Number of vehicles
428
+ list(range(num_vehicles)), # Vehicle start nodes (depot indices)
429
+ list(range(num_vehicles)) # Vehicle end nodes (back to depots)
430
+ )
431
+
432
+ # Create Routing Model
433
+ routing = pywrapcp.RoutingModel(manager)
434
+
435
+ # Define distance callback with priority weighting
436
+ # This implements the objective function: min sum_{i,j,k} c_jk * x_ijk * p_k^α
437
+ def distance_callback(from_index, to_index):
438
+ """Returns the weighted distance between the two nodes."""
439
+ # Convert from routing variable Index to distance matrix NodeIndex
440
+ from_node = manager.IndexToNode(from_index)
441
+ to_node = manager.IndexToNode(to_index)
442
+
443
+ # Get base distance
444
+ base_distance = int(dist_matrix[from_node, to_node] * 1000) # Convert to integers
445
+
446
+ # Apply priority weighting to destination node (if it's a delivery)
447
+ if to_node >= num_vehicles: # It's a delivery node
448
+ delivery_idx = to_node - num_vehicles
449
+ # Apply the priority factor with the priority weight (α)
450
+ priority_factor = delivery_locations[delivery_idx]['priority_factor']
451
+ # Higher priority_weight = stronger effect of priority on cost
452
+ priority_multiplier = priority_factor ** priority_weight
453
+ return int(base_distance * priority_multiplier)
454
+
455
+ return base_distance
456
+
457
+ # Define time callback
458
+ def time_callback(from_index, to_index):
459
+ """Returns the travel time between the two nodes."""
460
+ # Convert from routing variable Index to time matrix NodeIndex
461
+ from_node = manager.IndexToNode(from_index)
462
+ to_node = manager.IndexToNode(to_index)
463
+ return int(time_matrix_mins[from_node, to_node] * 60) # Convert minutes to seconds (integers)
464
+
465
+ # Define service time callback - time spent at each delivery
466
+ def service_time_callback(from_index):
467
+ """Returns the service time for the node."""
468
+ # Service time is 0 for depots and 10 minutes (600 seconds) for deliveries
469
+ node_idx = manager.IndexToNode(from_index)
470
+ if node_idx >= num_vehicles: # It's a delivery node
471
+ return 600 # 10 minutes in seconds
472
+ return 0 # No service time for depots
473
+
474
+ # Define demand callback
475
+ def demand_callback(from_index):
476
+ """Returns the demand of the node."""
477
+ # Convert from routing variable Index to demands array
478
+ from_node = manager.IndexToNode(from_index)
479
+ return demands[from_node]
480
+
481
+ # Register callbacks
482
+ transit_callback_index = routing.RegisterTransitCallback(distance_callback)
483
+ time_callback_index = routing.RegisterTransitCallback(time_callback)
484
+ service_callback_index = routing.RegisterUnaryTransitCallback(service_time_callback)
485
+ demand_callback_index = routing.RegisterUnaryTransitCallback(demand_callback)
486
+
487
+ # Set the arc cost evaluator for all vehicles - this is our objective function
488
+ routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
489
+
490
+ # Add capacity dimension - Hard Constraint 2: Vehicle Capacity Limits
491
+ routing.AddDimensionWithVehicleCapacity(
492
+ demand_callback_index,
493
+ 0, # null capacity slack
494
+ vehicle_capacities, # vehicle maximum capacities
495
+ True, # start cumul to zero
496
+ 'Capacity'
497
+ )
498
+
499
+ capacity_dimension = routing.GetDimensionOrDie('Capacity')
500
+
501
+ # Add load balancing penalties - Soft Constraint 3: Load Balancing Penalties
502
+ if balance_weight > 0.01:
503
+ # Calculate target weight per vehicle (ideal balanced load)
504
+ target_weight = total_delivery_weight / len(vehicle_capacities)
505
+
506
+ for i in range(num_vehicles):
507
+ # Get vehicle capacity
508
+ vehicle_capacity = vehicle_capacities[i]
509
+
510
+ # Set penalties for deviating from balanced load
511
+ # Scale penalty based on the balance_weight parameter (γ)
512
+ balance_penalty = int(10000 * balance_weight)
513
+
514
+ # Add soft bounds around the target weight
515
+ # Lower bound: Don't penalize for being under the target if there's not enough weight
516
+ lower_target = max(0, int(target_weight * 0.8))
517
+ capacity_dimension.SetCumulVarSoftLowerBound(
518
+ routing.End(i), lower_target, balance_penalty
519
+ )
520
+
521
+ # Upper bound: Penalize for going over the target
522
+ # But allow using more capacity if necessary to assign all deliveries
523
+ upper_target = min(vehicle_capacity, int(target_weight * 1.2))
524
+ capacity_dimension.SetCumulVarSoftUpperBound(
525
+ routing.End(i), upper_target, balance_penalty
526
+ )
527
+
528
+ # Add time dimension with service times
529
+ # This implements Hard Constraint 5: Time Continuity and
530
+ # Hard Constraint 6: Maximum Route Duration
531
+ routing.AddDimension(
532
+ time_callback_index,
533
+ 60 * 60, # Allow waiting time of 60 mins
534
+ 24 * 60 * 60, # Maximum time per vehicle (24 hours in seconds) - Hard Constraint 6
535
+ False, # Don't force start cumul to zero
536
+ 'Time'
537
+ )
538
+ time_dimension = routing.GetDimensionOrDie('Time')
539
+
540
+ # Add service time to each node's visit duration
541
+ for node_idx in range(len(all_locations)):
542
+ index = manager.NodeToIndex(node_idx)
543
+ time_dimension.SetCumulVarSoftUpperBound(
544
+ index,
545
+ 24 * 60 * 60, # 24 hours in seconds
546
+ 1000000 # High penalty for violating the 24-hour constraint
547
+ )
548
+ time_dimension.SlackVar(index).SetValue(0)
549
+
550
+ # Store time window variables to track compliance
551
+ time_window_vars = []
552
+ compliance_threshold = int(min_time_window_compliance * num_deliveries)
553
+
554
+ # Add time window constraints - Hard Constraint 7: Time Window Compliance
555
+ if time_window_weight > 0.01:
556
+ # Create binary variables to track time window compliance
557
+ for delivery_idx, delivery in enumerate(delivery_locations):
558
+ if 'time_window' in delivery and delivery['time_window']:
559
+ try:
560
+ start_time_str, end_time_str = delivery['time_window'].split('-')
561
+ start_hour, start_min = map(int, start_time_str.split(':'))
562
+ end_hour, end_min = map(int, end_time_str.split(':'))
563
+
564
+ # Convert to seconds since midnight
565
+ start_time_sec = (start_hour * 60 + start_min) * 60
566
+ end_time_sec = (end_hour * 60 + end_min) * 60
567
+
568
+ # Get the node index
569
+ index = manager.NodeToIndex(num_vehicles + delivery_idx)
570
+
571
+ # Add soft upper bound penalty with very high weight for late deliveries
572
+ # This implements Soft Constraint 2: Time Window Penalties
573
+ time_dimension.SetCumulVarSoftUpperBound(
574
+ index,
575
+ end_time_sec,
576
+ int(1000000 * time_window_weight) # High penalty for being late
577
+ )
578
+
579
+ # Don't penalize for early deliveries, just wait
580
+ time_dimension.CumulVar(index).SetMin(start_time_sec)
581
+
582
+ # Track this time window for compliance calculation
583
+ time_window_vars.append((index, start_time_sec, end_time_sec))
584
+ except:
585
+ # Skip if time window format is invalid
586
+ pass
587
+
588
+ # Hard Constraint 1: All Deliveries Must Be Assigned
589
+ # This is enforced by not creating disjunctions with penalties, but instead making all nodes mandatory
590
+
591
+ # Hard Constraint 3: Flow Conservation (Route Continuity) is inherently enforced by OR-Tools
592
+
593
+ # Hard Constraint 4: Start and End at Assigned Depots is enforced by the RoutingIndexManager setup
594
+
595
+ # Set parameters for the solver
596
+ search_parameters = pywrapcp.DefaultRoutingSearchParameters()
597
+
598
+ # Use guided local search to find good solutions
599
+ search_parameters.local_search_metaheuristic = (
600
+ routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH
601
+ )
602
+
603
+ # Use path cheapest arc with resource constraints as the first solution strategy
604
+ search_parameters.first_solution_strategy = (
605
+ routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC
606
+ )
607
+
608
+ # Give the solver enough time to find a good solution
609
+ search_parameters.time_limit.seconds = 10
610
+
611
+ # Enable logging
612
+ search_parameters.log_search = True
613
+
614
+ # Try to enforce the time window compliance threshold
615
+ if compliance_threshold > 0:
616
+ # First try to solve with all deliveries required
617
+ routing.CloseModelWithParameters(search_parameters)
618
+
619
+ # Solve the problem
620
+ st.write(f"Solving optimization model with {num_deliveries} deliveries and {num_vehicles} vehicles...")
621
+ st.write(f"Target: At least {compliance_threshold} of {num_deliveries} deliveries ({min_time_window_compliance*100:.0f}%) must be within time windows")
622
+
623
+ solution = routing.SolveWithParameters(search_parameters)
624
+ else:
625
+ # If no time window compliance required, solve normally
626
+ solution = routing.SolveWithParameters(search_parameters)
627
+
628
+ # If no solution was found, try a relaxed version (allow some deliveries to be unassigned)
629
+ if not solution:
630
+ st.warning("Could not find a solution with all deliveries assigned. Trying a relaxed version...")
631
+
632
+ # Create a new model with disjunctions to allow dropping some deliveries with high penalties
633
+ routing = pywrapcp.RoutingModel(manager)
634
+
635
+ # Re-register callbacks
636
+ transit_callback_index = routing.RegisterTransitCallback(distance_callback)
637
+ time_callback_index = routing.RegisterTransitCallback(time_callback)
638
+ service_callback_index = routing.RegisterUnaryTransitCallback(service_time_callback)
639
+ demand_callback_index = routing.RegisterUnaryTransitCallback(demand_callback)
640
+
641
+ routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
642
+
643
+ # Add capacity dimension again
644
+ routing.AddDimensionWithVehicleCapacity(
645
+ demand_callback_index,
646
+ 0, vehicle_capacities, True, 'Capacity'
647
+ )
648
+
649
+ # Add time dimension again
650
+ routing.AddDimension(
651
+ time_callback_index,
652
+ 60 * 60, 24 * 60 * 60, False, 'Time'
653
+ )
654
+ time_dimension = routing.GetDimensionOrDie('Time')
655
+
656
+ # Add disjunctions with very high penalties to try to include all deliveries
657
+ for delivery_idx in range(num_deliveries):
658
+ index = manager.NodeToIndex(num_vehicles + delivery_idx)
659
+ routing.AddDisjunction([index], 1000000) # High penalty but allows dropping if necessary
660
+
661
+ # Try to solve with relaxed constraints
662
+ search_parameters.time_limit.seconds = 15 # Give more time for relaxed version
663
+ solution = routing.SolveWithParameters(search_parameters)
664
+
665
+ if not solution:
666
+ st.error("Could not find any solution. Try increasing the number of vehicles or relaxing other constraints.")
667
+ return {
668
+ 'routes': {},
669
+ 'stats': {},
670
+ 'parameters': {
671
+ 'priority_weight': priority_weight,
672
+ 'time_window_weight': time_window_weight,
673
+ 'balance_weight': balance_weight,
674
+ 'min_time_window_compliance': min_time_window_compliance
675
+ }
676
+ }
677
+
678
+ # Extract solution
679
+ optimized_routes = {}
680
+ route_stats = {}
681
+
682
+ if solution:
683
+ st.success("Solution found!")
684
+
685
+ total_time_window_compliance = 0
686
+ total_deliveries_assigned = 0
687
+
688
+ for vehicle_idx in range(num_vehicles):
689
+ route = []
690
+ vehicle_id = vehicle_data.iloc[vehicle_idx]['vehicle_id']
691
+
692
+ # Get the vehicle information
693
+ vehicle_info = {
694
+ 'id': vehicle_id,
695
+ 'type': vehicle_data.iloc[vehicle_idx].get('vehicle_type', 'Standard'),
696
+ 'capacity': vehicle_data.iloc[vehicle_idx].get('max_weight_kg', 1000),
697
+ 'depot_latitude': vehicle_data.iloc[vehicle_idx]['depot_latitude'],
698
+ 'depot_longitude': vehicle_data.iloc[vehicle_idx]['depot_longitude']
699
+ }
700
+
701
+ # Initialize variables for tracking
702
+ index = routing.Start(vehicle_idx)
703
+ total_distance = 0
704
+ total_time = 0
705
+ total_load = 0
706
+ time_window_compliant = 0
707
+ total_deliveries = 0
708
+
709
+ # Initialize variables to track current position and time
710
+ current_time_sec = 8 * 3600 # Start at 8:00 AM (8 hours * 3600 seconds)
711
+
712
+ while not routing.IsEnd(index):
713
+ # Get the node index in the original data
714
+ node_idx = manager.IndexToNode(index)
715
+
716
+ # Skip depot nodes (they're already at the start)
717
+ if node_idx >= num_vehicles:
718
+ # This is a delivery node - get the corresponding delivery
719
+ delivery_idx = node_idx - num_vehicles
720
+ delivery = delivery_locations[delivery_idx].copy() # Create a copy to modify
721
+
722
+ # Calculate estimated arrival time in minutes since start of day
723
+ arrival_time_sec = solution.Min(time_dimension.CumulVar(index))
724
+ arrival_time_mins = arrival_time_sec // 60
725
+
726
+ # Store the estimated arrival time in the delivery
727
+ delivery['estimated_arrival'] = arrival_time_mins
728
+
729
+ # Check time window compliance
730
+ if 'time_window' in delivery and delivery['time_window']:
731
+ try:
732
+ start_time_str, end_time_str = delivery['time_window'].split('-')
733
+ start_hour, start_min = map(int, start_time_str.split(':'))
734
+ end_hour, end_min = map(int, end_time_str.split(':'))
735
+
736
+ # Convert to minutes for comparison
737
+ start_mins = start_hour * 60 + start_min
738
+ end_mins = end_hour * 60 + end_min
739
+
740
+ # Check if delivery is within time window
741
+ on_time = False
742
+
743
+ # If arrival <= end_time, consider it on-time (including early arrivals)
744
+ if arrival_time_mins <= end_mins:
745
+ on_time = True
746
+ time_window_compliant += 1
747
+ total_time_window_compliance += 1
748
+
749
+ delivery['within_time_window'] = on_time
750
+ except Exception as e:
751
+ st.warning(f"Error parsing time window for delivery {delivery['id']}: {str(e)}")
752
+ delivery['within_time_window'] = False
753
+
754
+ # Add to route
755
+ route.append(delivery)
756
+ total_deliveries += 1
757
+ total_deliveries_assigned += 1
758
+
759
+ # Add to total load
760
+ total_load += delivery['demand'] / 100 # Convert back to original units
761
+
762
+ # Move to the next node
763
+ previous_idx = index
764
+ index = solution.Value(routing.NextVar(index))
765
+
766
+ # Add distance and time from previous to current
767
+ if not routing.IsEnd(index):
768
+ previous_node = manager.IndexToNode(previous_idx)
769
+ next_node = manager.IndexToNode(index)
770
+
771
+ # Add distance between these points
772
+ segment_distance = dist_matrix[previous_node, next_node]
773
+ total_distance += segment_distance
774
+
775
+ # Add travel time between these points
776
+ segment_time_sec = int(time_matrix_mins[previous_node, next_node] * 60)
777
+ total_time += segment_time_sec / 60 # Convert seconds back to minutes
778
+
779
+ # Store the route if it's not empty
780
+ if route:
781
+ optimized_routes[vehicle_id] = route
782
+
783
+ # Calculate time window compliance percentage
784
+ time_window_percent = (time_window_compliant / total_deliveries * 100) if total_deliveries > 0 else 0
785
+
786
+ # Store route statistics
787
+ route_stats[vehicle_id] = {
788
+ 'vehicle_type': vehicle_info['type'],
789
+ 'capacity_kg': vehicle_info['capacity'],
790
+ 'deliveries': len(route),
791
+ 'total_distance_km': round(total_distance, 2),
792
+ 'estimated_time_mins': round(total_time),
793
+ 'total_load_kg': round(total_load, 2),
794
+ 'time_window_compliant': time_window_compliant,
795
+ 'time_window_compliance': time_window_percent
796
+ }
797
+
798
+ # Check if overall time window compliance meets the minimum requirement
799
+ overall_compliance = 0
800
+ if total_deliveries_assigned > 0:
801
+ overall_compliance = (total_time_window_compliance / total_deliveries_assigned)
802
+
803
+ if overall_compliance < min_time_window_compliance:
804
+ st.warning(f"Solution found, but time window compliance ({overall_compliance*100:.1f}%) is below the minimum required ({min_time_window_compliance*100:.0f}%).")
805
+ st.info("Consider adjusting parameters: increase the number of vehicles, reduce the minimum compliance requirement, or adjust time window importance.")
806
+ else:
807
+ st.success(f"Solution meets time window compliance requirement: {overall_compliance*100:.1f}% (minimum required: {min_time_window_compliance*100:.0f}%)")
808
+ else:
809
+ st.error("No solution found. Try adjusting the parameters.")
810
+ optimized_routes = {}
811
+ route_stats = {}
812
+
813
+ return {
814
+ 'routes': optimized_routes,
815
+ 'stats': route_stats,
816
+ 'parameters': {
817
+ 'priority_weight': priority_weight,
818
+ 'time_window_weight': time_window_weight,
819
+ 'balance_weight': balance_weight,
820
+ 'min_time_window_compliance': min_time_window_compliance
821
+ }
822
+ }
823
+
824
+ def display_optimization_results(optimization_result, delivery_data, vehicle_data,
825
+ distance_matrix, time_matrix, locations):
826
+ """
827
+ Display the optimization results
828
+
829
+ Parameters:
830
+ optimization_result (dict): Result from the optimization algorithm
831
+ delivery_data (pd.DataFrame): Delivery information
832
+ vehicle_data (pd.DataFrame): Vehicle information
833
+ distance_matrix (pd.DataFrame): Distance matrix between locations
834
+ time_matrix (pd.DataFrame): Time matrix between locations
835
+ locations (pd.DataFrame): Location details
836
+ """
837
+ # Define colors for vehicle routes
838
+ colors = ['blue', 'red', 'green', 'purple', 'orange', 'darkblue',
839
+ 'darkred', 'darkgreen', 'cadetblue', 'darkpurple', 'pink',
840
+ 'lightblue', 'lightred', 'lightgreen', 'gray', 'black', 'lightgray']
841
+
842
+ routes = optimization_result['routes']
843
+
844
+ # Display summary statistics
845
+ st.subheader("Optimization Results")
846
+
847
+ # Calculate overall statistics
848
+ total_deliveries = sum(len(route) for route in routes.values())
849
+ active_vehicles = sum(1 for route in routes.values() if len(route) > 0)
850
+
851
+ # Calculate additional metrics
852
+ total_distance = sum(stats.get('total_distance_km', 0) for stats in optimization_result.get('stats', {}).values())
853
+ total_time_mins = sum(stats.get('estimated_time_mins', 0) for stats in optimization_result.get('stats', {}).values())
854
+
855
+ # Calculate time window compliance (on-time percentage)
856
+ on_time_deliveries = 0
857
+ total_route_deliveries = 0
858
+
859
+ # Count deliveries within time window
860
+ for vehicle_id, route in routes.items():
861
+ stats = optimization_result.get('stats', {}).get(vehicle_id, {})
862
+
863
+ # Only process if we have stats for this vehicle
864
+ if stats and 'time_window_compliant' in stats:
865
+ # Use the actual count of compliant deliveries, not the percentage
866
+ on_time_deliveries += stats['time_window_compliant']
867
+ else:
868
+ # Try to estimate based on delivery details
869
+ for delivery in route:
870
+ if 'time_window' in delivery and 'estimated_arrival' in delivery:
871
+ # Format is typically "HH:MM-HH:MM"
872
+ try:
873
+ time_window = delivery['time_window']
874
+ start_time_str, end_time_str = time_window.split('-')
875
+
876
+ # Convert to minutes for comparison
877
+ start_mins = int(start_time_str.split(':')[0]) * 60 + int(start_time_str.split(':')[1])
878
+ end_mins = int(end_time_str.split(':')[0]) * 60 + int(end_time_str.split(':')[1])
879
+ arrival_mins = delivery.get('estimated_arrival', 0)
880
+
881
+ # Only consider deliveries late if they arrive after the end time
882
+ if arrival_mins <= end_mins:
883
+ on_time_deliveries += 1
884
+ except:
885
+ pass
886
+
887
+ total_route_deliveries += len(route)
888
+
889
+ # Ensure we have a valid number for on-time percentage
890
+ delivery_ontime_percent = 0
891
+ if total_route_deliveries > 0:
892
+ delivery_ontime_percent = (on_time_deliveries / total_route_deliveries) * 100
893
+
894
+ # Display metrics in a nicer layout with columns
895
+ st.write("### Overall Performance")
896
+ col1, col2, col3 = st.columns(3)
897
+ with col1:
898
+ st.metric("Deliveries Assigned", f"{total_deliveries}/{len(delivery_data)}")
899
+ st.metric("Vehicles Used", f"{active_vehicles}/{len(vehicle_data)}")
900
+
901
+ with col2:
902
+ st.metric("Total Distance", f"{total_distance:.1f} km")
903
+ st.metric("Total Time", f"{int(total_time_mins//60)}h {int(total_time_mins%60)}m")
904
+
905
+ with col3:
906
+ st.metric("Time Window Compliance", f"{delivery_ontime_percent:.0f}%")
907
+
908
+ # Calculate route efficiency (meters per delivery)
909
+ if total_deliveries > 0:
910
+ efficiency = (total_distance * 1000) / total_deliveries
911
+ st.metric("Avg Distance per Delivery", f"{efficiency:.0f} m")
912
+
913
+ # Add a visualization of time distribution
914
+ st.write("### Time & Distance Distribution by Vehicle")
915
+ time_data = {vehicle_id: stats.get('estimated_time_mins', 0)
916
+ for vehicle_id, stats in optimization_result.get('stats', {}).items()
917
+ if len(routes.get(vehicle_id, [])) > 0}
918
+
919
+ if time_data:
920
+ # Create bar charts for time and distance
921
+ time_df = pd.DataFrame({
922
+ 'Vehicle': list(time_data.keys()),
923
+ 'Time (mins)': list(time_data.values())
924
+ })
925
+
926
+ distance_data = {vehicle_id: stats.get('total_distance_km', 0)
927
+ for vehicle_id, stats in optimization_result.get('stats', {}).items()
928
+ if len(routes.get(vehicle_id, [])) > 0}
929
+
930
+ distance_df = pd.DataFrame({
931
+ 'Vehicle': list(distance_data.keys()),
932
+ 'Distance (km)': list(distance_data.values())
933
+ })
934
+
935
+ col1, col2 = st.columns(2)
936
+ with col1:
937
+ st.bar_chart(time_df.set_index('Vehicle'))
938
+ with col2:
939
+ st.bar_chart(distance_df.set_index('Vehicle'))
940
+
941
+ # Display the map with all routes
942
+ st.subheader("Route Map with Road Navigation")
943
+
944
+ # Add info about the route visualization
945
+ st.info("""
946
+ The map shows delivery routes that follow road networks from the depot to each stop in sequence, and back to the depot.
947
+ Numbered circles indicate the stop sequence, and arrows show travel direction.
948
+ """)
949
+
950
+ # Extract all available dates from the delivery data
951
+ if 'delivery_date' in delivery_data.columns:
952
+ # Extract unique dates, ensuring all are converted to datetime objects
953
+ available_dates = sorted(pd.to_datetime(delivery_data['delivery_date'].unique()))
954
+
955
+ # Format dates for display
956
+ date_options = {}
957
+ for date in available_dates:
958
+ # Ensure date is a proper datetime object before formatting
959
+ if isinstance(date, str):
960
+ date_obj = pd.to_datetime(date)
961
+ else:
962
+ date_obj = date
963
+ # Create the formatted string key
964
+ date_str = date_obj.strftime('%b %d, %Y')
965
+ date_options[date_str] = date_obj
966
+
967
+ # Default to earliest date
968
+ default_date = min(available_dates) if available_dates else None
969
+ default_date_str = default_date.strftime('%b %d, %Y') if default_date else None
970
+
971
+ # Create date selection dropdown
972
+ selected_date_str = st.selectbox(
973
+ "Select date to show routes for:",
974
+ options=list(date_options.keys()),
975
+ index=0 if default_date_str else None,
976
+ )
977
+
978
+ # Convert selected string back to date object
979
+ selected_date = date_options[selected_date_str] if selected_date_str else None
980
+
981
+ # Filter routes to only show deliveries for the selected date
982
+ if selected_date is not None:
983
+ filtered_routes = {}
984
+
985
+ for vehicle_id, route in routes.items():
986
+ # Keep only deliveries for the selected date
987
+ filtered_route = []
988
+
989
+ for delivery in route:
990
+ delivery_id = delivery['id']
991
+ # Find the delivery in the original data to get its date
992
+ delivery_row = delivery_data[delivery_data['delivery_id'] == delivery_id]
993
+
994
+ if not delivery_row.empty and 'delivery_date' in delivery_row:
995
+ delivery_date = delivery_row['delivery_date'].iloc[0]
996
+
997
+ # Check if this delivery is for the selected date
998
+ if pd.to_datetime(delivery_date).date() == pd.to_datetime(selected_date).date():
999
+ filtered_route.append(delivery)
1000
+
1001
+ # Only add the vehicle if it has deliveries on this date
1002
+ if filtered_route:
1003
+ filtered_routes[vehicle_id] = filtered_route
1004
+
1005
+ # Replace the original routes with filtered ones for map display
1006
+ routes_for_map = filtered_routes
1007
+ st.write(f"Showing routes for {len(routes_for_map)} vehicles on {selected_date_str}")
1008
+ else:
1009
+ routes_for_map = routes
1010
+ else:
1011
+ routes_for_map = routes
1012
+ st.warning("No delivery dates available in data. Showing all routes.")
1013
+
1014
+ # Create a map centered on Singapore
1015
+ singapore_coords = [1.3521, 103.8198]
1016
+ m = folium.Map(location=singapore_coords, zoom_start=12)
1017
+
1018
+ # Modify loop to use routes_for_map instead of routes
1019
+ # Count total route segments for progress bar
1020
+ total_segments = sum(len(route) + 1 for route in routes_for_map.values() if route) # +1 for return to depot
1021
+
1022
+ # Create a unique key for this optimization result to use in session state
1023
+ optimization_key = hash(str(optimization_result))
1024
+
1025
+ # Check if we have stored routes for this optimization result
1026
+ if optimization_key not in st.session_state.calculated_road_routes:
1027
+ # Initialize storage for this optimization
1028
+ st.session_state.calculated_road_routes[optimization_key] = {}
1029
+
1030
+ # Count total route segments for progress bar
1031
+ total_segments = sum(len(route) + 1 for route in routes_for_map.values() if route) # +1 for return to depot
1032
+ route_progress = st.progress(0)
1033
+ progress_container = st.empty()
1034
+ progress_container.text("Calculating routes: 0%")
1035
+
1036
+ # Counter for processed segments
1037
+ processed_segments = 0
1038
+
1039
+ for i, (vehicle_id, route) in enumerate(routes_for_map.items()):
1040
+ if not route:
1041
+ continue
1042
+
1043
+ # Get vehicle info
1044
+ vehicle_info = vehicle_data[vehicle_data['vehicle_id'] == vehicle_id].iloc[0]
1045
+
1046
+ # Use color cycling if we have more vehicles than colors
1047
+ color = colors[i % len(colors)]
1048
+
1049
+ # Add depot marker
1050
+ depot_lat, depot_lon = vehicle_info['depot_latitude'], vehicle_info['depot_longitude']
1051
+
1052
+ # Create depot popup content
1053
+ depot_popup = f"""
1054
+ <b>Depot:</b> {vehicle_id}<br>
1055
+ <b>Vehicle Type:</b> {vehicle_info['vehicle_type']}<br>
1056
+ <b>Driver:</b> {vehicle_info.get('driver_name', 'Unknown')}<br>
1057
+ """
1058
+
1059
+ # Add depot marker with START label
1060
+ folium.Marker(
1061
+ [depot_lat, depot_lon],
1062
+ popup=folium.Popup(depot_popup, max_width=300),
1063
+ tooltip=f"Depot: {vehicle_id} (START/END)",
1064
+ icon=folium.Icon(color=color, icon='home', prefix='fa')
1065
+ ).add_to(m)
1066
+
1067
+ # Create route points for complete journey
1068
+ waypoints = [(depot_lat, depot_lon)] # Start at depot
1069
+
1070
+ # Add all delivery locations as waypoints
1071
+ for delivery in route:
1072
+ waypoints.append((delivery['latitude'], delivery['longitude']))
1073
+
1074
+ # Close the loop back to depot
1075
+ waypoints.append((depot_lat, depot_lon))
1076
+
1077
+ # Add delivery point markers with sequenced numbering
1078
+ for j, delivery in enumerate(route):
1079
+ lat, lon = delivery['latitude'], delivery['longitude']
1080
+
1081
+ # Create popup content
1082
+ popup_content = f"""
1083
+ <b>Stop {j+1}:</b> {delivery['id']}<br>
1084
+ <b>Customer:</b> {delivery.get('customer_name', 'Unknown')}<br>
1085
+ """
1086
+
1087
+ if 'priority' in delivery:
1088
+ popup_content += f"<b>Priority:</b> {delivery['priority']}<br>"
1089
+
1090
+ if 'weight_kg' in delivery:
1091
+ popup_content += f"<b>Weight:</b> {delivery['weight_kg']:.2f} kg<br>"
1092
+
1093
+ if 'time_window' in delivery:
1094
+ popup_content += f"<b>Time Window:</b> {delivery['time_window']}<br>"
1095
+
1096
+ # Add circle markers and other delivery visualizations
1097
+ folium.Circle(
1098
+ location=[lat, lon],
1099
+ radius=50,
1100
+ color=color,
1101
+ fill=True,
1102
+ fill_color=color,
1103
+ fill_opacity=0.7,
1104
+ tooltip=f"Stop {j+1}: {delivery['id']}"
1105
+ ).add_to(m)
1106
+
1107
+ # Add text label with stop number
1108
+ folium.map.Marker(
1109
+ [lat, lon],
1110
+ icon=DivIcon(
1111
+ icon_size=(20, 20),
1112
+ icon_anchor=(10, 10),
1113
+ html=f'<div style="font-size: 12pt; color: #444444; font-weight: bold; text-align: center;">{j+1}</div>',
1114
+ )
1115
+ ).add_to(m)
1116
+
1117
+ # Add regular marker with popup
1118
+ folium.Marker(
1119
+ [lat + 0.0003, lon], # slight offset to not overlap with the circle
1120
+ popup=folium.Popup(popup_content, max_width=300),
1121
+ tooltip=f"Delivery {delivery['id']}",
1122
+ icon=folium.Icon(color=color, icon='box', prefix='fa')
1123
+ ).add_to(m)
1124
+
1125
+ # Create road-based routes between each waypoint with progress tracking
1126
+ for k in range(len(waypoints) - 1):
1127
+ # Get start and end points of this segment
1128
+ start_point = waypoints[k]
1129
+ end_point = waypoints[k+1]
1130
+
1131
+ # Create a key for this route segment
1132
+ route_key = f"{vehicle_id}_{k}"
1133
+
1134
+ # Update progress text
1135
+ segment_desc = "depot" if k == 0 else f"stop {k}"
1136
+ next_desc = f"stop {k+1}" if k < len(waypoints) - 2 else "depot"
1137
+
1138
+ # Check if we have already calculated this route
1139
+ if route_key in st.session_state.calculated_road_routes[optimization_key]:
1140
+ # Use stored route
1141
+ road_route = st.session_state.calculated_road_routes[optimization_key][route_key]
1142
+ progress_text = f"Using stored route for Vehicle {vehicle_id}: {segment_desc} → {next_desc}"
1143
+ else:
1144
+ # Calculate and store new route
1145
+ progress_text = f"Calculating route for Vehicle {vehicle_id}: {segment_desc} → {next_desc}"
1146
+ with st.spinner(progress_text):
1147
+ # Get a road-like route between these points
1148
+ road_route = get_road_route(start_point, end_point)
1149
+ # Store for future use
1150
+ st.session_state.calculated_road_routes[optimization_key][route_key] = road_route
1151
+
1152
+ # Add the route line (non-animated)
1153
+ folium.PolyLine(
1154
+ road_route,
1155
+ color=color,
1156
+ weight=4,
1157
+ opacity=0.8,
1158
+ tooltip=f"Route {vehicle_id}: {segment_desc} → {next_desc}"
1159
+ ).add_to(m)
1160
+
1161
+ # Add direction arrow
1162
+ idx = int(len(road_route) * 0.7)
1163
+ if idx < len(road_route) - 1:
1164
+ p1 = road_route[idx]
1165
+ p2 = road_route[idx + 1]
1166
+
1167
+ # Calculate direction angle
1168
+ dy = p2[0] - p1[0]
1169
+ dx = p2[1] - p1[1]
1170
+ angle = (90 - np.degrees(np.arctan2(dy, dx))) % 360
1171
+
1172
+ # Add arrow marker
1173
+ folium.RegularPolygonMarker(
1174
+ location=p1,
1175
+ number_of_sides=3,
1176
+ radius=8,
1177
+ rotation=angle,
1178
+ color=color,
1179
+ fill_color=color,
1180
+ fill_opacity=0.8
1181
+ ).add_to(m)
1182
+
1183
+ # Update progress after each segment
1184
+ processed_segments += 1
1185
+ progress_percentage = int((processed_segments / total_segments) * 100)
1186
+ route_progress.progress(processed_segments / total_segments)
1187
+ progress_container.text(f"Calculating routes: {progress_percentage}%")
1188
+
1189
+ # Add a message to show when using cached routes
1190
+ if optimization_key in st.session_state.calculated_road_routes:
1191
+ cached_count = len(st.session_state.calculated_road_routes[optimization_key])
1192
+ if cached_count > 0 and cached_count >= processed_segments:
1193
+ st.info(f"✅ Using {cached_count} previously calculated routes. No recalculation needed.")
1194
+
1195
+ # Clear progress display when done
1196
+ progress_container.empty()
1197
+ route_progress.empty()
1198
+ st.success("All routes calculated successfully!")
1199
+
1200
+ # Display the map
1201
+ folium_static(m, width=800, height=600)
1202
+
1203
+ # -----------------------------------------------------
1204
+ # Unified Schedule Calendar Section
1205
+ # -----------------------------------------------------
1206
+ st.subheader("Schedule Calendar View")
1207
+ st.write("This calendar shows both delivery schedules and vehicle assignments. On-time deliveries are shown in green, late deliveries in red.")
1208
+
1209
+ # Process data for calendar view
1210
+ if routes:
1211
+ # First, collect all assigned deliveries and their details
1212
+ calendar_data = []
1213
+
1214
+ # Track which deliveries were actually included in routes
1215
+ assigned_delivery_ids = set()
1216
+
1217
+ # Step 1: Process all assigned deliveries first
1218
+ for vehicle_id, route in routes.items():
1219
+ for delivery in route:
1220
+ assigned_delivery_ids.add(delivery['id'])
1221
+ # Get vehicle info
1222
+ vehicle_info = vehicle_data[vehicle_data['vehicle_id'] == vehicle_id].iloc[0]
1223
+ vehicle_type = vehicle_info.get('vehicle_type', 'Standard')
1224
+ driver_name = vehicle_info.get('driver_name', 'Unknown')
1225
+
1226
+ # Extract delivery data
1227
+ delivery_id = delivery['id']
1228
+ customer_name = delivery.get('customer_name', 'Unknown')
1229
+ priority = delivery.get('priority', 'Medium')
1230
+ time_window = delivery.get('time_window', '09:00-17:00')
1231
+ weight = delivery.get('weight_kg', 0)
1232
+
1233
+ # Extract start and end times from time_window
1234
+ start_time_str, end_time_str = time_window.split('-')
1235
+
1236
+ # Get delivery date from original data
1237
+ delivery_row = delivery_data[delivery_data['delivery_id'] == delivery_id]
1238
+ delivery_date = delivery_row['delivery_date'].iloc[0] if not delivery_row.empty and 'delivery_date' in delivery_row else datetime.now().date()
1239
+
1240
+ # Create start and end datetime for the delivery
1241
+ try:
1242
+ # Convert to pandas datetime
1243
+ if isinstance(delivery_date, pd.Timestamp):
1244
+ date_str = delivery_date.strftime('%Y-%m-%d')
1245
+ elif isinstance(delivery_date, str):
1246
+ date_str = pd.to_datetime(delivery_date).strftime('%Y-%m-%d')
1247
+ else:
1248
+ date_str = delivery_date.strftime('%Y-%m-%d')
1249
+
1250
+ start_datetime = pd.to_datetime(f"{date_str} {start_time_str}")
1251
+ end_datetime = pd.to_datetime(f"{date_str} {end_time_str}")
1252
+
1253
+ # Check if this is on time (based on the estimated arrival from the route)
1254
+ estimated_arrival_mins = delivery.get('estimated_arrival', 0)
1255
+
1256
+ # Convert time_window to minutes for comparison
1257
+ start_mins = int(start_time_str.split(':')[0]) * 60 + int(start_time_str.split(':')[1])
1258
+ end_mins = int(end_time_str.split(':')[0]) * 60 + int(end_time_str.split(':')[1])
1259
+
1260
+ # Determine if delivery is on time
1261
+ on_time = start_mins <= estimated_arrival_mins <= end_mins
1262
+
1263
+ # Set color based on on-time status and assignment
1264
+ if on_time:
1265
+ # Green for on-time
1266
+ color = 'on_time'
1267
+ else:
1268
+ # Red for not on-time
1269
+ color = 'late'
1270
+
1271
+ calendar_data.append({
1272
+ 'delivery_id': delivery_id,
1273
+ 'customer_name': customer_name,
1274
+ 'vehicle_id': vehicle_id,
1275
+ 'driver_name': driver_name,
1276
+ 'vehicle_type': vehicle_type,
1277
+ 'priority': priority,
1278
+ 'time_window': time_window,
1279
+ 'estimated_arrival_mins': estimated_arrival_mins,
1280
+ 'estimated_arrival_time': f"{estimated_arrival_mins//60:02d}:{estimated_arrival_mins%60:02d}",
1281
+ 'weight_kg': weight,
1282
+ 'Start': start_datetime,
1283
+ 'Finish': end_datetime,
1284
+ 'Task': f"{delivery_id}: {customer_name}",
1285
+ 'Vehicle Task': f"{vehicle_id}: {driver_name}",
1286
+ 'on_time': on_time,
1287
+ 'assigned': True,
1288
+ 'color': color,
1289
+ 'delivery_date': pd.to_datetime(date_str)
1290
+ })
1291
+ except Exception as e:
1292
+ st.warning(f"Could not process time window for delivery {delivery_id}: {str(e)}")
1293
+
1294
+ # Step 2: Now add unassigned deliveries
1295
+ for _, row in delivery_data.iterrows():
1296
+ delivery_id = row['delivery_id']
1297
+
1298
+ # Skip if already assigned
1299
+ if delivery_id in assigned_delivery_ids:
1300
+ continue
1301
+
1302
+ # Extract data for unassigned delivery
1303
+ customer_name = row.get('customer_name', 'Unknown')
1304
+ priority = row.get('priority', 'Medium')
1305
+ time_window = row.get('time_window', '09:00-17:00')
1306
+ weight = row.get('weight_kg', 0)
1307
+
1308
+ # Extract start and end times from time_window
1309
+ start_time_str, end_time_str = time_window.split('-')
1310
+
1311
+ # Get delivery date
1312
+ if 'delivery_date' in row:
1313
+ delivery_date = row['delivery_date']
1314
+ else:
1315
+ delivery_date = datetime.now().date()
1316
+
1317
+ # Create start and end datetime
1318
+ try:
1319
+ # Convert to pandas datetime
1320
+ if isinstance(delivery_date, pd.Timestamp):
1321
+ date_str = delivery_date.strftime('%Y-%m-%d')
1322
+ elif isinstance(delivery_date, str):
1323
+ date_str = pd.to_datetime(delivery_date).strftime('%Y-%m-%d')
1324
+ else:
1325
+ date_str = delivery_date.strftime('%Y-%m-%d')
1326
+
1327
+ start_datetime = pd.to_datetime(f"{date_str} {start_time_str}")
1328
+ end_datetime = pd.to_datetime(f"{date_str} {end_time_str}")
1329
+
1330
+ # For unassigned deliveries set color to 'unassigned'
1331
+ calendar_data.append({
1332
+ 'delivery_id': delivery_id,
1333
+ 'customer_name': customer_name,
1334
+ 'vehicle_id': 'Unassigned',
1335
+ 'driver_name': 'N/A',
1336
+ 'vehicle_type': 'N/A',
1337
+ 'priority': priority,
1338
+ 'time_window': time_window,
1339
+ 'estimated_arrival_mins': 0,
1340
+ 'estimated_arrival_time': 'N/A',
1341
+ 'weight_kg': weight,
1342
+ 'Start': start_datetime,
1343
+ 'Finish': end_datetime,
1344
+ 'Task': f"{delivery_id}: {customer_name} (UNASSIGNED)",
1345
+ 'Vehicle Task': 'Unassigned',
1346
+ 'on_time': False,
1347
+ 'assigned': False,
1348
+ 'color': 'unassigned', # Color for unassigned
1349
+ 'delivery_date': pd.to_datetime(date_str)
1350
+ })
1351
+ except Exception as e:
1352
+ st.warning(f"Could not process time window for unassigned delivery {delivery_id}: {str(e)}")
1353
+
1354
+ if calendar_data:
1355
+ # Convert to DataFrame
1356
+ cal_df = pd.DataFrame(calendar_data)
1357
+
1358
+ # Create color mapping for on-time status
1359
+ cal_df['Color'] = cal_df['on_time'].map({True: 'rgb(0, 200, 0)', False: 'rgb(255, 0, 0)'})
1360
+
1361
+ # Get all available dates
1362
+ all_dates = sorted(cal_df['delivery_date'].dt.date.unique())
1363
+
1364
+ # Format dates for display in the dropdown
1365
+ date_options = {date.strftime('%b %d, %Y'): date for date in all_dates}
1366
+
1367
+ # Initialize calendar display dates if not already set or if dates have changed
1368
+ available_date_keys = list(date_options.keys())
1369
+
1370
+ # Default to all dates
1371
+ if st.session_state.calendar_display_dates is None or not all(date in available_date_keys for date in st.session_state.calendar_display_dates):
1372
+ st.session_state.calendar_display_dates = available_date_keys
1373
+
1374
+ # Add multiselect for date filtering with session state
1375
+ selected_date_strings = st.multiselect(
1376
+ "Select dates to display",
1377
+ options=available_date_keys,
1378
+ default=st.session_state.calendar_display_dates,
1379
+ key="calendar_date_selector"
1380
+ )
1381
+
1382
+ # Update the session state
1383
+ st.session_state.calendar_display_dates = selected_date_strings
1384
+
1385
+ # Convert selected strings back to date objects
1386
+ selected_dates = [date_options[date_str] for date_str in selected_date_strings]
1387
+
1388
+ if not selected_dates:
1389
+ st.info("Please select at least one date to view the delivery schedule.")
1390
+ else:
1391
+ # Filter calendar data to only include selected dates
1392
+ filtered_cal_df = cal_df[cal_df['delivery_date'].dt.date.isin(selected_dates)]
1393
+
1394
+ # Create tabs only for the selected dates
1395
+ date_tabs = st.tabs([date.strftime('%b %d, %Y') for date in selected_dates])
1396
+
1397
+ for i, (date, tab) in enumerate(zip(selected_dates, date_tabs)):
1398
+ with tab:
1399
+ # Filter data for this date
1400
+ day_data = filtered_cal_df[filtered_cal_df['delivery_date'].dt.date == date]
1401
+
1402
+ if len(day_data) > 0:
1403
+ # FIRST SECTION: DELIVERY SCHEDULE VIEW
1404
+ st.write("#### Delivery Schedule")
1405
+
1406
+ # Create figure for delivery view
1407
+ fig = px.timeline(
1408
+ day_data,
1409
+ x_start="Start",
1410
+ x_end="Finish",
1411
+ y="Task",
1412
+ color="color", # Use our color column
1413
+ color_discrete_map={
1414
+ "on_time": "green",
1415
+ "late": "orange",
1416
+ "unassigned": "red" # Unassigned deliveries also red
1417
+ },
1418
+ hover_data=["customer_name", "vehicle_id", "driver_name", "priority", "time_window",
1419
+ "estimated_arrival_time", "weight_kg", "assigned"]
1420
+ )
1421
+
1422
+ # Fix the pattern application code
1423
+ for i, row in day_data.iterrows():
1424
+ # Only add diagonal pattern to assigned deliveries
1425
+ if row['assigned']:
1426
+ for trace in fig.data:
1427
+ # Find which trace corresponds to this row's color group
1428
+ color_value = row['color']
1429
+
1430
+ # Look for matching trace
1431
+ if trace.name == color_value and any(y == row['Task'] for y in trace.y):
1432
+ # Add pattern only to assigned bars
1433
+ if 'marker' not in trace:
1434
+ trace.marker = dict()
1435
+ if 'pattern' not in trace.marker:
1436
+ trace.marker.pattern = dict(
1437
+ shape="\\", # Diagonal lines
1438
+ size=4,
1439
+ solidity=0.5,
1440
+ fgcolor="black"
1441
+ )
1442
+
1443
+ # Add status labels to the bars
1444
+ for idx, row in day_data.iterrows():
1445
+ status_text = "✓ On-time" if row['on_time'] and row['assigned'] else "⚠ Late" if row['assigned'] else "Not assigned"
1446
+ position = (row['Start'] + (row['Finish'] - row['Start'])/2)
1447
+
1448
+ # Only add labels to assigned deliveries
1449
+ if row['assigned']:
1450
+ fig.add_annotation(
1451
+ x=position,
1452
+ y=row['Task'],
1453
+ text=status_text,
1454
+ showarrow=False,
1455
+ font=dict(color="black", size=10),
1456
+ xanchor="center"
1457
+ )
1458
+
1459
+ # Update layout
1460
+ fig.update_layout(
1461
+ title=f"Deliveries by Customer - {date.strftime('%b %d, %Y')}",
1462
+ xaxis_title="Time of Day",
1463
+ yaxis_title="Delivery",
1464
+ height=max(300, 50 * len(day_data)),
1465
+ yaxis={'categoryorder':'category ascending'},
1466
+ showlegend=False # Hide the legend as we have custom annotations
1467
+ )
1468
+
1469
+ # Display figure
1470
+ st.plotly_chart(fig, use_container_width=True)
1471
+
1472
+ # Show summary metrics for delivery view
1473
+ col1, col2, col3, col4 = st.columns(4)
1474
+ with col1:
1475
+ st.metric("Total Deliveries", len(day_data))
1476
+ with col2:
1477
+ st.metric("On-Time Deliveries", len(day_data[day_data['on_time']]))
1478
+ with col3:
1479
+ st.metric("Late Deliveries", len(day_data[~day_data['on_time']]))
1480
+ with col4:
1481
+ if 'weight_kg' in day_data.columns:
1482
+ st.metric("Total Weight", f"{day_data['weight_kg'].sum():.2f} kg")
1483
+
1484
+ # Add breakdown of deliveries by priority
1485
+ if 'priority' in day_data.columns:
1486
+ st.write("##### Deliveries by Priority")
1487
+ priority_counts = day_data['priority'].value_counts()
1488
+ priority_cols = st.columns(min(4, len(priority_counts)))
1489
+
1490
+ for j, (priority, count) in enumerate(priority_counts.items()):
1491
+ col_idx = j % len(priority_cols)
1492
+ with priority_cols[col_idx]:
1493
+ st.metric(priority, count)
1494
+
1495
+ # SECOND SECTION: VEHICLE SCHEDULE VIEW
1496
+ st.write("#### Vehicle Schedule")
1497
+
1498
+ # Create figure grouped by vehicle
1499
+ fig_vehicle = px.timeline(
1500
+ day_data,
1501
+ x_start="Start",
1502
+ x_end="Finish",
1503
+ y="Vehicle Task",
1504
+ color="on_time",
1505
+ color_discrete_map={True: "green", False: "red"},
1506
+ hover_data=["delivery_id", "customer_name", "priority", "time_window",
1507
+ "estimated_arrival_time", "weight_kg"]
1508
+ )
1509
+
1510
+ # Add labels for each delivery to the bars
1511
+ for idx, row in day_data.iterrows():
1512
+ fig_vehicle.add_annotation(
1513
+ x=(row['Start'] + (row['Finish'] - row['Start'])/2),
1514
+ y=row['Vehicle Task'],
1515
+ text=f"#{row['delivery_id']}",
1516
+ showarrow=False,
1517
+ font=dict(size=10, color="black")
1518
+ )
1519
+
1520
+ # Update layout
1521
+ fig_vehicle.update_layout(
1522
+ title=f"Vehicle Assignment Schedule - {date.strftime('%b %d, %Y')}",
1523
+ xaxis_title="Time of Day",
1524
+ yaxis_title="Vehicle",
1525
+ height=max(300, 70 * day_data['Vehicle Task'].nunique()),
1526
+ yaxis={'categoryorder':'category ascending'}
1527
+ )
1528
+
1529
+ # Display figure for vehicle view
1530
+ st.plotly_chart(fig_vehicle, use_container_width=True)
1531
+
1532
+ # Show vehicle utilization summary
1533
+ st.write("##### Vehicle Utilization")
1534
+
1535
+ # Calculate vehicle utilization metrics
1536
+ vehicle_metrics = []
1537
+ for vehicle_id in day_data['vehicle_id'].unique():
1538
+ vehicle_deliveries = day_data[day_data['vehicle_id'] == vehicle_id]
1539
+
1540
+ # Calculate total delivery time for this vehicle
1541
+ total_mins = sum((row['Finish'] - row['Start']).total_seconds() / 60 for _, row in vehicle_deliveries.iterrows())
1542
+
1543
+ # Count on-time deliveries
1544
+ on_time_count = len(vehicle_deliveries[vehicle_deliveries['on_time'] == True])
1545
+
1546
+ # Get the driver name
1547
+ driver_name = vehicle_deliveries['driver_name'].iloc[0] if not vehicle_deliveries.empty else "Unknown"
1548
+
1549
+ vehicle_metrics.append({
1550
+ 'vehicle_id': vehicle_id,
1551
+ 'driver_name': driver_name,
1552
+ 'deliveries': len(vehicle_deliveries),
1553
+ 'delivery_time_mins': total_mins,
1554
+ 'on_time_deliveries': on_time_count,
1555
+ 'on_time_percentage': (on_time_count / len(vehicle_deliveries)) * 100 if len(vehicle_deliveries) > 0 else 0
1556
+ })
1557
+
1558
+ # Display metrics in a nice format
1559
+ metrics_df = pd.DataFrame(vehicle_metrics)
1560
+
1561
+ # Show as a table
1562
+ st.dataframe(metrics_df.style.format({
1563
+ 'delivery_time_mins': '{:.0f}',
1564
+ 'on_time_percentage': '{:.1f}%'
1565
+ }))
1566
+
1567
+ else:
1568
+ st.info(f"No deliveries scheduled for {date.strftime('%b %d, %Y')}")
1569
+ else:
1570
+ st.info("No calendar data available. Please generate routes first.")
1571
+
1572
+ def create_distance_matrix(locations):
1573
+ """
1574
+ Create a simple Euclidean distance matrix between locations
1575
+
1576
+ In a real implementation, this would be replaced by actual road distances
1577
+
1578
+ Parameters:
1579
+ locations (list): List of location dictionaries with lat and lon
1580
+
1581
+ Returns:
1582
+ numpy.ndarray: Distance matrix
1583
+ """
1584
+ n = len(locations)
1585
+ matrix = np.zeros((n, n))
1586
+ for i in range(n):
1587
+ for j in range(n):
1588
+ if i == j:
1589
+ continue
1590
+
1591
+ # Approximate distance in km (very rough)
1592
+ lat1, lon1 = locations[i]['latitude'], locations[i]['longitude']
1593
+ lat2, lon2 = locations[j]['latitude'], locations[j]['longitude']
1594
+
1595
+ # Simple Euclidean distance (for demo purposes)
1596
+ # In reality, we'd use actual road distances
1597
+ dist = ((lat1 - lat2) ** 2 + (lon1 - lon2) ** 2) ** 0.5 * 111
1598
+ matrix[i, j] = dist
1599
+
1600
+ return matrix
1601
+
1602
+ def get_road_route(start_point, end_point):
1603
+ """
1604
+ Get a route that follows actual roads between two points using OpenStreetMap's routing service.
1605
+
1606
+ Args:
1607
+ start_point: (lat, lon) tuple of start location
1608
+ end_point: (lat, lon) tuple of end location
1609
+
1610
+ Returns:
1611
+ list: List of (lat, lon) points representing the actual road route
1612
+ """
1613
+ try:
1614
+ # OSRM expects coordinates in lon,lat format
1615
+ start_lat, start_lon = start_point
1616
+ end_lat, end_lon = end_point
1617
+
1618
+ # Build the API URL for OSRM (OpenStreetMap Routing Machine)
1619
+ url = f"http://router.project-osrm.org/route/v1/driving/{start_lon},{start_lat};{end_lon},{end_lat}"
1620
+ params = {
1621
+ "overview": "full",
1622
+ "geometries": "geojson",
1623
+ "steps": "true"
1624
+ }
1625
+
1626
+ # Replace direct text output with spinner
1627
+ with st.spinner(f"Getting route from ({start_lat:.4f}, {start_lon:.4f}) to ({end_lat:.4f}, {end_lon:.4f})..."):
1628
+ response = requests.get(url, params=params, timeout=5)
1629
+
1630
+ if response.status_code == 200:
1631
+ data = response.json()
1632
+
1633
+ # Check if a route was found
1634
+ if data['code'] == 'Ok' and len(data['routes']) > 0:
1635
+ # Extract the geometry (list of coordinates) from the response
1636
+ geometry = data['routes'][0]['geometry']['coordinates']
1637
+
1638
+ # OSRM returns points as [lon, lat], but we need [lat, lon]
1639
+ route_points = [(lon, lat) for lat, lon in geometry]
1640
+ return route_points
1641
+
1642
+ # If we get here, something went wrong with the API call
1643
+ st.warning(f"Could not get road route: {response.status_code} - {response.text if response.status_code != 200 else 'No routes found'}")
1644
+
1645
+ except Exception as e:
1646
+ st.warning(f"Error getting road route: {str(e)}")
1647
+
1648
+ # Fallback to our approximation method if the API call fails
1649
+ with st.spinner("Generating approximate route..."):
1650
+ # Create a more sophisticated approximation with higher density of points
1651
+ start_lat, start_lon = start_point
1652
+ end_lat, end_lon = end_point
1653
+
1654
+ # Calculate the direct distance
1655
+ direct_dist = ((start_lat - end_lat)**2 + (start_lon - end_lon)**2)**0.5
1656
+
1657
+ # Generate more points for longer distances
1658
+ num_points = max(10, int(direct_dist * 10000)) # Scale based on distance
1659
+
1660
+ # Create a path with small random deviations to look like a road
1661
+ route_points = []
1662
+
1663
+ # Starting point
1664
+ route_points.append((start_lat, start_lon))
1665
+
1666
+ # Calculate major waypoints - like going through major roads
1667
+ # Find a midpoint that's slightly off the direct line
1668
+ mid_lat = (start_lat + end_lat) / 2
1669
+ mid_lon = (start_lon + end_lon) / 2
1670
+
1671
+ # Add some perpendicular deviation to simulate taking streets
1672
+ # Get perpendicular direction
1673
+ dx = end_lat - start_lat
1674
+ dy = end_lon - start_lon
1675
+
1676
+ # Perpendicular direction
1677
+ perpendicular_x = -dy
1678
+ perpendicular_y = dx
1679
+
1680
+ # Normalize and scale
1681
+ magnitude = (perpendicular_x**2 + perpendicular_y**2)**0.5
1682
+ if magnitude > 0:
1683
+ perpendicular_x /= magnitude
1684
+ perpendicular_y /= magnitude
1685
+
1686
+ # Scale the perpendicular offset based on distance
1687
+ offset_scale = direct_dist * 0.2 # 20% of direct distance
1688
+
1689
+ # Apply offset to midpoint
1690
+ mid_lat += perpendicular_x * offset_scale * random.choice([-1, 1])
1691
+ mid_lon += perpendicular_y * offset_scale * random.choice([-1, 1])
1692
+
1693
+ # Generate a smooth path from start to midpoint
1694
+ for i in range(1, num_points // 2):
1695
+ t = i / (num_points // 2)
1696
+ # Quadratic Bezier curve parameters
1697
+ u = 1 - t
1698
+ lat = u**2 * start_lat + 2 * u * t * mid_lat + t**2 * mid_lat
1699
+ lon = u**2 * start_lon + 2 * u * t * mid_lon + t**2 * mid_lon
1700
+
1701
+ # Add small random noise to make it look like following streets
1702
+ noise_scale = 0.0002 * direct_dist
1703
+ lat += random.uniform(-noise_scale, noise_scale)
1704
+ lon += random.uniform(-noise_scale, noise_scale)
1705
+
1706
+ route_points.append((lat, lon))
1707
+
1708
+ # Generate a smooth path from midpoint to end
1709
+ for i in range(num_points // 2, num_points):
1710
+ t = (i - num_points // 2) / (num_points // 2)
1711
+ # Quadratic Bezier curve parameters
1712
+ u = 1 - t
1713
+ lat = u**2 * mid_lat + 2 * u * t * mid_lat + t**2 * end_lat
1714
+ lon = u**2 * mid_lon + 2 * u * t * mid_lon + t**2 * end_lon
1715
+
1716
+ # Add small random noise to make it look like following streets
1717
+ noise_scale = 0.0002 * direct_dist
1718
+ lat += random.uniform(-noise_scale, noise_scale)
1719
+ lon += random.uniform(-noise_scale, noise_scale)
1720
+
1721
+ route_points.append((lat, lon))
1722
+
1723
+ # Ending point
1724
+ route_points.append((end_lat, end_lon))
1725
+
1726
+ return route_points
1727
+
1728
+ # Add this condition to make the function importable
1729
+ if __name__ == "__main__":
1730
+ st.set_page_config(
1731
+ page_title="Route Optimizer - Delivery Route Optimization",
1732
+ page_icon="🛣️",
1733
+ layout="wide"
1734
+ )
1735
+ optimize_page()