add pages src
Browse files- src/pages/__init__.py +8 -0
- src/pages/_about_page.py +97 -0
- src/pages/_contact_page.py +28 -0
- src/pages/_home_page.py +74 -0
- src/pages/_map_page.py +457 -0
- src/pages/_optimize_page.py +1735 -0
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()
|