File size: 83,807 Bytes
dc171c8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53d2b59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dc171c8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
import streamlit as st
import pandas as pd
import numpy as np
import folium
from streamlit_folium import folium_static
import os
from pathlib import Path
from datetime import datetime, timedelta
import matplotlib.pyplot as plt
import random
import time
from ortools.constraint_solver import routing_enums_pb2
from ortools.constraint_solver import pywrapcp
import folium.plugins
from folium.features import DivIcon
import requests
import plotly.express as px

def clear_optimization_results():
    """Clear optimization results when parameters change"""
    if 'optimization_result' in st.session_state:
        st.session_state.optimization_result = None

def optimize_page():
    """
    Render the optimization page with controls for route optimization
    """
    st.title("Delivery Route Optimization")

    # Add help section with expander
    with st.expander("πŸ“š How to Use This Page"):
        st.markdown("""
        ## Step-by-Step Guide to Route Optimization
        
        This application helps you optimize delivery routes by assigning deliveries to vehicles in the most efficient way possible. Follow these steps to get started:
        
        ### 1. Set Optimization Parameters (Sidebar)
        
        - **Select Delivery Dates**: Choose which dates to include in optimization. Select "All" to include all dates.
        - **Priority Importance**: Higher values give more weight to high-priority deliveries.
        - **Time Window Importance**: Higher values enforce stricter adherence to delivery time windows.
        - **Load Balancing vs Distance**: Higher values distribute deliveries more evenly across vehicles.
        - **Maximum Vehicles**: Set the maximum number of vehicles to use for deliveries.
        - **Minimum Time Window Compliance**: Set the minimum percentage of deliveries that must be within their time windows.
        
        ### 2. Generate Routes
        
        - Review the delivery statistics and vehicle availability information
        - Click the **Generate Optimal Routes** button to run the optimization algorithm
        - The algorithm will assign deliveries to vehicles based on your parameters
        
        ### 3. Review Optimization Results
        
        - **Overall Performance**: Check metrics like assigned deliveries, vehicles used, and time window compliance
        - **Time & Distance Distribution**: See how delivery workload is distributed across vehicles
        - **Route Map**: Interactive map showing the optimized routes for each vehicle
          - Use the date filter to show routes for specific days
          - Hover over markers and routes for detailed information
        - **Calendar View**: View delivery schedules organized by date
          - Green bars indicate on-time deliveries
          - Orange bars indicate late deliveries
          - Red bars indicate unassigned deliveries
        
        ### 4. Adjust and Refine
        
        If the results don't meet your requirements:
        
        - **Not enough vehicles?** Increase the maximum vehicles allowed
        - **Time windows not met?** Decrease the time window importance or minimum compliance
        - **High priority deliveries not assigned?** Increase priority importance
        - **Routes too unbalanced?** Increase load balancing parameter
        
        Remember to click **Generate Optimal Routes** after changing any parameters to see the updated results.
        """)
    
    # Initialize session state variables
    if 'optimization_result' not in st.session_state:
        st.session_state.optimization_result = None
    if 'optimization_params' not in st.session_state:
        st.session_state.optimization_params = {
            'priority_weight': 0.3,
            'time_window_weight': 0.5,
            'balance_weight': 0.2,
            'max_vehicles': 5,
            'selected_dates': ["All"]
        }
    if 'calendar_display_dates' not in st.session_state:
        st.session_state.calendar_display_dates = None
    # Add this new session state variable to store calculated road routes
    if 'calculated_road_routes' not in st.session_state:
        st.session_state.calculated_road_routes = {}
    
    # Load data
    data = load_all_data()
    if not data:
        return
    
    delivery_data, vehicle_data, distance_matrix, time_matrix, locations = data
    
    # Optimization parameters
    st.sidebar.header("Optimization Parameters")
    
    # Date selection for deliveries
    if 'delivery_date' in delivery_data.columns:
        available_dates = sorted(delivery_data['delivery_date'].unique())
        date_options = ["All"] + list(available_dates)
        
        # Store current value before selection
        current_selected_dates = st.session_state.optimization_params['selected_dates']
        
        selected_dates = st.sidebar.multiselect(
            "Select Delivery Dates",
            options=date_options,
            default=current_selected_dates,
            key="delivery_date_selector"
        )
        
        # Check if selection changed
        if selected_dates != current_selected_dates:
            clear_optimization_results()
            st.session_state.optimization_params['selected_dates'] = selected_dates
            
        # Handle filtering based on selection
        if "All" not in selected_dates:
            if selected_dates:  # If specific dates were selected
                delivery_data = delivery_data[delivery_data['delivery_date'].isin(selected_dates)]
            elif available_dates:  # No dates selected, show warning
                st.sidebar.warning("No dates selected. Please select at least one delivery date.")
                return
        # If "All" is selected, keep all dates - no filtering needed
    
    # Priority weighting
    current_priority = st.session_state.optimization_params['priority_weight']
    priority_weight = st.sidebar.slider(
        "Priority Importance",
        min_value=0.0,
        max_value=1.0,
        value=current_priority,
        help="Higher values give more importance to high-priority deliveries",
        key="priority_weight",
        on_change=clear_optimization_results
    )

    # Time window importance
    current_time_window = st.session_state.optimization_params['time_window_weight']
    time_window_weight = st.sidebar.slider(
        "Time Window Importance",
        min_value=0.0,
        max_value=1.0,
        value=current_time_window,
        help="Higher values enforce stricter adherence to delivery time windows",
        key="time_window_weight",
        on_change=clear_optimization_results
    )

    # Distance vs load balancing
    current_balance = st.session_state.optimization_params['balance_weight']
    balance_weight = st.sidebar.slider(
        "Load Balancing vs Distance",
        min_value=0.0,
        max_value=1.0,
        value=current_balance,
        help="Higher values prioritize even distribution of deliveries across vehicles over total distance",
        key="balance_weight",
        on_change=clear_optimization_results
    )

    # Max vehicles to use
    available_vehicles = vehicle_data[vehicle_data['status'] == 'Available']
    current_max_vehicles = st.session_state.optimization_params['max_vehicles']
    max_vehicles = st.sidebar.slider(
        "Maximum Vehicles to Use",
        min_value=1,
        max_value=len(available_vehicles),
        value=min(current_max_vehicles, len(available_vehicles)),
        key="max_vehicles",
        on_change=clear_optimization_results
    )

    # Add minimum time window compliance slider
    min_time_window_compliance = st.sidebar.slider(
        "Minimum Time Window Compliance (%)",
        min_value=0,
        max_value=100,
        value=75,
        help="Minimum percentage of deliveries that must be within their time window",
        key="min_time_window_compliance",
        on_change=clear_optimization_results
    )

    # Update session state with new parameter values
    st.session_state.optimization_params['priority_weight'] = priority_weight
    st.session_state.optimization_params['time_window_weight'] = time_window_weight
    st.session_state.optimization_params['balance_weight'] = balance_weight
    st.session_state.optimization_params['max_vehicles'] = max_vehicles

    # # Add a notification when parameters have changed and results need regenerating
    # if ('optimization_result' not in st.session_state or st.session_state.optimization_result is None):
    #     st.warning("⚠️ Optimization parameters have changed. Please click 'Generate Optimal Routes' to update results.")
    
    # Main optimization section
    col1, col2 = st.columns([2, 1])
    
    with col1:
        st.subheader("Delivery Route Optimizer")
        
        # Filter out completed deliveries for statistics
        if 'status' in delivery_data.columns:
            pending_deliveries = delivery_data[delivery_data['status'] != 'Delivered']
            completed_count = len(delivery_data) - len(pending_deliveries)
        else:
            pending_deliveries = delivery_data
            completed_count = 0
            
        st.write(f"Optimizing routes for {len(pending_deliveries)} pending deliveries using up to {max_vehicles} vehicles")
        
        # Statistics
        st.write("#### Delivery Statistics")
        total_count = len(delivery_data)
        pending_count = len(pending_deliveries)
        
        col1a, col1b = st.columns(2)
        with col1a:
            st.metric("Total Deliveries", total_count)
        with col1b:
            st.metric("Pending Deliveries", pending_count, 
                     delta=f"-{completed_count}" if completed_count > 0 else None,
                     delta_color="inverse" if completed_count > 0 else "normal")
        
        if 'priority' in delivery_data.columns:
            # Show priority breakdown for pending deliveries only
            priority_counts = pending_deliveries['priority'].value_counts()
            
            # Display priority counts in a more visual way
            st.write("##### Priority Breakdown")
            priority_cols = st.columns(min(3, len(priority_counts)))
            
            for i, (priority, count) in enumerate(priority_counts.items()):
                col_idx = i % len(priority_cols)
                with priority_cols[col_idx]:
                    st.metric(f"{priority}", count)
        
        if 'weight_kg' in delivery_data.columns:
            # Calculate weight only for pending deliveries
            total_weight = pending_deliveries['weight_kg'].sum()
            st.metric("Total Weight (Pending)", f"{total_weight:.2f} kg")
    
    with col2:
        st.write("#### Vehicle Availability")
        st.write(f"Available Vehicles: {len(available_vehicles)}")
        
        # Show vehicle capacity
        if 'max_weight_kg' in vehicle_data.columns:
            total_capacity = available_vehicles['max_weight_kg'].sum()
            st.write(f"Total Capacity: {total_capacity:.2f} kg")
            
            # Check if we have enough capacity
            if 'weight_kg' in delivery_data.columns:
                if total_capacity < total_weight:
                    st.warning("⚠️ Insufficient vehicle capacity for all deliveries")
                else:
                    st.success("βœ… Sufficient vehicle capacity")
    
    # Run optimization button
    run_optimization_btn = st.button("Generate Optimal Routes")
    
    # Check if we should display results (either have results in session or button was clicked)
    if run_optimization_btn or st.session_state.optimization_result is not None:
        if run_optimization_btn:
            # Run new optimization
            with st.spinner("Calculating optimal routes..."):
                start_time = time.time()
                
                # Filter out completed deliveries before optimization
                if 'status' in delivery_data.columns:
                    pending_deliveries = delivery_data[delivery_data['status'] != 'Delivered']
                else:
                    pending_deliveries = delivery_data
                
                # Prepare data for optimization - USE PENDING DELIVERIES ONLY
                optimization_result = run_optimization(
                    delivery_data=pending_deliveries,
                    vehicle_data=available_vehicles.iloc[:max_vehicles],
                    distance_matrix=distance_matrix,
                    time_matrix=time_matrix,
                    locations=locations,
                    priority_weight=priority_weight,
                    time_window_weight=time_window_weight,
                    balance_weight=balance_weight,
                    min_time_window_compliance=min_time_window_compliance/100.0  # Convert to decimal
                )
                
                end_time = time.time()
                st.success(f"Optimization completed in {end_time - start_time:.2f} seconds")
                
                # Store results in session state
                st.session_state.optimization_result = optimization_result
        else:
            # Use existing results
            optimization_result = st.session_state.optimization_result
            
        # Filter pending deliveries before displaying results    
        if 'status' in delivery_data.columns:
            pending_deliveries = delivery_data[delivery_data['status'] != 'Delivered']
        else:
            pending_deliveries = delivery_data
            
        # Display results with filtered pending deliveries
        display_optimization_results(
            optimization_result=optimization_result,
            delivery_data=pending_deliveries,  # ← CHANGED: Use pending_deliveries instead
            vehicle_data=available_vehicles.iloc[:max_vehicles],
            distance_matrix=distance_matrix,
            time_matrix=time_matrix,
            locations=locations
        )

