-
Notifications
You must be signed in to change notification settings - Fork 0
/
RouteSummaryGenerator.py
1251 lines (1058 loc) · 48.6 KB
/
RouteSummaryGenerator.py
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
# Author: Quinn James ([email protected])
#
# A command-line utility to generate a ridership summary for bus routes.
# Functionality of this program's input and output files are documented in the
# main README.md file.
#
# More details about this project can be found in the README file or at:
# https://github.com/qcjames53/AJM-RouteSummaries
from calendar import c
from turtle import color
import openpyxl
from openpyxl.styles import Alignment, PatternFill, Color
import datetime
from enum import Enum
from typing import Dict, Tuple, List
from Log import Log
# Route direction enum
class Direction(Enum):
IB = "IB" # inbound
OB = "OB" # outbound
NB = "NB" # northbound
SB = "SB" # southbound
EB = "EB" # eastbound
WB = "WB" # westbound
LP = "LP" # loop
UN = "UN" # unknown
# implemented to allow for sorting by direction
def __lt__(self, other):
return str(self) < str(other)
# Utility functions
def stringToDirection(dir_string) -> Direction:
"""
Converts a direction string to a direction enum
@param dir_string The string to convert
@returns Direction enum for the direction of this string
"""
direction = Direction.UN
if dir_string == "IB":
direction = Direction.IB
elif dir_string == "OB":
direction = Direction.OB
elif dir_string == "NB":
direction = Direction.NB
elif dir_string == "SB":
direction = Direction.SB
elif dir_string == "EB":
direction = Direction.EB
elif dir_string == "WB":
direction = Direction.WB
elif dir_string == "LP":
direction = Direction.LP
return direction
class RouteManager:
"""
A class that allows for easy storage of routes and their respective data
"""
def __init__(self, log:Log) -> None:
"""
Initilize RouteManager with the appropriate internal variables
@param log The log object in the currect workbook
"""
# A map of routes from routestring to route object
self.routes = {}
self.log = log
def __str__(self) -> str:
output = ""
for key in sorted(self.routes.keys()):
output += self.routes[key].__str__() + '\n'
return output
def __repr__(self) -> str:
return self.__str__()
def addStop(self, route, direction: Direction, stop_no, street, cross_street, is_timed) -> None:
"""
Adds a stop to the specified route object.
@param route The route with which to add the stop
@param stop_no The stop number to add
@param street The main street for the stop
@param cross_street The cross street for the stop
@param is_timed True if this is a timed stop
"""
# if the route key does not exist, create the route
if (route, direction) not in self.routes:
self.routes[(route, direction)] = Route(route, direction, self.log)
# Add the stop to the appropriate route
self.routes[(route, direction)].addStop(stop_no, street, cross_street, is_timed)
def addData(self, route, direction: Direction, stop_no, datetime, run, arrival_time, \
schedule_time, offs, ons, onboard) -> bool:
"""
Adds data about a stop to the specified route object
@param route The route with which to add the stop
@param stop_no The stop number
@param datetime The datetime the route began
@param run The run value of this stop
@param arrival_time The arrival time of the stop
@param schedule_time The scheduled arrival time of the stop
@param offs The number of passengers departing the bus
@param ons The number of passengers boarding the bus
@param onboard The number of passengers carrying over from a prev route
@returns Boolean of whether the data was successfully added
"""
# If the route does not exist, log an error and return
if (route, direction) not in self.routes:
self.log.logError(f"Tried to add data to nonexistent route: {route} {direction}")
return False
# Add the data to the appropriate route
return self.routes[(route, direction)].addData(stop_no, datetime, run, arrival_time,\
schedule_time, offs, ons, onboard)
def setRouteData(self, route, description, direction: Direction) -> None:
"""
Sets the metadata of a particular route.
@param route The number of the route
@param description A text description of the route (University, Uptown)
@param direction The direction of the route as a Direction object
"""
# if the route key does not exist, create the route
if route not in self.routes:
self.routes[(route, direction)] = Route(route, direction, self.log)
# Add data to appropriate route
self.routes[(route, direction)].setRouteData(description)
def setCityName(self, name) -> None:
"""
Sets the city name for this project (should be identical across all routes)
@param name The name of this city / project
"""
for route in self.routes:
self.routes[route].setCityName(name)
def buildLoad(self) -> None:
"""
Builds and saves the loads for all routes
"""
for key in self.routes:
self.routes[key].buildLoad()
def buildRouteTotals(self, worksheet) -> None:
"""
Builds a worksheet of route totals. See the README file for further description regarding this functionality.
@param worksheet The excel worksheet to operate on
"""
# Display headers
worksheet["A1"] = "Route #"
worksheet["A1"].alignment = Alignment(horizontal="right")
worksheet["C1"] = "Route"
worksheet["D1"] = "Ons"
worksheet["D1"].alignment = Alignment(horizontal="right")
worksheet["E1"] = "Offs"
worksheet["E1"].alignment = Alignment(horizontal="right")
worksheet["F1"] = "Total"
worksheet["F1"].alignment = Alignment(horizontal="right")
# Set the width of the columns
worksheet.column_dimensions["A"].width = 8
worksheet.column_dimensions["B"].width = 1
worksheet.column_dimensions["C"].width = 30
# Display values
current_row = 2
for key in sorted(self.routes.keys()):
offs, ons, total = self.routes[key].getTotalOffsAndOns()
worksheet.cell(row=current_row, column=1).value = key[0]
worksheet.cell(row=current_row, column=3).value = \
self.routes[key].getDescriptorAndDirectionTrunc(29)
worksheet.cell(row=current_row, column=4).value = ons
worksheet.cell(row=current_row, column=5).value = offs
worksheet.cell(row=current_row, column=6).value = total
current_row += 1
def buildMaxLoads(self, worksheet) -> None:
"""
Builds a worksheet of max loads for each route. See the README file for
further description regarding this functionality.
@param worksheet The excel worksheet to operate on
"""
# Display headers
worksheet["A1"] = "Route #"
worksheet["A1"].alignment = Alignment(horizontal="right")
worksheet["C1"] = "Route"
worksheet["D1"] = "Start Time"
worksheet["E1"] = "Ons"
worksheet["E1"].alignment = Alignment(horizontal="right")
worksheet["F1"] = "Offs"
worksheet["F1"].alignment = Alignment(horizontal="right")
worksheet["G1"] = "Max Load"
worksheet["G1"].alignment = Alignment(horizontal="right")
# Set the width of the columns
worksheet.column_dimensions["A"].width = 8
worksheet.column_dimensions["B"].width = 1
worksheet.column_dimensions["C"].width = 30
worksheet.column_dimensions["D"].width = 10
# Display values
current_row = 2
for key in sorted(self.routes.keys()):
current_row = self.routes[key].buildTotalsByTime(worksheet,\
current_row)
def buildRouteTotalsByStop(self, worksheet) -> None:
"""
Builds a worksheet of route totals for each stop. See the README file
for further description regarding this functionality.
@param worksheet The excel worksheet to operate on
"""
# Set the width of the columns
worksheet.column_dimensions["A"].width = 8
worksheet.column_dimensions["B"].width = 1
worksheet.column_dimensions["C"].width = 20
worksheet.column_dimensions["D"].width = 10
worksheet.column_dimensions["E"].width = 1
worksheet.column_dimensions["F"].width = 15
worksheet.column_dimensions["G"].width = 15
# Display values
current_row = 0
for key in sorted(self.routes.keys()):
# Display headers
current_row += 1
worksheet.cell(row=current_row, column=1).value = "Route #"
worksheet.cell(row=current_row, column=1).alignment = \
Alignment(horizontal="right")
worksheet.cell(row=current_row, column=3).value = "Route"
worksheet.cell(row=current_row, column=4).value = "Stop"
worksheet.cell(row=current_row, column=4).alignment = \
Alignment(horizontal="right")
worksheet.cell(row=current_row, column=6).value = "Street"
worksheet.cell(row=current_row, column=7).value = "Cross Street"
worksheet.cell(row=current_row, column=8).value = "Ons"
worksheet.cell(row=current_row, column=8).alignment = \
Alignment(horizontal="right")
worksheet.cell(row=current_row, column=9).value = "Offs"
worksheet.cell(row=current_row, column=9).alignment = \
Alignment(horizontal="right")
worksheet.cell(row=current_row, column=10).value = "Total"
worksheet.cell(row=current_row, column=10).alignment = \
Alignment(horizontal="right")
worksheet.cell(row=current_row, column=11).value = "Load"
worksheet.cell(row=current_row, column=11).alignment = \
Alignment(horizontal="right")
# Display stop information
current_row = self.routes[key].buildRouteTotalsByStop(worksheet, \
current_row + 1)
def buildOnTimeDetail(self, worksheet) -> None:
"""
Builds a worksheet of whether busses were on time or not. See the README
file for further description regarding this functionality.
@param worksheet The excel worksheet to operate on
"""
# Display headers
worksheet["A1"] = "Route #"
worksheet["A1"].alignment = Alignment(horizontal="right")
worksheet["C1"] = "Route Name"
worksheet["D1"] = "Date"
worksheet["D1"].alignment = Alignment(horizontal="right")
worksheet["E1"] = "Time"
worksheet["E1"].alignment = Alignment(horizontal="right")
worksheet["F1"] = "Run"
worksheet["F1"].alignment = Alignment(horizontal="right")
# Set the width of the columns
worksheet.column_dimensions["A"].width = 8
worksheet.column_dimensions["B"].width = 1
worksheet.column_dimensions["C"].width = 12
worksheet.column_dimensions["D"].width = 11
# Display values
current_row = 2
for key in sorted(self.routes.keys()):
current_row = self.routes[key].buildOnTimeDetail(worksheet, \
current_row)
def buildDetailReport(self, worksheet) -> None:
"""
Builds a detailed report of all data collected. See the README file for
further details regarding this functionality.
@param worksheet The excel worksheet to operate on
"""
# Set column widths
worksheet.column_dimensions["A"].width = 1
worksheet.column_dimensions["B"].width = 4
worksheet.column_dimensions["C"].width = 11
worksheet.column_dimensions["D"].width = 11
current_row = 1
for key in sorted(self.routes.keys()):
current_row = self.routes[key].buildDetailReport(worksheet, \
current_row)
class Route:
"""
A class that allows for easy storage of the stops inside of a route
"""
def __init__(self, route, direction: Direction, log:Log) -> None:
"""
Initialize Route with the appropriate internal variables
@param route The route number
@param direction The direction of the route as a Direction object
@param log The workbook's log object
"""
self.route = route
self.stops = {}
self.times = []
self.descriptor = "Descriptor Unset"
self.direction = direction
self.log = log
self.timed_stops = []
self.onboard = {}
self.city_name = "City Name Unset"
def __str__(self) -> str:
output = ""
for key in sorted(self.stops.keys()):
output += str(self.stops[key])
return output
def __repr__(self) -> str:
return self.__str__()
def addStop(self, stop_no, street, cross_street, is_timed) -> None:
"""
Adds a stop of stop_no to this route
@param stop_no The number of the stop.
@param street The main street of this stop.
@param cross_street The main cross street of this stop.
@param is_timed True if this is a timed stop
"""
# If stop exists, throw error and return
if stop_no in self.stops:
self.log.logError("Tried to add stop " + str(stop_no) + " to route " + str(self.route) + " when it already exists.")
return
# If timed stop, add to timed stops list
if is_timed:
self.timed_stops.append(stop_no)
self.timed_stops.sort()
self.stops[stop_no] = Stop(self.route, stop_no, street, cross_street, self.log)
self.stops[stop_no].setRouteData(self.descriptor, self.direction)
def addData(self, stop_no, datetime, run, arrival_time, schedule_time, \
offs, ons, onboard) -> bool:
"""
Adds data about a stop to a specific stop object
@param stop_no The stop number where we're adding this data
@param datetime The datetime when the route began
@param run The run value for this stop
@param arrival_time Optional: The arrival time for this stop
@param schedule_time Optional: The scheduled arrival time for this stop
@param offs Optional: The number of passengers departing the bus
@param ons Optional: The number of passengers boarding the bus
@param onboard The number of passengers carrying over from a prev route
@returns Boolean of whether the data was successfully added
"""
# If stop_no does not exist, throw error and return
if stop_no not in self.stops:
self.log.logError("Tried to add data to stop " + str(stop_no) + " in route " + str(self.route) + " " + str(self.direction) + " when stop does not exist.")
return False
# If datetime does not exist, add it to datetimes and sort
if datetime not in self.times:
self.times.append(datetime)
# Sort by time, date instead of date, time
self.times.sort(key=lambda x: (x.time(), x.date()))
# Set the onboard value for this route if the value is provided
if onboard is not None:
# Log a warning if we're changing an already set onboard number
if datetime in self.onboard and self.onboard[datetime] != onboard:
self.log.logWarning("Route " + str(self.route) + " " + str(self.direction) + " time " + \
str(datetime) + " stop " + str(stop_no) + \
"Overriding onboard value " + str(self.onboard[datetime]) +\
" with new value " + str(onboard))
# Set the value regardless
self.onboard[datetime] = onboard
return self.stops[stop_no].addData(datetime, run, arrival_time, \
schedule_time, offs, ons)
def setRouteData(self, description) -> None:
"""
Sets the metadata for this route
@param description A text description of the route (University, Uptown)
"""
self.descriptor = description
# Set for all child stops
for stop_no in self.stops:
self.stops[stop_no].setRouteData(description, direction)
def setCityName(self, name) -> None:
"""
Sets the city name for this route (should be identical across all routes)
@param name The name of this city / project
"""
self.city_name = name
def buildLoad(self) -> None:
"""
Builds and saves the loads for all data points within this route
"""
times_by_datetime = sorted(self.times)
current_load = 0
for datetime in times_by_datetime:
# Set the starting load to the onboard value (stored)
current_load = 0
if datetime in self.onboard:
current_load = self.onboard[datetime]
for stop_no in sorted(self.stops.keys()):
current_off, current_on = \
self.stops[stop_no].getOffsAndOns(datetime)
current_load = current_load + current_on - current_off
self.stops[stop_no].setLoad(datetime, current_load)
# Display an error if current_load drops below 0
if current_load < 0:
self.log.logWarning(f"Route {self.route} {self.direction} {datetime} stop {stop_no}: The load has dropped below 0 (check for bad data)")
def getDescriptorAndDirection(self) -> str:
"""
@returns A string representation of this routes descriptor and direction
"""
return self.descriptor + " " + str(self.direction.value)
def getDescriptorAndDirectionTrunc(self, l) -> str:
"""
@param l Length of the output string
@returns A truncated string representation of this route's descriptor and direction
"""
remaining_l = max(0, l - len(str(self.direction.value)) - 1)
return self.descriptor[0:remaining_l] + " " + str(self.direction.value)
def getTotalOffsAndOns(self) -> tuple:
"""
@returns the total offs, ons, and total passengers for this route over
all stops
"""
offs = 0
ons = 0
for stop_no in self.stops:
current_off, current_on = self.stops[stop_no].getTotalOffsAndOns()
offs += current_off
ons += current_on
total = offs + ons
return offs, ons, total
def buildTotalsByTime(self, worksheet, current_row) -> int:
"""
Output totals for every time of every route into a provided workbook
@param worksheet The workbook to output to
@param current_row The row of the worksheet to start on
@returns The next empty row in the worksheet
"""
# Loop over every datetime
dt_search_index = 0
while dt_search_index < len(self.times):
# Get a list of all datetimes with the same start time as the
# current index
search_datetimes = [self.times[dt_search_index]]
dt_search_index += 1
while dt_search_index < len(self.times) and \
search_datetimes[0].time() == \
self.times[dt_search_index].time():
search_datetimes.append(self.times[dt_search_index])
dt_search_index += 1
# Generate totals and max for all stops for all datetimes in list
offs = 0
ons = 0
current_load = 0
max_load = 0
for datetime in search_datetimes:
for stop_no in self.stops:
current_off, current_on, current_load = \
self.stops[stop_no].getOffsOnsAndLoad(datetime)
# Add to tallies
offs += current_off
ons += current_on
# Save max load if is the current max
if current_load > max_load:
max_load = current_load
# Write data to sheet
worksheet.cell(row=current_row, column=1).value = self.route
worksheet.cell(row=current_row, column=3).value = \
self.getDescriptorAndDirectionTrunc(29)
worksheet.cell(row=current_row, column=4).value = \
datetime.strftime("%H:%M")
worksheet.cell(row=current_row, column=5).value = ons
worksheet.cell(row=current_row, column=6).value = offs
worksheet.cell(row=current_row, column=7).value = max_load
current_row += 1
return current_row
def buildRouteTotalsByStop(self, worksheet, current_row) -> int:
"""
Builds total values for the route for each stop.
@param worksheet The excel worksheet to operate on
@param current_row The row of the worksheet to start on
@returns The next empty row in the worksheet
"""
# Include the onboard row
worksheet.cell(row=current_row, column=7).value = "Onboard"
# Get the total onboards for the route
onboard_total = 0
for time in self.onboard:
onboard_total += self.onboard[time]
worksheet.cell(row=current_row, column=11).value = onboard_total
current_row += 1
# Build totals for all stops
running_totals = [0, 0, 0, onboard_total]
for stop_no in self.stops:
# if this stop is a timed stop, shade it yellow
if stop_no in self.timed_stops:
worksheet.cell(row=current_row, column=4).fill = \
PatternFill(patternType="solid", fill_type="solid", fgColor=Color("FFFF00"))
worksheet.cell(row=current_row, column=5).fill = \
PatternFill(patternType="solid", fill_type="solid", fgColor=Color("FFFF00"))
worksheet.cell(row=current_row, column=6).fill = \
PatternFill(patternType="solid", fill_type="solid", fgColor=Color("FFFF00"))
worksheet.cell(row=current_row, column=7).fill = \
PatternFill(patternType="solid", fill_type="solid", fgColor=Color("FFFF00"))
worksheet.cell(row=current_row, column=8).fill = \
PatternFill(patternType="solid", fill_type="solid", fgColor=Color("FFFF00"))
worksheet.cell(row=current_row, column=9).fill = \
PatternFill(patternType="solid", fill_type="solid", fgColor=Color("FFFF00"))
worksheet.cell(row=current_row, column=10).fill = \
PatternFill(patternType="solid", fill_type="solid", fgColor=Color("FFFF00"))
worksheet.cell(row=current_row, column=11).fill = \
PatternFill(patternType="solid", fill_type="solid", fgColor=Color("FFFF00"))
# input stop data
current_row, running_totals = self.stops[stop_no].buildRouteTotalsByStop( \
worksheet, current_row, running_totals)
# Display totals
worksheet.cell(row=current_row, column=7).value = "Totals"
worksheet.cell(row=current_row, column=8).value = running_totals[0]
worksheet.cell(row=current_row, column=9).value = running_totals[1]
worksheet.cell(row=current_row, column=10).value = running_totals[2]
worksheet.cell(row=current_row, column=11).value = running_totals[3]
return current_row + 1
def buildOnTimeDetail(self, worksheet, current_row) -> int:
"""
Builds table of stops that are on time or not.
@param worksheet The excel worksheet to operate on
@param current_row The row of the worksheet to start on
@returns The next empty row in the worksheet
"""
# If there are no timed stops, skip this stop
if len(self.timed_stops) == 0:
return current_row
# Build header
col = 7
for stop in self.timed_stops:
worksheet.cell(row=current_row, column=col).value = \
self.stops[stop].getStreetTrunc(7)
worksheet.cell(row=current_row + 1, column=col).value = \
self.stops[stop].getCrossStreetTrunc(7)
col += 1
current_row += 2
# Generate one row per time
for datetime in self.times:
# Write route data to row
worksheet.cell(row=current_row, column=1).value = self.route
worksheet.cell(row=current_row, column=3).value = \
self.getDescriptorAndDirectionTrunc(10)
worksheet.cell(row=current_row, column=4).value = datetime.date()
worksheet.cell(row=current_row, column=5).value = datetime.time()
# Write run number to row
worksheet.cell(row=current_row, column=6).value = \
self.stops[self.timed_stops[0]].getRun(datetime)
worksheet.cell(row=current_row, column=6).alignment = \
Alignment(horizontal="right")
# Write stop data to row
col = 7
for stop in self.timed_stops:
minutes_late = self.stops[stop].getMinutesLate(datetime)
# Handle error cases
if minutes_late is None:
worksheet.cell(row=current_row, column=col).value = "NA"
else:
worksheet.cell(row=current_row, column=col).value = \
minutes_late
col += 1
current_row += 1
return current_row + 1
def buildDetailReport(self, worksheet, current_row) -> int:
"""
Builds a detailed report of all data collected.
@param worksheet The excel worksheet to operate on
@param current_row The worksheet row to start on
@returns The next empty row in the worksheet
"""
# Skip if no data stored
if self.times is None or len(self.times) == 0:
return current_row
# Display the header
worksheet.cell(row=current_row, column=3).value = self.city_name
worksheet.cell(row=current_row + 1, column=3).value = "Route #" + str(self.route)
worksheet.cell(row=current_row + 1, column=4).value = \
self.getDescriptorAndDirection()
worksheet.cell(row=current_row + 3, column=3).value = "Stop Location"
worksheet.cell(row=current_row + 4, column=4).value = "Onboard"
# Display the time headers, sort by time, date
col_totals = {}
col = 5
self.times.sort(key=lambda x: (x.time(), x.date()))
for datetime in self.times:
worksheet.cell(row=current_row+2, column=col).value = \
datetime.date()
worksheet.cell(row=current_row+2, column=col+1).value = \
datetime.time()
worksheet.cell(row=current_row+3, column=col).value = "On"
worksheet.cell(row=current_row+3, column=col+1).value = "Off"
worksheet.cell(row=current_row+3, column=col+2).value = "OB"
worksheet.column_dimensions[openpyxl.utils.get_column_letter(col)].width = 11
# Display onboard if it exists
if datetime in self.onboard:
worksheet.cell(row=current_row+4, column=col+2).value = \
self.onboard[datetime]
col_totals[col + 2] = self.onboard[datetime]
else:
worksheet.cell(row=current_row+4, column=col+2).value = 0
col += 3
# Display the stops with info
current_row += 5
for stop_no in self.stops:
col_totals = self.stops[stop_no].buildDetailReport( \
worksheet, current_row, col_totals)
current_row += 1
# Display totals
worksheet.cell(row=current_row, column=4).value = "Totals"
for col_key in col_totals:
worksheet.cell(row=current_row, column=col_key).value = col_totals[col_key]
return current_row + 2
class Stop:
"""
A class that represents a single stop on a bus route
"""
def __init__(self, route, stop_no, street, cross_street, log:Log) -> None:
"""
Initialize with metadata
@param route The route number this stop sits on
@param stop_no The stop number of this stop
@param street The main street of this stop
@param cross_street The cross street of this stop
@param log The workbook's log object
"""
self.route = route
self.stop_no = stop_no
self.street = street
self.cross_street = cross_street
self.descriptor = "Descriptor Unset"
self.direction = Direction.UN
self.log = log
# Data is of the following format:
# [
# run,
# arrival_time,
# schedule_time,
# offs,
# ons,
# load
# ]
self.data = {}
def __str__(self) -> str:
output = str(self.route) + ": " + str(self.stop_no) + " [" + \
str(self.street) + "/" + str(self.cross_street) + "]\n"
for datetime in sorted(self.data.keys()):
output += str(datetime) + " " + str(self.data[datetime][0]) + " " + str(self.data[datetime][1]) + " " + str(self.data[datetime][2]) + " " + str(self.data[datetime][3]) + " " + str(self.data[datetime][4]) + "\n"
output += "\n"
return output
def __repr__(self) -> str:
return self.__str__()
def addData(self, datetime, run, arrival_time, schedule_time, offs, ons)\
-> bool:
"""
Add data from a certain run to this stop
@param datetime The datetime when this route begain
@param run The run value of this stop
@param arrival_time Optional: The arrival time for this stop
@param schedule_time Optional: The scheduled arrival time for this stop
@param offs Optional: The number of passengers departing the bus
@param ons Optional: The number of passengers boarding the bus
@returns Boolean of whether the data was successfully added
"""
# Check whether this data is a duplicate of an existing row
if datetime in self.data:
self.log.logError(f"Route {self.route} stop {self.stop_no} at {datetime}: Tried to overwrite existing data. Check input for duplicate rows.")
return False
# Clean input data
if not (isinstance(offs, int) and offs >= 0):
offs = 0
if not (isinstance(ons, int) and ons >= 0):
ons = 0
self.data[datetime] = [run, arrival_time, schedule_time, offs, ons, 0]
return True
def setLoad(self, datetime, load) -> None:
"""
Sets the load for a given datetime.
@param datetime The datetime when this route began
@param load The passenger load
"""
# Add data if doesn't exist
if datetime not in self.data:
self.data[datetime] = [None, None, None, 0, 0, load]
else:
self.data[datetime][5] = load
def setRouteData(self, description, direction: Direction) -> None:
"""
Sets the metadata for this route
@param description A text description of the route (University, Uptown)
@param direction The direction of the route as a Direction object
"""
self.descriptor = description
self.direction = direction
def getDescriptorAndDirection(self) -> str:
"""
@returns A string representation of this routes descriptor and direction
"""
return self.descriptor + " " + str(self.direction.value)
def getDescriptorAndDirectionTrunc(self, l) -> str:
"""
@param l Length of the output string
@returns A truncated string representation of this routes descriptor and direction
"""
remaining_l = max(0, l - len(str(self.direction.value)) - 1)
return self.descriptor[0:remaining_l] + " " + str(self.direction.value)
def getRun(self, datetime) -> str:
# If datetime does not exist, return None
if datetime not in self.data:
return None
return self.data[datetime][0]
def getOffsAndOns(self, datetime) -> tuple:
"""
@returns the offs and ons for a specific datetime
"""
# If datetime does not exist, return zeros
if datetime not in self.data:
return 0, 0
return self.data[datetime][3], self.data[datetime][4]
def getOffsOnsAndLoad(self, datetime) -> tuple:
"""
@returns the offs ons and load for a specific datetime
"""
# If datetime does not exist, return zeros
if datetime not in self.data:
return 0, 0, 0
return self.data[datetime][3], self.data[datetime][4],\
self.data[datetime][5]
def getTotalOffsAndOns(self) -> tuple:
"""
@returns the total offs and ons for all datetimes at this stop
"""
total_offs = 0
total_ons = 0
for datetime in self.data:
total_offs += self.data[datetime][3]
total_ons += self.data[datetime][4]
return total_offs, total_ons
def getStreetTrunc(self, l) -> str:
"""
@param l Length of the output string
@returns A truncated string representation of this stop's street
"""
if self.street is None:
return None
return str(self.street)[0:l]
def getCrossStreetTrunc(self, l) -> str:
"""
@param l Length of the output string
@returns A truncated string representation of this stop's cross street
"""
if self.cross_street is None:
return None
return str(self.cross_street)[0:l]
def buildRouteTotalsByStop(self, worksheet, current_row, running_totals) -> Tuple[int, List[int]]:
"""
Builds a route total for this stop for all datetimes
@param worksheet The worksheet to operate on
@param current_row The row to start on in the worksheet
@ param running_totals A list of running totals for each statistic
@returns The next blank row on the workbook, the running totals
"""
ons = 0
offs = 0
load = 0
for key in self.data:
offs += self.data[key][3]
ons += self.data[key][4]
load += self.data[key][5]
total = ons + offs
# Write data to sheet
worksheet.cell(row=current_row, column=1).value = self.route
worksheet.cell(row=current_row, column=3).value = \
self.getDescriptorAndDirectionTrunc(19)
worksheet.cell(row=current_row, column=4).value = self.stop_no
worksheet.cell(row=current_row, column=6).value = self.getStreetTrunc(14)
worksheet.cell(row=current_row, column=7).value = self.getCrossStreetTrunc(14)
worksheet.cell(row=current_row, column=8).value = ons
worksheet.cell(row=current_row, column=9).value = offs
worksheet.cell(row=current_row, column=10).value = total
worksheet.cell(row=current_row, column=11).value = load
# Update the running totals
running_totals[0] += ons
running_totals[1] += offs
running_totals[2] += total
running_totals[3] += load
return current_row + 1, running_totals
def getMinutesLate(self, datetime) -> int:
"""
Gets the minutes late a bus is for a given datetime
@param datetime The datetime of the stop
@returns How many minutes the bus was late. Early busses are negatives.
Returns None if datetime does not exist.
"""
# If datetime doesn't exist, return None
if datetime not in self.data:
return None
# If datetime does not have an arrival time or schedule time, ret None
data = self.data[datetime]
if data[1] is None or data[2] is None:
return None
# Data presumed good, return difference in minutes
delta = data[1] - data[2]
return round(delta.total_seconds() / 60)
def buildDetailReport(self, worksheet, current_row, col_totals) -> Dict[int, int]:
"""
Builds a single row of a detail report sheet.
@param worksheet The worksheet to modify
@param current_row The current row being operated on
@param col_totals A running total of all displayed columns for this route
@returns A dictionary of the current column totals
"""
# Display header
worksheet.cell(row=current_row, column=2).value = self.stop_no
worksheet.cell(row=current_row, column=3).value = self.getStreetTrunc(10)
worksheet.cell(row=current_row, column=4).value = self.getCrossStreetTrunc(10)
# Display data for each datetime
col = 5
keys = list(self.data.keys())
keys.sort(key=lambda x: (x.time(), x.date()))
for datetime in keys:
# Add data to the sheet
worksheet.cell(row=current_row, column=col).value = \
self.data[datetime][4]
worksheet.cell(row=current_row, column=col + 1).value = \
self.data[datetime][3]
worksheet.cell(row=current_row, column=col + 2).value = \
self.data[datetime][5]
# Add the totals to the running totals dict
# Ternary ops handle the case of uninitialized columns
col_totals[col] = self.data[datetime][4] + (col_totals[col] if col in col_totals else 0)
col_totals[col + 1] = self.data[datetime][3] + (col_totals[col + 1] if col + 1 in col_totals else 0)
col_totals[col + 2] = self.data[datetime][5] + (col_totals[col + 2] if col + 2 in col_totals else 0)
# Increment the current column
col += 3
# Return the column totals
return col_totals
def generateSummary(log:Log, ride_checks_filepath, bus_stop_filepath,
output_filepath) -> int:
"""
Creates an output workbook from the provided input workbooks. See the README
for more information on how this function operates.
@param ride_checks_filepath The filepath for the ridechecks workbook
@param bus_stop_filepath The filepath for the bus stop workbook
@param output_filepath The filepath for the output workbook
@returns An integer representing the status of the output workbook:
0 - OK, success or minor errors
1 - Major error. Check the workbook log for details.
2 - Output workbook could not be created
"""
# Create output workbook / sheet. Set date format to ISO 8601
wb = openpyxl.Workbook()
wb.iso_dates = True
log.logGeneral("Output document created")
# Try to open the ride checks file, if can't return major error
log.logGeneral("Loading ride checks workbook (this may take some time)")
try:
ride_checks_wb = openpyxl.load_workbook(filename=ride_checks_filepath, data_only=True)
except Exception:
log.logError("Could not open the ride checks workbook '" +\
ride_checks_filepath + "'")