|
import streamlit as st |
|
import folium |
|
from streamlit_folium import folium_static |
|
import pandas as pd |
|
import os |
|
import plotly.figure_factory as ff |
|
import plotly.express as px |
|
from pathlib import Path |
|
from datetime import datetime, timedelta |
|
import numpy as np |
|
|
|
def map_page(): |
|
""" |
|
Render the map visualization page with delivery and depot locations. |
|
Can be called from app.py to display within the main application. |
|
""" |
|
st.title("Delivery Route Map") |
|
st.write(""" |
|
This page visualizes the delivery locations and vehicle depots on an interactive map. |
|
Use the filters in the sidebar to customize the view. |
|
""") |
|
|
|
|
|
|
|
with st.expander("๐ How to Use this Page"): |
|
st.markdown(""" |
|
## Step-by-Step Guide to the Map Page |
|
|
|
The Map page provides an interactive visualization of all delivery locations and vehicle depots. It helps you understand delivery distribution, monitor delivery status, and plan logistics operations. |
|
|
|
### 1. Map Navigation |
|
|
|
- **Pan**: Click and drag to move around the map |
|
- **Zoom**: Use the scroll wheel or the +/- buttons in the top-left corner |
|
- **View Details**: Click on any marker to see detailed information about that delivery or depot |
|
|
|
### 2. Using Map Filters (Sidebar) |
|
|
|
- **Show/Hide Elements**: |
|
- Toggle "Show Deliveries" to display or hide delivery markers |
|
- Toggle "Show Depots" to display or hide vehicle depot markers |
|
- Enable "Show Data Table" to view raw delivery data below the map |
|
- Enable "Show Calendar View" to see delivery schedules organized by date |
|
|
|
- **Filter by Attributes**: |
|
- Use "Filter by Priority" to show only deliveries of selected priority levels (High, Medium, Low) |
|
- Use "Filter by Status" to show only deliveries with selected statuses (Pending, In Transit, Delivered) |
|
|
|
- **Date Filtering**: |
|
- Use the "Date Range" selector to focus on deliveries within specific dates |
|
- This affects both the map display and the calendar view |
|
|
|
### 3. Understanding the Map Markers |
|
|
|
- **Delivery Markers**: |
|
- Red markers: High priority deliveries |
|
- Orange markers: Medium priority deliveries |
|
- Blue markers: Low priority deliveries |
|
|
|
- **Depot Markers**: |
|
- Green house icons: Vehicle depot locations |
|
|
|
### 4. Using the Calendar View |
|
|
|
- Select specific dates from the dropdown to view scheduled deliveries |
|
- Each tab shows deliveries for one selected date |
|
- Timeline bars are color-coded by priority (red=High, orange=Medium, blue=Low) |
|
- Hover over timeline bars to see detailed delivery information |
|
- Check the summary metrics below each calendar for quick insights |
|
|
|
### 5. Reading the Delivery Statistics |
|
|
|
- The top section shows key metrics about displayed deliveries: |
|
- Total number of deliveries shown |
|
- Total weight of all displayed deliveries |
|
- Number of pending deliveries |
|
- Breakdown of deliveries by status |
|
|
|
### 6. Data Table Features |
|
|
|
When "Show Data Table" is enabled: |
|
- Green highlighted rows: Completed deliveries |
|
- Red highlighted rows: Urgent high-priority deliveries due within the next week |
|
- Sort any column by clicking the column header |
|
- Search across all fields using the search box |
|
|
|
This map view helps you visualize your delivery operations geographically while the calendar provides a time-based perspective of your delivery schedule. |
|
""") |
|
|
|
|
|
if 'map_filters' not in st.session_state: |
|
st.session_state.map_filters = { |
|
'selected_dates': ["All"], |
|
'priority_filter': [], |
|
'status_filter': [], |
|
'date_range': [None, None], |
|
'show_calendar': True, |
|
'show_map': True, |
|
'show_data_table': False, |
|
'cluster_markers': True |
|
} |
|
|
|
|
|
with st.sidebar: |
|
st.header("Map Filters") |
|
|
|
|
|
show_deliveries = st.checkbox( |
|
"Show Deliveries", |
|
value=st.session_state.map_filters.get('show_deliveries', True), |
|
key="show_deliveries_checkbox" |
|
) |
|
st.session_state.map_filters['show_deliveries'] = show_deliveries |
|
|
|
show_depots = st.checkbox( |
|
"Show Depots", |
|
value=st.session_state.map_filters.get('show_depots', True), |
|
key="show_depots_checkbox" |
|
) |
|
st.session_state.map_filters['show_depots'] = show_depots |
|
|
|
|
|
show_data_table = st.checkbox( |
|
"Show Data Table", |
|
value=st.session_state.map_filters.get('show_data_table', False), |
|
key="show_data_table_checkbox" |
|
) |
|
st.session_state.map_filters['show_data_table'] = show_data_table |
|
|
|
|
|
show_calendar = st.checkbox( |
|
"Show Calendar View", |
|
value=st.session_state.map_filters.get('show_calendar', True), |
|
key="show_calendar_checkbox" |
|
) |
|
st.session_state.map_filters['show_calendar'] = show_calendar |
|
|
|
|
|
try: |
|
|
|
root_dir = Path(__file__).resolve().parent.parent.parent |
|
delivery_path = os.path.join(root_dir, 'data', 'delivery-data', 'delivery_data.csv') |
|
vehicle_path = os.path.join(root_dir, 'data', 'vehicle-data', 'vehicle_data.csv') |
|
|
|
|
|
if not os.path.exists(delivery_path): |
|
|
|
delivery_path = os.path.join(root_dir, 'data', 'delivery-data', 'delivery_data.csv') |
|
if not os.path.exists(delivery_path): |
|
st.warning(f"Delivery data file not found at: {delivery_path}") |
|
st.info("Please generate data first with: python src/utils/generate_all_data.py") |
|
return |
|
|
|
if not os.path.exists(vehicle_path): |
|
|
|
vehicle_path = os.path.join(root_dir, 'data', 'vehicle-data', 'vehicle_data.csv') |
|
if not os.path.exists(vehicle_path): |
|
st.warning(f"Vehicle data file not found at: {vehicle_path}") |
|
st.info("Please generate data first with: python src/utils/generate_all_data.py") |
|
return |
|
|
|
|
|
delivery_data = pd.read_csv(delivery_path) |
|
vehicle_data = pd.read_csv(vehicle_path) |
|
|
|
|
|
if 'delivery_date' in delivery_data.columns: |
|
delivery_data['delivery_date'] = pd.to_datetime(delivery_data['delivery_date']) |
|
|
|
|
|
if 'priority' in delivery_data.columns: |
|
with st.sidebar: |
|
all_priorities = sorted(delivery_data['priority'].unique().tolist()) |
|
selected_priorities = st.multiselect( |
|
"Filter by Priority", |
|
options=all_priorities, |
|
default=st.session_state.map_filters.get('priority_filter', all_priorities), |
|
key="priority_multiselect" |
|
) |
|
st.session_state.map_filters['priority_filter'] = selected_priorities |
|
|
|
if selected_priorities: |
|
delivery_data = delivery_data[delivery_data['priority'].isin(selected_priorities)] |
|
|
|
if 'status' in delivery_data.columns: |
|
with st.sidebar: |
|
all_statuses = sorted(delivery_data['status'].unique().tolist()) |
|
selected_statuses = st.multiselect( |
|
"Filter by Status", |
|
options=all_statuses, |
|
default=st.session_state.map_filters.get('status_filter', all_statuses), |
|
key="status_multiselect" |
|
) |
|
st.session_state.map_filters['status_filter'] = selected_statuses |
|
|
|
if selected_statuses: |
|
delivery_data = delivery_data[delivery_data['status'].isin(selected_statuses)] |
|
|
|
if 'delivery_date' in delivery_data.columns: |
|
with st.sidebar: |
|
|
|
|
|
original_data = pd.read_csv(delivery_path) |
|
if 'delivery_date' in original_data.columns: |
|
original_data['delivery_date'] = pd.to_datetime(original_data['delivery_date']) |
|
|
|
min_date = original_data['delivery_date'].min().date() |
|
max_date = original_data['delivery_date'].max().date() |
|
|
|
|
|
saved_start_date = st.session_state.map_filters.get('date_range', [None, None])[0] |
|
saved_end_date = st.session_state.map_filters.get('date_range', [None, None])[1] |
|
|
|
|
|
if saved_start_date and saved_start_date < min_date: |
|
saved_start_date = min_date |
|
if saved_end_date and saved_end_date > max_date: |
|
saved_end_date = max_date |
|
|
|
|
|
default_start_date = saved_start_date if saved_start_date else min_date |
|
default_end_date = saved_end_date if saved_end_date else min(min_date + timedelta(days=7), max_date) |
|
|
|
|
|
try: |
|
date_range = st.date_input( |
|
"Date Range", |
|
value=(default_start_date, default_end_date), |
|
min_value=min_date, |
|
max_value=max_date, |
|
key="date_range_input" |
|
) |
|
|
|
|
|
if len(date_range) == 2: |
|
st.session_state.map_filters['date_range'] = list(date_range) |
|
start_date, end_date = date_range |
|
mask = (delivery_data['delivery_date'].dt.date >= start_date) & (delivery_data['delivery_date'].dt.date <= end_date) |
|
delivery_data = delivery_data[mask] |
|
except Exception as e: |
|
|
|
st.error(f"Error with date range: {e}") |
|
st.session_state.map_filters['date_range'] = [min_date, max_date] |
|
date_range = (min_date, max_date) |
|
mask = (delivery_data['delivery_date'].dt.date >= min_date) & (delivery_data['delivery_date'].dt.date <= max_date) |
|
delivery_data = delivery_data[mask] |
|
|
|
|
|
st.subheader("Delivery Overview") |
|
col1, col2, col3 = st.columns(3) |
|
|
|
with col1: |
|
st.metric("Deliveries Shown", len(delivery_data)) |
|
|
|
with col2: |
|
if 'weight_kg' in delivery_data.columns: |
|
total_weight = delivery_data['weight_kg'].sum() |
|
st.metric("Total Weight", f"{total_weight:.2f} kg") |
|
|
|
with col3: |
|
if 'status' in delivery_data.columns: |
|
pending = len(delivery_data[delivery_data['status'] == 'Pending']) |
|
st.metric("Pending Deliveries", pending) |
|
|
|
|
|
if 'status' in delivery_data.columns: |
|
status_counts = delivery_data['status'].value_counts() |
|
|
|
status_cols = st.columns(len(status_counts)) |
|
|
|
for i, (status, count) in enumerate(status_counts.items()): |
|
with status_cols[i]: |
|
|
|
delta_color = "normal" |
|
if status == "Delivered": |
|
delta_color = "off" |
|
elif status == "In Transit": |
|
delta_color = "normal" |
|
elif status == "Pending": |
|
delta_color = "inverse" |
|
|
|
|
|
percentage = round((count / len(delivery_data)) * 100, 1) |
|
st.metric( |
|
f"{status}", |
|
count, |
|
f"{percentage}% of total", |
|
delta_color=delta_color |
|
) |
|
|
|
|
|
singapore_coords = [1.3521, 103.8198] |
|
m = folium.Map(location=singapore_coords, zoom_start=12) |
|
|
|
|
|
if show_deliveries: |
|
for _, row in delivery_data.iterrows(): |
|
|
|
popup_content = f"<b>ID:</b> {row['delivery_id']}<br>" |
|
|
|
if 'customer_name' in row: |
|
popup_content += f"<b>Customer:</b> {row['customer_name']}<br>" |
|
|
|
if 'address' in row: |
|
popup_content += f"<b>Address:</b> {row['address']}<br>" |
|
|
|
if 'time_window' in row: |
|
popup_content += f"<b>Time Window:</b> {row['time_window']}<br>" |
|
|
|
if 'priority' in row: |
|
popup_content += f"<b>Priority:</b> {row['priority']}<br>" |
|
|
|
if 'delivery_date' in row: |
|
popup_content += f"<b>Date:</b> {row['delivery_date'].strftime('%b %d, %Y')}<br>" |
|
|
|
if 'status' in row: |
|
popup_content += f"<b>Status:</b> {row['status']}<br>" |
|
|
|
|
|
color = 'blue' |
|
if 'priority' in row: |
|
if row['priority'] == 'High': |
|
color = 'red' |
|
elif row['priority'] == 'Medium': |
|
color = 'orange' |
|
|
|
|
|
folium.Marker( |
|
[row['latitude'], row['longitude']], |
|
popup=folium.Popup(popup_content, max_width=300), |
|
tooltip=f"Delivery {row['delivery_id']}", |
|
icon=folium.Icon(color=color) |
|
).add_to(m) |
|
|
|
|
|
if show_depots: |
|
for _, row in vehicle_data.iterrows(): |
|
|
|
popup_content = f"<b>Vehicle ID:</b> {row['vehicle_id']}<br>" |
|
|
|
if 'vehicle_type' in row: |
|
popup_content += f"<b>Type:</b> {row['vehicle_type']}<br>" |
|
|
|
if 'driver_name' in row: |
|
popup_content += f"<b>Driver:</b> {row['driver_name']}<br>" |
|
|
|
|
|
folium.Marker( |
|
[row['depot_latitude'], row['depot_longitude']], |
|
popup=folium.Popup(popup_content, max_width=300), |
|
tooltip=f"Depot: {row['vehicle_id']}", |
|
icon=folium.Icon(color='green', icon='home', prefix='fa') |
|
).add_to(m) |
|
|
|
|
|
folium_static(m, width=800, height=500) |
|
|
|
|
|
if show_calendar and 'delivery_date' in delivery_data.columns and 'time_window' in delivery_data.columns: |
|
st.subheader("Delivery Schedule Calendar") |
|
|
|
|
|
calendar_data = delivery_data.copy() |
|
|
|
|
|
calendar_data[['start_time', 'end_time']] = calendar_data['time_window'].str.split('-', expand=True) |
|
|
|
|
|
calendar_data['Start'] = pd.to_datetime( |
|
calendar_data['delivery_date'].dt.strftime('%Y-%m-%d') + ' ' + calendar_data['start_time'] |
|
) |
|
calendar_data['Finish'] = pd.to_datetime( |
|
calendar_data['delivery_date'].dt.strftime('%Y-%m-%d') + ' ' + calendar_data['end_time'] |
|
) |
|
|
|
|
|
calendar_data['Task'] = calendar_data['delivery_id'] + ': ' + calendar_data['customer_name'] |
|
|
|
|
|
if 'priority' in calendar_data.columns: |
|
color_map = {'High': 'rgb(255, 0, 0)', 'Medium': 'rgb(255, 165, 0)', 'Low': 'rgb(0, 0, 255)'} |
|
calendar_data['Color'] = calendar_data['priority'].map(color_map) |
|
else: |
|
calendar_data['Color'] = 'rgb(0, 0, 255)' |
|
|
|
|
|
all_dates = sorted(calendar_data['delivery_date'].dt.date.unique()) |
|
|
|
|
|
date_options = {date.strftime('%b %d, %Y'): date for date in all_dates} |
|
|
|
|
|
default_selections = st.session_state.map_filters.get('calendar_selected_dates', []) |
|
|
|
|
|
valid_default_selections = [date_str for date_str in default_selections if date_str in date_options.keys()] |
|
|
|
|
|
if not valid_default_selections and date_options: |
|
valid_default_selections = [list(date_options.keys())[0]] |
|
|
|
|
|
selected_date_strings = st.multiselect( |
|
"Select dates to display", |
|
options=list(date_options.keys()), |
|
default=valid_default_selections, |
|
key="calendar_date_selector" |
|
) |
|
|
|
|
|
st.session_state.map_filters['calendar_selected_dates'] = selected_date_strings |
|
|
|
|
|
selected_dates = [date_options[date_str] for date_str in selected_date_strings] |
|
|
|
if not selected_dates: |
|
st.info("Please select at least one date to view the delivery schedule.") |
|
else: |
|
|
|
filtered_calendar = calendar_data[calendar_data['delivery_date'].dt.date.isin(selected_dates)] |
|
|
|
|
|
date_groups = filtered_calendar.groupby(filtered_calendar['delivery_date'].dt.date) |
|
|
|
|
|
date_tabs = st.tabs([date.strftime('%b %d, %Y') for date in selected_dates]) |
|
|
|
for i, (date, tab) in enumerate(zip(selected_dates, date_tabs)): |
|
with tab: |
|
|
|
day_data = filtered_calendar[filtered_calendar['delivery_date'].dt.date == date] |
|
|
|
if len(day_data) > 0: |
|
|
|
fig = px.timeline( |
|
day_data, |
|
x_start="Start", |
|
x_end="Finish", |
|
y="Task", |
|
color="priority" if 'priority' in day_data.columns else None, |
|
color_discrete_map={"High": "red", "Medium": "orange", "Low": "blue"}, |
|
hover_data=["customer_name", "address", "weight_kg", "status"] |
|
) |
|
|
|
|
|
fig.update_layout( |
|
title=f"Deliveries scheduled for {date.strftime('%b %d, %Y')}", |
|
xaxis_title="Time of Day", |
|
yaxis_title="Delivery", |
|
height=max(300, 50 * len(day_data)), |
|
yaxis={'categoryorder':'category ascending'} |
|
) |
|
|
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
col1, col2, col3 = st.columns(3) |
|
with col1: |
|
st.metric("Total Deliveries", len(day_data)) |
|
with col2: |
|
if 'weight_kg' in day_data.columns: |
|
st.metric("Total Weight", f"{day_data['weight_kg'].sum():.2f} kg") |
|
with col3: |
|
if 'priority' in day_data.columns and 'High' in day_data['priority'].values: |
|
st.metric("High Priority", len(day_data[day_data['priority'] == 'High'])) |
|
|
|
|
|
if 'status' in day_data.columns: |
|
st.write("##### Deliveries by Status") |
|
status_counts = day_data['status'].value_counts() |
|
status_cols = st.columns(min(4, len(status_counts))) |
|
|
|
for i, (status, count) in enumerate(status_counts.items()): |
|
col_idx = i % len(status_cols) |
|
with status_cols[col_idx]: |
|
st.metric(status, count) |
|
else: |
|
st.info(f"No deliveries scheduled for {date.strftime('%b %d, %Y')}") |
|
|
|
|
|
if show_data_table: |
|
st.subheader("Delivery Data") |
|
|
|
|
|
display_df = delivery_data.copy() |
|
|
|
|
|
if 'delivery_date' in display_df.columns: |
|
display_df['delivery_date'] = display_df['delivery_date'].dt.strftime('%b %d, %Y') |
|
|
|
|
|
if 'delivery_date' in delivery_data.columns: |
|
today = datetime.now().date() |
|
next_week = today + timedelta(days=7) |
|
|
|
|
|
def highlight_rows(row): |
|
delivery_date = pd.to_datetime(row['delivery_date']).date() if 'delivery_date' in row else None |
|
|
|
|
|
if 'status' in row and row['status'] == 'Delivered': |
|
return ['background-color: rgba(0, 255, 0, 0.1)'] * len(row) |
|
|
|
elif delivery_date and delivery_date <= next_week and delivery_date >= today and row['priority'] == 'High': |
|
return ['background-color: rgba(255, 0, 0, 0.1)'] * len(row) |
|
else: |
|
return [''] * len(row) |
|
|
|
|
|
st.dataframe(display_df.style.apply(highlight_rows, axis=1)) |
|
else: |
|
st.dataframe(display_df) |
|
|
|
except Exception as e: |
|
st.error(f"Error loading data: {str(e)}") |
|
st.info("Please generate the data first by running: python src/utils/generate_all_data.py") |
|
st.write("Error details:", e) |
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
st.set_page_config(page_title="Map View - Delivery Route Optimization", page_icon="๐บ๏ธ", layout="wide") |
|
map_page() |