def load_all_data():
    """
    Load all necessary data for optimization
    
    Returns:
        tuple of (delivery_data, vehicle_data, distance_matrix, time_matrix, locations)
    """
    # Get data paths
    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')
    distance_matrix_path = os.path.join(root_dir, 'data', 'time-matrix', 'distance_matrix.csv')
    time_matrix_path = os.path.join(root_dir, 'data', 'time-matrix', 'base_time_matrix.csv')
    locations_path = os.path.join(root_dir, 'data', 'time-matrix', 'locations.csv')
    
    # Check if files exist
    missing_files = []
    for path, name in [
        (delivery_path, "delivery data"),
        (vehicle_path, "vehicle data"),
        (distance_matrix_path, "distance matrix"),
        (time_matrix_path, "time matrix"),
        (locations_path, "locations data")
    ]:
        if not os.path.exists(path):
            missing_files.append(name)
    
    if missing_files:
        st.error(f"Missing required data: {', '.join(missing_files)}")
        st.info("Please generate all data first by running: python src/utils/generate_all_data.py")
        return None
    
    # Load data
    delivery_data = pd.read_csv(delivery_path)
    vehicle_data = pd.read_csv(vehicle_path)
    distance_matrix = pd.read_csv(distance_matrix_path, index_col=0)
    time_matrix = pd.read_csv(time_matrix_path, index_col=0)
    locations = pd.read_csv(locations_path)
    
    return delivery_data, vehicle_data, distance_matrix, time_matrix, locations

