Spaces:
Sleeping
Sleeping
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. | |
""") | |
# Add help section with expander | |
with st.expander("📚 How to Use the Map 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. | |
""") | |
# Initialize session state variables for filters | |
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 | |
} | |
# Create filters in sidebar | |
with st.sidebar: | |
st.header("Map Filters") | |
# Show/hide options - use session state values as defaults | |
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/hide data table | |
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 | |
# Choose visualization tabs | |
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 to load data | |
try: | |
# Get data paths | |
root_dir = Path(__file__).resolve().parent.parent.parent # Go up to project root level | |
delivery_path = os.path.join(root_dir, 'data', 'delivery-data', 'delivery_data.csv') # Fixed directory name with underscore | |
vehicle_path = os.path.join(root_dir, 'data', 'vehicle-data', 'vehicle_data.csv') # Fixed directory name with underscore | |
# Check if files exist | |
if not os.path.exists(delivery_path): | |
# Try with hyphen instead of underscore | |
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): | |
# Try with hyphen instead of underscore | |
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 | |
# Load data | |
delivery_data = pd.read_csv(delivery_path) | |
vehicle_data = pd.read_csv(vehicle_path) | |
# Ensure delivery_date is properly formatted as datetime | |
if 'delivery_date' in delivery_data.columns: | |
delivery_data['delivery_date'] = pd.to_datetime(delivery_data['delivery_date']) | |
# Add more filters if data is available - CONVERT TO MULTI-SELECT | |
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: | |
# Get the min/max dates from the ORIGINAL unfiltered data | |
# Load original data to get proper date range | |
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() | |
# Get saved values from session state | |
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] | |
# Validate saved dates - ensure they're within allowed range | |
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 | |
# Set default values with proper validation | |
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) | |
# Add date range picker | |
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" | |
) | |
# Update session state with new date range | |
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: | |
# If there's any error with the date range, reset it | |
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] | |
# MOVED STATISTICS TO THE TOP | |
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) | |
# Status count columns - dynamic based on available statuses | |
if 'status' in delivery_data.columns: | |
status_counts = delivery_data['status'].value_counts() | |
# Create a varying number of columns based on unique statuses | |
status_cols = st.columns(len(status_counts)) | |
for i, (status, count) in enumerate(status_counts.items()): | |
with status_cols[i]: | |
# Choose color based on status | |
delta_color = "normal" | |
if status == "Delivered": | |
delta_color = "off" | |
elif status == "In Transit": | |
delta_color = "normal" | |
elif status == "Pending": | |
delta_color = "inverse" # Red | |
# Calculate percentage | |
percentage = round((count / len(delivery_data)) * 100, 1) | |
st.metric( | |
f"{status}", | |
count, | |
f"{percentage}% of total", | |
delta_color=delta_color | |
) | |
# Create map | |
singapore_coords = [1.3521, 103.8198] # Center of Singapore | |
m = folium.Map(location=singapore_coords, zoom_start=12) | |
# Add delivery markers | |
if show_deliveries: | |
for _, row in delivery_data.iterrows(): | |
# Create popup content | |
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>" | |
# Choose marker color based on priority | |
color = 'blue' | |
if 'priority' in row: | |
if row['priority'] == 'High': | |
color = 'red' | |
elif row['priority'] == 'Medium': | |
color = 'orange' | |
# Add marker to map | |
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) | |
# Add depot markers | |
if show_depots: | |
for _, row in vehicle_data.iterrows(): | |
# Create popup content | |
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>" | |
# Add marker to map | |
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) | |
# Display the map | |
folium_static(m, width=800, height=500) | |
# Display calendar visualization if selected | |
if show_calendar and 'delivery_date' in delivery_data.columns and 'time_window' in delivery_data.columns: | |
st.subheader("Delivery Schedule Calendar") | |
# Process data for calendar view | |
calendar_data = delivery_data.copy() | |
# Extract start and end times from time_window | |
calendar_data[['start_time', 'end_time']] = calendar_data['time_window'].str.split('-', expand=True) | |
# Create start and end datetime for each delivery | |
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'] | |
) | |
# Create task column for Gantt chart | |
calendar_data['Task'] = calendar_data['delivery_id'] + ': ' + calendar_data['customer_name'] | |
# Create color mapping for priority | |
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)' # Default blue | |
# Get all available dates and add ƒmulti-select filter | |
all_dates = sorted(calendar_data['delivery_date'].dt.date.unique()) | |
# Format dates for display in the dropdown | |
date_options = {date.strftime('%b %d, %Y'): date for date in all_dates} | |
# Get default selection from session state | |
default_selections = st.session_state.map_filters.get('calendar_selected_dates', []) | |
# Validate default selections - only keep dates that exist in current options | |
valid_default_selections = [date_str for date_str in default_selections if date_str in date_options.keys()] | |
# If no valid selections remain, default to first date (if available) | |
if not valid_default_selections and date_options: | |
valid_default_selections = [list(date_options.keys())[0]] | |
# Add multiselect for date filtering with validated defaults | |
selected_date_strings = st.multiselect( | |
"Select dates to display", | |
options=list(date_options.keys()), | |
default=valid_default_selections, | |
key="calendar_date_selector" | |
) | |
# Save selections to session state | |
st.session_state.map_filters['calendar_selected_dates'] = selected_date_strings | |
# Convert selected strings back to date objects | |
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: | |
# Filter calendar data to only include selected dates | |
filtered_calendar = calendar_data[calendar_data['delivery_date'].dt.date.isin(selected_dates)] | |
# Group tasks by date for better visualization | |
date_groups = filtered_calendar.groupby(filtered_calendar['delivery_date'].dt.date) | |
# Create tabs only for the selected dates | |
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: | |
# Filter data for this date | |
day_data = filtered_calendar[filtered_calendar['delivery_date'].dt.date == date] | |
if len(day_data) > 0: | |
# Create figure | |
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"] | |
) | |
# Update layout | |
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'} | |
) | |
# Display figure | |
st.plotly_chart(fig, use_container_width=True) | |
# Show summary | |
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'])) | |
# NEW - Add delivery status breakdown for this day | |
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')}") | |
# Display raw data table if selected | |
if show_data_table: | |
st.subheader("Delivery Data") | |
# Create a copy for display | |
display_df = delivery_data.copy() | |
# Convert delivery_date back to string for display | |
if 'delivery_date' in display_df.columns: | |
display_df['delivery_date'] = display_df['delivery_date'].dt.strftime('%b %d, %Y') | |
# Compute which deliveries are urgent (next 7 days) | |
if 'delivery_date' in delivery_data.columns: | |
today = datetime.now().date() | |
next_week = today + timedelta(days=7) | |
# Function to highlight rows based on delivery status and urgency | |
def highlight_rows(row): | |
delivery_date = pd.to_datetime(row['delivery_date']).date() if 'delivery_date' in row else None | |
# Check status first - highlight delivered rows in green | |
if 'status' in row and row['status'] == 'Delivered': | |
return ['background-color: rgba(0, 255, 0, 0.1)'] * len(row) | |
# Then check for urgent high-priority deliveries - highlight in red | |
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) | |
# Display styled dataframe | |
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) # Detailed error for debugging | |
# Make the function executable when file is run directly | |
if __name__ == "__main__": | |
# This is for debugging/testing the function independently | |
st.set_page_config(page_title="Map View - Delivery Route Optimization", page_icon="🗺️", layout="wide") | |
map_page() |