def run_optimization(delivery_data, vehicle_data, distance_matrix, time_matrix, locations, 
                    priority_weight, time_window_weight, balance_weight, min_time_window_compliance=0.75):
    """
    Run the route optimization algorithm using Google OR-Tools
    
    Parameters:
        delivery_data (pd.DataFrame): DataFrame containing delivery information
        vehicle_data (pd.DataFrame): DataFrame containing vehicle information
        distance_matrix (pd.DataFrame): Distance matrix between locations
        time_matrix (pd.DataFrame): Time matrix between locations
        locations (pd.DataFrame): DataFrame with location details
        priority_weight (float): Weight for delivery priority in optimization (Ξ±)
        time_window_weight (float): Weight for time window adherence (Ξ²)
        balance_weight (float): Weight for balancing load across vehicles (Ξ³)
        min_time_window_compliance (float): Minimum required time window compliance (Ξ΄)
        
    Returns:
        dict: Optimization results
    """
    st.write("Setting up optimization model with OR-Tools...")
    
    # Extract required data for optimization
    num_vehicles = len(vehicle_data)
    num_deliveries = len(delivery_data)
    
    # Create a list of all locations (depots + delivery points)
    all_locations = []
    delivery_locations = []
    depot_locations = []
    vehicle_capacities = []
    
    # First, add depot locations (one per vehicle)
    for i, (_, vehicle) in enumerate(vehicle_data.iterrows()):
        depot_loc = {
            'id': vehicle['vehicle_id'],
            'type': 'depot',
            'index': i,  # Important for mapping to OR-Tools indices
            'latitude': vehicle['depot_latitude'],
            'longitude': vehicle['depot_longitude'],
            'vehicle_index': i
        }
        depot_locations.append(depot_loc)
        all_locations.append(depot_loc)
        
        # Add vehicle capacity
        if 'max_weight_kg' in vehicle:
            vehicle_capacities.append(int(vehicle['max_weight_kg'] * 100))  # Convert to integers (OR-Tools works better with integers)
        else:
            vehicle_capacities.append(1000)  # Default capacity of 10kg (1000 in scaled units)
    
    # Then add delivery locations
    for i, (_, delivery) in enumerate(delivery_data.iterrows()):
        # Determine priority factor (will be used in the objective function)
        priority_factor = 1.0
        if 'priority' in delivery:
            if delivery['priority'] == 'High':
                priority_factor = 0.5  # Higher priority = lower cost
            elif delivery['priority'] == 'Low':
                priority_factor = 2.0  # Lower priority = higher cost
        
        # Calculate delivery demand (weight)
        demand = int(delivery.get('weight_kg', 1.0) * 100)  # Convert to integers
        
        delivery_loc = {
            'id': delivery['delivery_id'],
            'type': 'delivery',
            'index': num_vehicles + i,  # Important for mapping to OR-Tools indices
            'latitude': delivery['latitude'],
            'longitude': delivery['longitude'],
            'priority': delivery.get('priority', 'Medium'),
            'priority_factor': priority_factor,
            'weight_kg': delivery.get('weight_kg', 1.0),
            'demand': demand,
            'time_window': delivery.get('time_window', '09:00-17:00'),
            'customer_name': delivery.get('customer_name', 'Unknown')
        }
        delivery_locations.append(delivery_loc)
        all_locations.append(delivery_loc)
    
    # Create distance and time matrices for OR-Tools
    dist_matrix = np.zeros((len(all_locations), len(all_locations)))
    time_matrix_mins = np.zeros((len(all_locations), len(all_locations)))
    
    # Use the provided distance_matrix if it's the right size, otherwise compute distances
    if isinstance(distance_matrix, pd.DataFrame) and len(distance_matrix) == len(all_locations):
        # Convert dataframe to numpy array
        dist_matrix = distance_matrix.values
        time_matrix_mins = time_matrix.values
    else:
        # Compute simple Euclidean distances (this is a fallback)
        for i in range(len(all_locations)):
            for j in range(len(all_locations)):
                if i == j:
                    continue
                
                # Approximate distance in km (very rough)
                lat1, lon1 = all_locations[i]['latitude'], all_locations[i]['longitude']
                lat2, lon2 = all_locations[j]['latitude'], all_locations[j]['longitude']
                
                # Simple Euclidean distance (for demo purposes)
                dist = ((lat1 - lat2) ** 2 + (lon1 - lon2) ** 2) ** 0.5 * 111  # Convert to km
                dist_matrix[i, j] = dist
                time_matrix_mins[i, j] = dist * 2  # Rough estimate: 30km/h -> 2 mins per km
    
    # Prepare demand array (0 for depots, actual demand for deliveries)
    demands = [0] * num_vehicles + [d['demand'] for d in delivery_locations]
    
    # Calculate total weight of all deliveries
    total_delivery_weight = sum(d['demand'] for d in delivery_locations)
    
    # OR-Tools setup
    # Create the routing index manager
    manager = pywrapcp.RoutingIndexManager(
        len(all_locations),  # Number of nodes (depots + deliveries)
        num_vehicles,        # Number of vehicles
        list(range(num_vehicles)),  # Vehicle start nodes (depot indices)
        list(range(num_vehicles))   # Vehicle end nodes (back to depots)
    )
    
    # Create Routing Model
    routing = pywrapcp.RoutingModel(manager)
    
    # Define distance callback with priority weighting
    # This implements the objective function: min sum_{i,j,k} c_jk * x_ijk * p_k^Ξ±
    def distance_callback(from_index, to_index):
        """Returns the weighted distance between the two nodes."""
        # Convert from routing variable Index to distance matrix NodeIndex
        from_node = manager.IndexToNode(from_index)
        to_node = manager.IndexToNode(to_index)
        
        # Get base distance
        base_distance = int(dist_matrix[from_node, to_node] * 1000)  # Convert to integers
        
        # Apply priority weighting to destination node (if it's a delivery)
        if to_node >= num_vehicles:  # It's a delivery node
            delivery_idx = to_node - num_vehicles
            # Apply the priority factor with the priority weight (Ξ±)
            priority_factor = delivery_locations[delivery_idx]['priority_factor']
            # Higher priority_weight = stronger effect of priority on cost
            priority_multiplier = priority_factor ** priority_weight
            return int(base_distance * priority_multiplier)
        
        return base_distance
    
    # Define time callback
    def time_callback(from_index, to_index):
        """Returns the travel time between the two nodes."""
        # Convert from routing variable Index to time matrix NodeIndex
        from_node = manager.IndexToNode(from_index)
        to_node = manager.IndexToNode(to_index)
        return int(time_matrix_mins[from_node, to_node] * 60)  # Convert minutes to seconds (integers)
    
    # Define service time callback - time spent at each delivery
    def service_time_callback(from_index):
        """Returns the service time for the node."""
        # Service time is 0 for depots and 10 minutes (600 seconds) for deliveries
        node_idx = manager.IndexToNode(from_index)
        if node_idx >= num_vehicles:  # It's a delivery node
            return 600  # 10 minutes in seconds
        return 0  # No service time for depots
    
    # Define demand callback
    def demand_callback(from_index):
        """Returns the demand of the node."""
        # Convert from routing variable Index to demands array
        from_node = manager.IndexToNode(from_index)
        return demands[from_node]
    
    # Register callbacks
    transit_callback_index = routing.RegisterTransitCallback(distance_callback)
    time_callback_index = routing.RegisterTransitCallback(time_callback)
    service_callback_index = routing.RegisterUnaryTransitCallback(service_time_callback)
    demand_callback_index = routing.RegisterUnaryTransitCallback(demand_callback)
    
    # Set the arc cost evaluator for all vehicles - this is our objective function
    routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
    
    # Add capacity dimension - Hard Constraint 2: Vehicle Capacity Limits
    routing.AddDimensionWithVehicleCapacity(
        demand_callback_index,
        0,                   # null capacity slack
        vehicle_capacities,  # vehicle maximum capacities
        True,                # start cumul to zero
        'Capacity'
    )
    
    capacity_dimension = routing.GetDimensionOrDie('Capacity')
    
    # Add load balancing penalties - Soft Constraint 3: Load Balancing Penalties
    if balance_weight > 0.01:
        # Calculate target weight per vehicle (ideal balanced load)
        target_weight = total_delivery_weight / len(vehicle_capacities)
        
        for i in range(num_vehicles):
            # Get vehicle capacity
            vehicle_capacity = vehicle_capacities[i]
            
            # Set penalties for deviating from balanced load
            # Scale penalty based on the balance_weight parameter (Ξ³)
            balance_penalty = int(10000 * balance_weight)
            
            # Add soft bounds around the target weight
            # Lower bound: Don't penalize for being under the target if there's not enough weight
            lower_target = max(0, int(target_weight * 0.8))
            capacity_dimension.SetCumulVarSoftLowerBound(
                routing.End(i), lower_target, balance_penalty
            )
            
            # Upper bound: Penalize for going over the target
            # But allow using more capacity if necessary to assign all deliveries
            upper_target = min(vehicle_capacity, int(target_weight * 1.2))
            capacity_dimension.SetCumulVarSoftUpperBound(
                routing.End(i), upper_target, balance_penalty
            )
    
    # Add time dimension with service times
    # This implements Hard Constraint 5: Time Continuity and 
    # Hard Constraint 6: Maximum Route Duration
    routing.AddDimension(
        time_callback_index,
        60 * 60,       # Allow waiting time of 60 mins
        24 * 60 * 60,  # Maximum time per vehicle (24 hours in seconds) - Hard Constraint 6
        False,         # Don't force start cumul to zero
        'Time'
    )
    time_dimension = routing.GetDimensionOrDie('Time')
    
    # Add service time to each node's visit duration
    for node_idx in range(len(all_locations)):
        index = manager.NodeToIndex(node_idx)
        time_dimension.SetCumulVarSoftUpperBound(
            index, 
            24 * 60 * 60,  # 24 hours in seconds
            1000000  # High penalty for violating the 24-hour constraint
        )
        time_dimension.SlackVar(index).SetValue(0)
    
    # Store time window variables to track compliance
    time_window_vars = []
    compliance_threshold = int(min_time_window_compliance * num_deliveries)
    
    # Add time window constraints - Hard Constraint 7: Time Window Compliance
    if time_window_weight > 0.01:
        # Create binary variables to track time window compliance
        for delivery_idx, delivery in enumerate(delivery_locations):
            if 'time_window' in delivery and delivery['time_window']:
                try:
                    start_time_str, end_time_str = delivery['time_window'].split('-')
                    start_hour, start_min = map(int, start_time_str.split(':'))
                    end_hour, end_min = map(int, end_time_str.split(':'))
                    
                    # Convert to seconds since midnight
                    start_time_sec = (start_hour * 60 + start_min) * 60
                    end_time_sec = (end_hour * 60 + end_min) * 60
                    
                    # Get the node index
                    index = manager.NodeToIndex(num_vehicles + delivery_idx)
                    
                    # Add soft upper bound penalty with very high weight for late deliveries
                    # This implements Soft Constraint 2: Time Window Penalties
                    time_dimension.SetCumulVarSoftUpperBound(
                        index, 
                        end_time_sec, 
                        int(1000000 * time_window_weight)  # High penalty for being late
                    )
                    
                    # Don't penalize for early deliveries, just wait
                    time_dimension.CumulVar(index).SetMin(start_time_sec)
                    
                    # Track this time window for compliance calculation
                    time_window_vars.append((index, start_time_sec, end_time_sec))
                except:
                    # Skip if time window format is invalid
                    pass
    
    # Hard Constraint 1: All Deliveries Must Be Assigned
    # This is enforced by not creating disjunctions with penalties, but instead making all nodes mandatory
    
    # Hard Constraint 3: Flow Conservation (Route Continuity) is inherently enforced by OR-Tools
    
    # Hard Constraint 4: Start and End at Assigned Depots is enforced by the RoutingIndexManager setup
    
    # Set parameters for the solver
    search_parameters = pywrapcp.DefaultRoutingSearchParameters()
    
    # Use guided local search to find good solutions
    search_parameters.local_search_metaheuristic = (
        routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH
    )
    
    # Use path cheapest arc with resource constraints as the first solution strategy
    search_parameters.first_solution_strategy = (
        routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC
    )
    
    # Give the solver enough time to find a good solution
    search_parameters.time_limit.seconds = 10
    
    # Enable logging
    search_parameters.log_search = True
    
    # Try to enforce the time window compliance threshold
    if compliance_threshold > 0:
        # First try to solve with all deliveries required
        routing.CloseModelWithParameters(search_parameters)
        
        # Solve the problem
        st.write(f"Solving optimization model with {num_deliveries} deliveries and {num_vehicles} vehicles...")
        st.write(f"Target: At least {compliance_threshold} of {num_deliveries} deliveries ({min_time_window_compliance*100:.0f}%) must be within time windows")
        
        solution = routing.SolveWithParameters(search_parameters)
    else:
        # If no time window compliance required, solve normally
        solution = routing.SolveWithParameters(search_parameters)
    
    # If no solution was found, try a relaxed version (allow some deliveries to be unassigned)
    if not solution:
        st.warning("Could not find a solution with all deliveries assigned. Trying a relaxed version...")
        
        # Create a new model with disjunctions to allow dropping some deliveries with high penalties
        routing = pywrapcp.RoutingModel(manager)
        
        # Re-register callbacks
        transit_callback_index = routing.RegisterTransitCallback(distance_callback)
        time_callback_index = routing.RegisterTransitCallback(time_callback)
        service_callback_index = routing.RegisterUnaryTransitCallback(service_time_callback)
        demand_callback_index = routing.RegisterUnaryTransitCallback(demand_callback)
        
        routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
        
        # Add capacity dimension again
        routing.AddDimensionWithVehicleCapacity(
            demand_callback_index,
            0, vehicle_capacities, True, 'Capacity'
        )
        
        # Add time dimension again
        routing.AddDimension(
            time_callback_index,
            60 * 60, 24 * 60 * 60, False, 'Time'
        )
        time_dimension = routing.GetDimensionOrDie('Time')
        
        # Add disjunctions with very high penalties to try to include all deliveries
        for delivery_idx in range(num_deliveries):
            index = manager.NodeToIndex(num_vehicles + delivery_idx)
            routing.AddDisjunction([index], 1000000)  # High penalty but allows dropping if necessary
        
        # Try to solve with relaxed constraints
        search_parameters.time_limit.seconds = 15  # Give more time for relaxed version
        solution = routing.SolveWithParameters(search_parameters)
        
        if not solution:
            st.error("Could not find any solution. Try increasing the number of vehicles or relaxing other constraints.")
            return {
                'routes': {},
                'stats': {},
                'parameters': {
                    'priority_weight': priority_weight,
                    'time_window_weight': time_window_weight,
                    'balance_weight': balance_weight,
                    'min_time_window_compliance': min_time_window_compliance
                }
            }
    
    # Extract solution
    optimized_routes = {}
    route_stats = {}
    
    if solution:
        st.success("Solution found!")
        
        total_time_window_compliance = 0
        total_deliveries_assigned = 0
        
        for vehicle_idx in range(num_vehicles):
            route = []
            vehicle_id = vehicle_data.iloc[vehicle_idx]['vehicle_id']
            
            # Get the vehicle information
            vehicle_info = {
                'id': vehicle_id,
                'type': vehicle_data.iloc[vehicle_idx].get('vehicle_type', 'Standard'),
                'capacity': vehicle_data.iloc[vehicle_idx].get('max_weight_kg', 1000),
                'depot_latitude': vehicle_data.iloc[vehicle_idx]['depot_latitude'],
                'depot_longitude': vehicle_data.iloc[vehicle_idx]['depot_longitude']
            }
            
            # Initialize variables for tracking
            index = routing.Start(vehicle_idx)
            total_distance = 0
            total_time = 0
            total_load = 0
            time_window_compliant = 0
            total_deliveries = 0
            
            # Initialize variables to track current position and time
            current_time_sec = 8 * 3600  # Start at 8:00 AM (8 hours * 3600 seconds)
            
            while not routing.IsEnd(index):
                # Get the node index in the original data
                node_idx = manager.IndexToNode(index)
                
                # Skip depot nodes (they're already at the start)
                if node_idx >= num_vehicles:
                    # This is a delivery node - get the corresponding delivery
                    delivery_idx = node_idx - num_vehicles
                    delivery = delivery_locations[delivery_idx].copy()  # Create a copy to modify
                    
                    # Calculate estimated arrival time in minutes since start of day
                    arrival_time_sec = solution.Min(time_dimension.CumulVar(index))
                    arrival_time_mins = arrival_time_sec // 60
                    
                    # Store the estimated arrival time in the delivery
                    delivery['estimated_arrival'] = arrival_time_mins
                    
                    # Check time window compliance
                    if 'time_window' in delivery and delivery['time_window']:
                        try:
                            start_time_str, end_time_str = delivery['time_window'].split('-')
                            start_hour, start_min = map(int, start_time_str.split(':'))
                            end_hour, end_min = map(int, end_time_str.split(':'))
                            
                            # Convert to minutes for comparison
                            start_mins = start_hour * 60 + start_min
                            end_mins = end_hour * 60 + end_min
                            
                            # Check if delivery is within time window
                            on_time = False
                            
                            # If arrival <= end_time, consider it on-time (including early arrivals)
                            if arrival_time_mins <= end_mins:
                                on_time = True
                                time_window_compliant += 1
                                total_time_window_compliance += 1
                            
                            delivery['within_time_window'] = on_time
                        except Exception as e:
                            st.warning(f"Error parsing time window for delivery {delivery['id']}: {str(e)}")
                            delivery['within_time_window'] = False
                    
                    # Add to route
                    route.append(delivery)
                    total_deliveries += 1
                    total_deliveries_assigned += 1
                    
                    # Add to total load
                    total_load += delivery['demand'] / 100  # Convert back to original units
                
                # Move to the next node
                previous_idx = index
                index = solution.Value(routing.NextVar(index))
                
                # Add distance and time from previous to current
                if not routing.IsEnd(index):
                    previous_node = manager.IndexToNode(previous_idx)
                    next_node = manager.IndexToNode(index)
                    
                    # Add distance between these points
                    segment_distance = dist_matrix[previous_node, next_node]
                    total_distance += segment_distance
                    
                    # Add travel time between these points
                    segment_time_sec = int(time_matrix_mins[previous_node, next_node] * 60)
                    total_time += segment_time_sec / 60  # Convert seconds back to minutes
            
            # Store the route if it's not empty
            if route:
                optimized_routes[vehicle_id] = route
                
                # Calculate time window compliance percentage
                time_window_percent = (time_window_compliant / total_deliveries * 100) if total_deliveries > 0 else 0
                
                # Store route statistics
                route_stats[vehicle_id] = {
                    'vehicle_type': vehicle_info['type'],
                    'capacity_kg': vehicle_info['capacity'],
                    'deliveries': len(route),
                    'total_distance_km': round(total_distance, 2),
                    'estimated_time_mins': round(total_time),
                    'total_load_kg': round(total_load, 2),
                    'time_window_compliant': time_window_compliant,
                    'time_window_compliance': time_window_percent
                }
        
        # Check if overall time window compliance meets the minimum requirement
        overall_compliance = 0
        if total_deliveries_assigned > 0:
            overall_compliance = (total_time_window_compliance / total_deliveries_assigned)
            
        if overall_compliance < min_time_window_compliance:
            st.warning(f"Solution found, but time window compliance ({overall_compliance*100:.1f}%) is below the minimum required ({min_time_window_compliance*100:.0f}%).")
            st.info("Consider adjusting parameters: increase the number of vehicles, reduce the minimum compliance requirement, or adjust time window importance.")
        else:
            st.success(f"Solution meets time window compliance requirement: {overall_compliance*100:.1f}% (minimum required: {min_time_window_compliance*100:.0f}%)")
    else:
        st.error("No solution found. Try adjusting the parameters.")
        optimized_routes = {}
        route_stats = {}
    
    return {
        'routes': optimized_routes,
        'stats': route_stats,
        'parameters': {
            'priority_weight': priority_weight,
            'time_window_weight': time_window_weight,
            'balance_weight': balance_weight,
            'min_time_window_compliance': min_time_window_compliance
        }
    }

def display_optimization_results(optimization_result, delivery_data, vehicle_data, 
                               distance_matrix, time_matrix, locations):
    """
    Display the optimization results
    
    Parameters:
        optimization_result (dict): Result from the optimization algorithm
        delivery_data (pd.DataFrame): Delivery information
        vehicle_data (pd.DataFrame): Vehicle information
        distance_matrix (pd.DataFrame): Distance matrix between locations
        time_matrix (pd.DataFrame): Time matrix between locations
        locations (pd.DataFrame): Location details
    """
    # Define colors for vehicle routes
    colors = ['blue', 'red', 'green', 'purple', 'orange', 'darkblue', 
              'darkred', 'darkgreen', 'cadetblue', 'darkpurple', 'pink', 
              'lightblue', 'lightred', 'lightgreen', 'gray', 'black', 'lightgray']
    
    routes = optimization_result['routes']
    
    # Display summary statistics
    st.subheader("Optimization Results")

    # Calculate overall statistics
    total_deliveries = sum(len(route) for route in routes.values())
    active_vehicles = sum(1 for route in routes.values() if len(route) > 0)

    # Calculate additional metrics
    total_distance = sum(stats.get('total_distance_km', 0) for stats in optimization_result.get('stats', {}).values())
    total_time_mins = sum(stats.get('estimated_time_mins', 0) for stats in optimization_result.get('stats', {}).values())

    # Calculate time window compliance (on-time percentage)
    on_time_deliveries = 0
    total_route_deliveries = 0

    # Count deliveries within time window
    for vehicle_id, route in routes.items():
        stats = optimization_result.get('stats', {}).get(vehicle_id, {})
        
        # Only process if we have stats for this vehicle
        if stats and 'time_window_compliant' in stats:
            # Use the actual count of compliant deliveries, not the percentage
            on_time_deliveries += stats['time_window_compliant']
        else:
            # Try to estimate based on delivery details
            for delivery in route:
                if 'time_window' in delivery and 'estimated_arrival' in delivery:
                    # Format is typically "HH:MM-HH:MM"
                    try:
                        time_window = delivery['time_window']
                        start_time_str, end_time_str = time_window.split('-')
                        
                        # Convert to minutes for comparison
                        start_mins = int(start_time_str.split(':')[0]) * 60 + int(start_time_str.split(':')[1])
                        end_mins = int(end_time_str.split(':')[0]) * 60 + int(end_time_str.split(':')[1])
                        arrival_mins = delivery.get('estimated_arrival', 0)
                        
                        # Only consider deliveries late if they arrive after the end time
                        if arrival_mins <= end_mins:
                            on_time_deliveries += 1
                    except:
                        pass
        
        total_route_deliveries += len(route)

    # Ensure we have a valid number for on-time percentage
    delivery_ontime_percent = 0
    if total_route_deliveries > 0:
        delivery_ontime_percent = (on_time_deliveries / total_route_deliveries) * 100

    # Display metrics in a nicer layout with columns
    st.write("### Overall Performance")
    col1, col2, col3 = st.columns(3)
    with col1:
        st.metric("Deliveries Assigned", f"{total_deliveries}/{len(delivery_data)}")
        st.metric("Vehicles Used", f"{active_vehicles}/{len(vehicle_data)}")

    with col2:
        st.metric("Total Distance", f"{total_distance:.1f} km")
        st.metric("Total Time", f"{int(total_time_mins//60)}h {int(total_time_mins%60)}m")

    with col3:
        st.metric("Time Window Compliance", f"{delivery_ontime_percent:.0f}%")
        
        # Calculate route efficiency (meters per delivery)
        if total_deliveries > 0:
            efficiency = (total_distance * 1000) / total_deliveries
            st.metric("Avg Distance per Delivery", f"{efficiency:.0f} m")

    # Add a visualization of time distribution
    st.write("### Time & Distance Distribution by Vehicle")
    time_data = {vehicle_id: stats.get('estimated_time_mins', 0) 
                 for vehicle_id, stats in optimization_result.get('stats', {}).items()
                 if len(routes.get(vehicle_id, [])) > 0}

    if time_data:
        # Create bar charts for time and distance
        time_df = pd.DataFrame({
            'Vehicle': list(time_data.keys()),
            'Time (mins)': list(time_data.values())
        })
        
        distance_data = {vehicle_id: stats.get('total_distance_km', 0) 
                         for vehicle_id, stats in optimization_result.get('stats', {}).items()
                         if len(routes.get(vehicle_id, [])) > 0}
        
        distance_df = pd.DataFrame({
            'Vehicle': list(distance_data.keys()),
            'Distance (km)': list(distance_data.values())
        })
        
        col1, col2 = st.columns(2)
        with col1:
            st.bar_chart(time_df.set_index('Vehicle'))
        with col2:
            st.bar_chart(distance_df.set_index('Vehicle'))
    
    # Display the map with all routes
    st.subheader("Route Map with Road Navigation")

    # Add info about the route visualization
    st.info("""
    The map shows delivery routes that follow road networks from the depot to each stop in sequence, and back to the depot.
    Numbered circles indicate the stop sequence, and arrows show travel direction.
    """)
    
    # Extract all available dates from the delivery data
    if 'delivery_date' in delivery_data.columns:
        # Extract unique dates, ensuring all are converted to datetime objects
        available_dates = sorted(pd.to_datetime(delivery_data['delivery_date'].unique()))
        
        # Format dates for display
        date_options = {}
        for date in available_dates:
            # Ensure date is a proper datetime object before formatting
            if isinstance(date, str):
                date_obj = pd.to_datetime(date)
            else:
                date_obj = date
            # Create the formatted string key
            date_str = date_obj.strftime('%b %d, %Y')
            date_options[date_str] = date_obj
        
        # Default to earliest date
        default_date = min(available_dates) if available_dates else None
        default_date_str = default_date.strftime('%b %d, %Y') if default_date else None
        
        # Create date selection dropdown
        selected_date_str = st.selectbox(
            "Select date to show routes for:",
            options=list(date_options.keys()),
            index=0 if default_date_str else None,
        )
        
        # Convert selected string back to date object
        selected_date = date_options[selected_date_str] if selected_date_str else None
        
        # Filter routes to only show deliveries for the selected date
        if selected_date is not None:
            filtered_routes = {}
            
            for vehicle_id, route in routes.items():
                # Keep only deliveries for the selected date
                filtered_route = []
                
                for delivery in route:
                    delivery_id = delivery['id']
                    # Find the delivery in the original data to get its date
                    delivery_row = delivery_data[delivery_data['delivery_id'] == delivery_id]
                    
                    if not delivery_row.empty and 'delivery_date' in delivery_row:
                        delivery_date = delivery_row['delivery_date'].iloc[0]
                        
                        # Check if this delivery is for the selected date
                        if pd.to_datetime(delivery_date).date() == pd.to_datetime(selected_date).date():
                            filtered_route.append(delivery)
                
                # Only add the vehicle if it has deliveries on this date
                if filtered_route:
                    filtered_routes[vehicle_id] = filtered_route
            
            # Replace the original routes with filtered ones for map display
            routes_for_map = filtered_routes
            st.write(f"Showing routes for {len(routes_for_map)} vehicles on {selected_date_str}")
        else:
            routes_for_map = routes
    else:
        routes_for_map = routes
        st.warning("No delivery dates available in data. Showing all routes.")
    
    # Create a map centered on Singapore
    singapore_coords = [1.3521, 103.8198]
    m = folium.Map(location=singapore_coords, zoom_start=12)
    
    # Modify loop to use routes_for_map instead of routes
    # Count total route segments for progress bar
    total_segments = sum(len(route) + 1 for route in routes_for_map.values() if route)  # +1 for return to depot

    # Create a unique key for this optimization result to use in session state
    optimization_key = hash(str(optimization_result))

    # Check if we have stored routes for this optimization result
    if optimization_key not in st.session_state.calculated_road_routes:
        # Initialize storage for this optimization
        st.session_state.calculated_road_routes[optimization_key] = {}

    # Count total route segments for progress bar
    total_segments = sum(len(route) + 1 for route in routes_for_map.values() if route)  # +1 for return to depot
    route_progress = st.progress(0)
    progress_container = st.empty()
    progress_container.text("Calculating routes: 0%")

    # Counter for processed segments
    processed_segments = 0

    for i, (vehicle_id, route) in enumerate(routes_for_map.items()):
        if not route:
            continue
        
        # Get vehicle info
        vehicle_info = vehicle_data[vehicle_data['vehicle_id'] == vehicle_id].iloc[0]
        
        # Use color cycling if we have more vehicles than colors
        color = colors[i % len(colors)]
        
        # Add depot marker
        depot_lat, depot_lon = vehicle_info['depot_latitude'], vehicle_info['depot_longitude']
        
        # Create depot popup content
        depot_popup = f"""
            <b>Depot:</b> {vehicle_id}<br>
            <b>Vehicle Type:</b> {vehicle_info['vehicle_type']}<br>
            <b>Driver:</b> {vehicle_info.get('driver_name', 'Unknown')}<br>
        """
        
        # Add depot marker with START label
        folium.Marker(
            [depot_lat, depot_lon],
            popup=folium.Popup(depot_popup, max_width=300),
            tooltip=f"Depot: {vehicle_id} (START/END)",
            icon=folium.Icon(color=color, icon='home', prefix='fa')
        ).add_to(m)
        
        # Create route points for complete journey
        waypoints = [(depot_lat, depot_lon)]  # Start at depot
        
        # Add all delivery locations as waypoints
        for delivery in route:
            waypoints.append((delivery['latitude'], delivery['longitude']))
        
        # Close the loop back to depot
        waypoints.append((depot_lat, depot_lon))
        
        # Add delivery point markers with sequenced numbering
        for j, delivery in enumerate(route):
            lat, lon = delivery['latitude'], delivery['longitude']
            
            # Create popup content
            popup_content = f"""
                <b>Stop {j+1}:</b> {delivery['id']}<br>
                <b>Customer:</b> {delivery.get('customer_name', 'Unknown')}<br>
            """
            
            if 'priority' in delivery:
                popup_content += f"<b>Priority:</b> {delivery['priority']}<br>"
                
            if 'weight_kg' in delivery:
                popup_content += f"<b>Weight:</b> {delivery['weight_kg']:.2f} kg<br>"
                
            if 'time_window' in delivery:
                popup_content += f"<b>Time Window:</b> {delivery['time_window']}<br>"
            
            # Add circle markers and other delivery visualizations
            folium.Circle(
                location=[lat, lon],
                radius=50,
                color=color,
                fill=True,
                fill_color=color,
                fill_opacity=0.7,
                tooltip=f"Stop {j+1}: {delivery['id']}"
            ).add_to(m)
            
            # Add text label with stop number
            folium.map.Marker(
                [lat, lon],
                icon=DivIcon(
                    icon_size=(20, 20),
                    icon_anchor=(10, 10),
                    html=f'<div style="font-size: 12pt; color: #444444; font-weight: bold; text-align: center;">{j+1}</div>',
                )
            ).add_to(m)
            
            # Add regular marker with popup
            folium.Marker(
                [lat + 0.0003, lon],  # slight offset to not overlap with the circle
                popup=folium.Popup(popup_content, max_width=300),
                tooltip=f"Delivery {delivery['id']}",
                icon=folium.Icon(color=color, icon='box', prefix='fa')
            ).add_to(m)
        
        # Create road-based routes between each waypoint with progress tracking
        for k in range(len(waypoints) - 1):
            # Get start and end points of this segment
            start_point = waypoints[k]
            end_point = waypoints[k+1]
            
            # Create a key for this route segment
            route_key = f"{vehicle_id}_{k}"
            
            # Update progress text
            segment_desc = "depot" if k == 0 else f"stop {k}"
            next_desc = f"stop {k+1}" if k < len(waypoints) - 2 else "depot"
            
            # Check if we have already calculated this route
            if route_key in st.session_state.calculated_road_routes[optimization_key]:
                # Use stored route
                road_route = st.session_state.calculated_road_routes[optimization_key][route_key]
                progress_text = f"Using stored route for Vehicle {vehicle_id}: {segment_desc} β†’ {next_desc}"
            else:
                # Calculate and store new route
                progress_text = f"Calculating route for Vehicle {vehicle_id}: {segment_desc} β†’ {next_desc}"
                with st.spinner(progress_text):
                    # Get a road-like route between these points
                    road_route = get_road_route(start_point, end_point)
                    # Store for future use
                    st.session_state.calculated_road_routes[optimization_key][route_key] = road_route
            
            # Add the route line (non-animated)
            folium.PolyLine(
                road_route,
                color=color,
                weight=4,
                opacity=0.8,
                tooltip=f"Route {vehicle_id}: {segment_desc} β†’ {next_desc}"
            ).add_to(m)
            
            # Add direction arrow
            idx = int(len(road_route) * 0.7)
            if idx < len(road_route) - 1:
                p1 = road_route[idx]
                p2 = road_route[idx + 1]
                
                # Calculate direction angle
                dy = p2[0] - p1[0]
                dx = p2[1] - p1[1]
                angle = (90 - np.degrees(np.arctan2(dy, dx))) % 360
                
                # Add arrow marker
                folium.RegularPolygonMarker(
                    location=p1,
                    number_of_sides=3,
                    radius=8,
                    rotation=angle,
                    color=color,
                    fill_color=color,
                    fill_opacity=0.8
                ).add_to(m)
            
            # Update progress after each segment
            processed_segments += 1
            progress_percentage = int((processed_segments / total_segments) * 100)
            route_progress.progress(processed_segments / total_segments)
            progress_container.text(f"Calculating routes: {progress_percentage}%")

    # Add a message to show when using cached routes
    if optimization_key in st.session_state.calculated_road_routes:
        cached_count = len(st.session_state.calculated_road_routes[optimization_key])
        if cached_count > 0 and cached_count >= processed_segments:
            st.info(f"βœ… Using {cached_count} previously calculated routes. No recalculation needed.")
    
    # Clear progress display when done
    progress_container.empty()
    route_progress.empty()
    st.success("All routes calculated successfully!")
    
    # Display the map
    folium_static(m, width=800, height=600)
    
    # -----------------------------------------------------
    # Unified Schedule Calendar Section
    # -----------------------------------------------------
    st.subheader("Schedule Calendar View")
    st.write("This calendar shows both delivery schedules and vehicle assignments. On-time deliveries are shown in green, late deliveries in red.")

    # Process data for calendar view
    if routes:
        # First, collect all assigned deliveries and their details
        calendar_data = []
        
        # Track which deliveries were actually included in routes
        assigned_delivery_ids = set()
        
        # Step 1: Process all assigned deliveries first
        for vehicle_id, route in routes.items():
            for delivery in route:
                assigned_delivery_ids.add(delivery['id'])
                # Get vehicle info
                vehicle_info = vehicle_data[vehicle_data['vehicle_id'] == vehicle_id].iloc[0]
                vehicle_type = vehicle_info.get('vehicle_type', 'Standard')
                driver_name = vehicle_info.get('driver_name', 'Unknown')
                
                # Extract delivery data
                delivery_id = delivery['id']
                customer_name = delivery.get('customer_name', 'Unknown')
                priority = delivery.get('priority', 'Medium')
                time_window = delivery.get('time_window', '09:00-17:00')
                weight = delivery.get('weight_kg', 0)
                
                # Extract start and end times from time_window
                start_time_str, end_time_str = time_window.split('-')
                
                # Get delivery date from original data
                delivery_row = delivery_data[delivery_data['delivery_id'] == delivery_id]
                delivery_date = delivery_row['delivery_date'].iloc[0] if not delivery_row.empty and 'delivery_date' in delivery_row else datetime.now().date()
                
                # Create start and end datetime for the delivery
                try:
                    # Convert to pandas datetime
                    if isinstance(delivery_date, pd.Timestamp):
                        date_str = delivery_date.strftime('%Y-%m-%d')
                    elif isinstance(delivery_date, str):
                        date_str = pd.to_datetime(delivery_date).strftime('%Y-%m-%d')
                    else:
                        date_str = delivery_date.strftime('%Y-%m-%d')
                    
                    start_datetime = pd.to_datetime(f"{date_str} {start_time_str}")
                    end_datetime = pd.to_datetime(f"{date_str} {end_time_str}")
                    
                    # Check if this is on time (based on the estimated arrival from the route)
                    estimated_arrival_mins = delivery.get('estimated_arrival', 0)
                    
                    # Convert time_window to minutes for comparison
                    start_mins = int(start_time_str.split(':')[0]) * 60 + int(start_time_str.split(':')[1])
                    end_mins = int(end_time_str.split(':')[0]) * 60 + int(end_time_str.split(':')[1])
                    
                    # Determine if delivery is on time
                    on_time = start_mins <= estimated_arrival_mins <= end_mins
                    
                    # Set color based on on-time status and assignment
                    if on_time:
                        # Green for on-time
                        color = 'on_time'
                    else:
                        # Red for not on-time
                        color = 'late'

                    calendar_data.append({
                        'delivery_id': delivery_id,
                        'customer_name': customer_name,
                        'vehicle_id': vehicle_id,
                        'driver_name': driver_name,
                        'vehicle_type': vehicle_type,
                        'priority': priority,
                        'time_window': time_window,
                        'estimated_arrival_mins': estimated_arrival_mins,
                        'estimated_arrival_time': f"{estimated_arrival_mins//60:02d}:{estimated_arrival_mins%60:02d}",
                        'weight_kg': weight,
                        'Start': start_datetime,
                        'Finish': end_datetime,
                        'Task': f"{delivery_id}: {customer_name}",
                        'Vehicle Task': f"{vehicle_id}: {driver_name}",
                        'on_time': on_time,
                        'assigned': True,
                        'color': color,
                        'delivery_date': pd.to_datetime(date_str)
                    })
                except Exception as e:
                    st.warning(f"Could not process time window for delivery {delivery_id}: {str(e)}")
        
        # Step 2: Now add unassigned deliveries
        for _, row in delivery_data.iterrows():
            delivery_id = row['delivery_id']
            
            # Skip if already assigned
            if delivery_id in assigned_delivery_ids:
                continue
                
            # Extract data for unassigned delivery
            customer_name = row.get('customer_name', 'Unknown')
            priority = row.get('priority', 'Medium')
            time_window = row.get('time_window', '09:00-17:00')
            weight = row.get('weight_kg', 0)
            
            # Extract start and end times from time_window
            start_time_str, end_time_str = time_window.split('-')
            
            # Get delivery date
            if 'delivery_date' in row:
                delivery_date = row['delivery_date']
            else:
                delivery_date = datetime.now().date()
            
            # Create start and end datetime
            try:
                # Convert to pandas datetime
                if isinstance(delivery_date, pd.Timestamp):
                    date_str = delivery_date.strftime('%Y-%m-%d')
                elif isinstance(delivery_date, str):
                    date_str = pd.to_datetime(delivery_date).strftime('%Y-%m-%d')
                else:
                    date_str = delivery_date.strftime('%Y-%m-%d')
                
                start_datetime = pd.to_datetime(f"{date_str} {start_time_str}")
                end_datetime = pd.to_datetime(f"{date_str} {end_time_str}")
                
                # For unassigned deliveries set color to 'unassigned'
                calendar_data.append({
                    'delivery_id': delivery_id,
                    'customer_name': customer_name,
                    'vehicle_id': 'Unassigned',
                    'driver_name': 'N/A',
                    'vehicle_type': 'N/A',
                    'priority': priority,
                    'time_window': time_window,
                    'estimated_arrival_mins': 0,
                    'estimated_arrival_time': 'N/A',
                    'weight_kg': weight,
                    'Start': start_datetime,
                    'Finish': end_datetime,
                    'Task': f"{delivery_id}: {customer_name} (UNASSIGNED)",
                    'Vehicle Task': 'Unassigned',
                    'on_time': False,
                    'assigned': False,
                    'color': 'unassigned',  # Color for unassigned
                    'delivery_date': pd.to_datetime(date_str)
                })
            except Exception as e:
                st.warning(f"Could not process time window for unassigned delivery {delivery_id}: {str(e)}")
        
        if calendar_data:
            # Convert to DataFrame
            cal_df = pd.DataFrame(calendar_data)
            
            # Create color mapping for on-time status
            cal_df['Color'] = cal_df['on_time'].map({True: 'rgb(0, 200, 0)', False: 'rgb(255, 0, 0)'})
            
            # Get all available dates 
            all_dates = sorted(cal_df['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}
            
            # Initialize calendar display dates if not already set or if dates have changed
            available_date_keys = list(date_options.keys())
            
            # Default to all dates
            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):
                st.session_state.calendar_display_dates = available_date_keys
            
            # Add multiselect for date filtering with session state
            selected_date_strings = st.multiselect(
                "Select dates to display",
                options=available_date_keys,
                default=st.session_state.calendar_display_dates,
                key="calendar_date_selector"
            )
            
            # Update the session state
            st.session_state.calendar_display_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_cal_df = cal_df[cal_df['delivery_date'].dt.date.isin(selected_dates)]
                
                # 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_cal_df[filtered_cal_df['delivery_date'].dt.date == date]
                        
                        if len(day_data) > 0:
                            # FIRST SECTION: DELIVERY SCHEDULE VIEW
                            st.write("#### Delivery Schedule")
                            
                            # Create figure for delivery view
                            fig = px.timeline(
                                day_data, 
                                x_start="Start", 
                                x_end="Finish", 
                                y="Task",
                                color="color",  # Use our color column
                                color_discrete_map={
                                    "on_time": "green", 
                                    "late": "orange",
                                    "unassigned": "red"  # Unassigned deliveries also red
                                },
                                hover_data=["customer_name", "vehicle_id", "driver_name", "priority", "time_window", 
                                           "estimated_arrival_time", "weight_kg", "assigned"]
                            )
                            
                            # Fix the pattern application code
                            for i, row in day_data.iterrows():
                                # Only add diagonal pattern to assigned deliveries
                                if row['assigned']:
                                    for trace in fig.data:
                                        # Find which trace corresponds to this row's color group
                                        color_value = row['color']
                                        
                                        # Look for matching trace
                                        if trace.name == color_value and any(y == row['Task'] for y in trace.y):
                                            # Add pattern only to assigned bars
                                            if 'marker' not in trace:
                                                trace.marker = dict()
                                            if 'pattern' not in trace.marker:
                                                trace.marker.pattern = dict(
                                                    shape="\\",  # Diagonal lines
                                                    size=4,
                                                    solidity=0.5,
                                                    fgcolor="black"
                                                )
                            
                            # Add status labels to the bars
                            for idx, row in day_data.iterrows():
                                status_text = "βœ“ On-time" if row['on_time'] and row['assigned'] else "⚠ Late" if row['assigned'] else "Not assigned"
                                position = (row['Start'] + (row['Finish'] - row['Start'])/2)
                                
                                # Only add labels to assigned deliveries
                                if row['assigned']:
                                    fig.add_annotation(
                                        x=position,
                                        y=row['Task'],
                                        text=status_text,
                                        showarrow=False,
                                        font=dict(color="black", size=10),
                                        xanchor="center"
                                    )
                            
                            # Update layout
                            fig.update_layout(
                                title=f"Deliveries by Customer - {date.strftime('%b %d, %Y')}",
                                xaxis_title="Time of Day",
                                yaxis_title="Delivery",
                                height=max(300, 50 * len(day_data)),
                                yaxis={'categoryorder':'category ascending'},
                                showlegend=False  # Hide the legend as we have custom annotations
                            )
                            
                            # Display figure
                            st.plotly_chart(fig, use_container_width=True)
                            
                            # Show summary metrics for delivery view
                            col1, col2, col3, col4 = st.columns(4)
                            with col1:
                                st.metric("Total Deliveries", len(day_data))
                            with col2:
                                st.metric("On-Time Deliveries", len(day_data[day_data['on_time']]))
                            with col3:
                                st.metric("Late Deliveries", len(day_data[~day_data['on_time']]))
                            with col4:
                                if 'weight_kg' in day_data.columns:
                                    st.metric("Total Weight", f"{day_data['weight_kg'].sum():.2f} kg")
                            
                            # Add breakdown of deliveries by priority
                            if 'priority' in day_data.columns:
                                st.write("##### Deliveries by Priority")
                                priority_counts = day_data['priority'].value_counts()
                                priority_cols = st.columns(min(4, len(priority_counts)))
                                
                                for j, (priority, count) in enumerate(priority_counts.items()):
                                    col_idx = j % len(priority_cols)
                                    with priority_cols[col_idx]:
                                        st.metric(priority, count)
                            
                            # SECOND SECTION: VEHICLE SCHEDULE VIEW
                            st.write("#### Vehicle Schedule")
                            
                            # Create figure grouped by vehicle
                            fig_vehicle = px.timeline(
                                day_data, 
                                x_start="Start", 
                                x_end="Finish", 
                                y="Vehicle Task",
                                color="on_time",
                                color_discrete_map={True: "green", False: "red"},
                                hover_data=["delivery_id", "customer_name", "priority", "time_window", 
                                           "estimated_arrival_time", "weight_kg"]
                            )
                            
                            # Add labels for each delivery to the bars
                            for idx, row in day_data.iterrows():
                                fig_vehicle.add_annotation(
                                    x=(row['Start'] + (row['Finish'] - row['Start'])/2),
                                    y=row['Vehicle Task'],
                                    text=f"#{row['delivery_id']}",
                                    showarrow=False,
                                    font=dict(size=10, color="black")
                                )
                            
                            # Update layout
                            fig_vehicle.update_layout(
                                title=f"Vehicle Assignment Schedule - {date.strftime('%b %d, %Y')}",
                                xaxis_title="Time of Day",
                                yaxis_title="Vehicle",
                                height=max(300, 70 * day_data['Vehicle Task'].nunique()),
                                yaxis={'categoryorder':'category ascending'}
                            )
                            
                            # Display figure for vehicle view
                            st.plotly_chart(fig_vehicle, use_container_width=True)
                            
                            # Show vehicle utilization summary
                            st.write("##### Vehicle Utilization")
                            
                            # Calculate vehicle utilization metrics
                            vehicle_metrics = []
                            for vehicle_id in day_data['vehicle_id'].unique():
                                vehicle_deliveries = day_data[day_data['vehicle_id'] == vehicle_id]
                                
                                # Calculate total delivery time for this vehicle
                                total_mins = sum((row['Finish'] - row['Start']).total_seconds() / 60 for _, row in vehicle_deliveries.iterrows())
                                
                                # Count on-time deliveries
                                on_time_count = len(vehicle_deliveries[vehicle_deliveries['on_time'] == True])
                                
                                # Get the driver name
                                driver_name = vehicle_deliveries['driver_name'].iloc[0] if not vehicle_deliveries.empty else "Unknown"
                                
                                vehicle_metrics.append({
                                    'vehicle_id': vehicle_id,
                                    'driver_name': driver_name,
                                    'deliveries': len(vehicle_deliveries),
                                    'delivery_time_mins': total_mins,
                                    'on_time_deliveries': on_time_count,
                                    'on_time_percentage': (on_time_count / len(vehicle_deliveries)) * 100 if len(vehicle_deliveries) > 0 else 0
                                })
                            
                            # Display metrics in a nice format
                            metrics_df = pd.DataFrame(vehicle_metrics)
                            
                            # Show as a table
                            st.dataframe(metrics_df.style.format({
                                'delivery_time_mins': '{:.0f}',
                                'on_time_percentage': '{:.1f}%'
                            }))
                            
                        else:
                            st.info(f"No deliveries scheduled for {date.strftime('%b %d, %Y')}")
        else:
            st.info("No calendar data available. Please generate routes first.")

def create_distance_matrix(locations):
    """
    Create a simple Euclidean distance matrix between locations
    
    In a real implementation, this would be replaced by actual road distances
    
    Parameters:
        locations (list): List of location dictionaries with lat and lon
        
    Returns:
        numpy.ndarray: Distance matrix
    """
    n = len(locations)
    matrix = np.zeros((n, n))
    for i in range(n):
        for j in range(n):
            if i == j:
                continue
            
            # Approximate distance in km (very rough)
            lat1, lon1 = locations[i]['latitude'], locations[i]['longitude']
            lat2, lon2 = locations[j]['latitude'], locations[j]['longitude']
            
            # Simple Euclidean distance (for demo purposes)
            # In reality, we'd use actual road distances
            dist = ((lat1 - lat2) ** 2 + (lon1 - lon2) ** 2) ** 0.5 * 111
            matrix[i, j] = dist
    
    return matrix

def get_road_route(start_point, end_point):
    """
    Get a route that follows actual roads between two points using OpenStreetMap's routing service.
    
    Args:
        start_point: (lat, lon) tuple of start location
        end_point: (lat, lon) tuple of end location
        
    Returns:
        list: List of (lat, lon) points representing the actual road route
    """
    try:
        # OSRM expects coordinates in lon,lat format
        start_lat, start_lon = start_point
        end_lat, end_lon = end_point
        
        # Build the API URL for OSRM (OpenStreetMap Routing Machine)
        url = f"http://router.project-osrm.org/route/v1/driving/{start_lon},{start_lat};{end_lon},{end_lat}"
        params = {
            "overview": "full",
            "geometries": "geojson",
            "steps": "true"
        }
        
        # Replace direct text output with spinner
        with st.spinner(f"Getting route from ({start_lat:.4f}, {start_lon:.4f}) to ({end_lat:.4f}, {end_lon:.4f})..."):
            response = requests.get(url, params=params, timeout=5)
            
            if response.status_code == 200:
                data = response.json()
                
                # Check if a route was found
                if data['code'] == 'Ok' and len(data['routes']) > 0:
                    # Extract the geometry (list of coordinates) from the response
                    geometry = data['routes'][0]['geometry']['coordinates']
                    
                    # OSRM returns points as [lon, lat], but we need [lat, lon]
                    route_points = [(lon, lat) for lat, lon in geometry]
                    return route_points
        
        # If we get here, something went wrong with the API call
        st.warning(f"Could not get road route: {response.status_code} - {response.text if response.status_code != 200 else 'No routes found'}")
        
    except Exception as e:
        st.warning(f"Error getting road route: {str(e)}")
    
    # Fallback to our approximation method if the API call fails
    with st.spinner("Generating approximate route..."):
        # Create a more sophisticated approximation with higher density of points
        start_lat, start_lon = start_point
        end_lat, end_lon = end_point
        
        # Calculate the direct distance
        direct_dist = ((start_lat - end_lat)**2 + (start_lon - end_lon)**2)**0.5
        
        # Generate more points for longer distances
        num_points = max(10, int(direct_dist * 10000))  # Scale based on distance
        
        # Create a path with small random deviations to look like a road
        route_points = []
        
        # Starting point
        route_points.append((start_lat, start_lon))
        
        # Calculate major waypoints - like going through major roads
        # Find a midpoint that's slightly off the direct line
        mid_lat = (start_lat + end_lat) / 2
        mid_lon = (start_lon + end_lon) / 2
        
        # Add some perpendicular deviation to simulate taking streets
        # Get perpendicular direction
        dx = end_lat - start_lat
        dy = end_lon - start_lon
        
        # Perpendicular direction
        perpendicular_x = -dy
        perpendicular_y = dx
        
        # Normalize and scale
        magnitude = (perpendicular_x**2 + perpendicular_y**2)**0.5
        if magnitude > 0:
            perpendicular_x /= magnitude
            perpendicular_y /= magnitude
        
        # Scale the perpendicular offset based on distance
        offset_scale = direct_dist * 0.2  # 20% of direct distance
        
        # Apply offset to midpoint
        mid_lat += perpendicular_x * offset_scale * random.choice([-1, 1])
        mid_lon += perpendicular_y * offset_scale * random.choice([-1, 1])
        
        # Generate a smooth path from start to midpoint
        for i in range(1, num_points // 2):
            t = i / (num_points // 2)
            # Quadratic Bezier curve parameters
            u = 1 - t
            lat = u**2 * start_lat + 2 * u * t * mid_lat + t**2 * mid_lat
            lon = u**2 * start_lon + 2 * u * t * mid_lon + t**2 * mid_lon
            
            # Add small random noise to make it look like following streets
            noise_scale = 0.0002 * direct_dist
            lat += random.uniform(-noise_scale, noise_scale)
            lon += random.uniform(-noise_scale, noise_scale)
            
            route_points.append((lat, lon))
        
        # Generate a smooth path from midpoint to end
        for i in range(num_points // 2, num_points):
            t = (i - num_points // 2) / (num_points // 2)
            # Quadratic Bezier curve parameters
            u = 1 - t
            lat = u**2 * mid_lat + 2 * u * t * mid_lat + t**2 * end_lat
            lon = u**2 * mid_lon + 2 * u * t * mid_lon + t**2 * end_lon
            
            # Add small random noise to make it look like following streets
            noise_scale = 0.0002 * direct_dist
            lat += random.uniform(-noise_scale, noise_scale)
            lon += random.uniform(-noise_scale, noise_scale)
            
            route_points.append((lat, lon))
        
        # Ending point
        route_points.append((end_lat, end_lon))
    
    return route_points

# Add this condition to make the function importable
if __name__ == "__main__":
    st.set_page_config(
        page_title="Route Optimizer - Delivery Route Optimization",
        page_icon="πŸ›£οΈ",
        layout="wide"
    )
    optimize_page()