From b98ffc0f7fa2efad94c33687eb35f38cc27af3cc Mon Sep 17 00:00:00 2001 From: Rick Date: Tue, 30 Sep 2025 17:44:46 -0400 Subject: [PATCH 01/19] setup nests for shuttleIDs: locations, loops, and breaks --- server/routes.py | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/server/routes.py b/server/routes.py index 209754950..e74768c5e 100644 --- a/server/routes.py +++ b/server/routes.py @@ -205,23 +205,45 @@ def data_today(): ).order_by(GeofenceEvent.event_time.asc()).all() locations_today_dict = {} + # locations for location in locations_today: - vehicle_location = { + # _coordinate = (location.latitude, location.longitude), use _corrdinate[0] and [1] for later + # _closest_route_location = Stops.get_closest_point(location.latitude, location.longitude) + vechicle_location = { "latitude": location.latitude, "longitude": location.longitude, - "timestamp": location.timestamp, - "speed_mph": location.speed_mph, - "heading_degrees": location.heading_degrees, - "address_id": location.address_id + "closest_route_location": (0, 0), # update this, maybe Stops.get_closest_point(_current_coords) + "distance": 0, # update this, maybe Stops.haversine((location.latitude, location.longitude), _closest_route_location) + "closest_route": "some", # update this... + "closest_polyline": 0 # update this... } if location.vehicle_id in locations_today_dict: - locations_today_dict[location.vehicle_id]["data"].append(vehicle_location) + locations_today_dict[location.vehicle_id]["locations"].append(vechicle_location) else: locations_today_dict[location.vehicle_id] = { - "entry": None, - "exit": None, - "data": [vehicle_location] + "locations": {location.timestamp: vechicle_location}, + "loops": [], + "breaks": [] } + # for location in locations_today: + # vehicle_location = { + # "latitude": location.latitude, + # "longitude": location.longitude, + # "timestamp": location.timestamp, + # "speed_mph": location.speed_mph, + # "heading_degrees": location.heading_degrees, + # "address_id": location.address_id + # } + # if location.vehicle_id in locations_today_dict: + # locations_today_dict[location.vehicle_id]["data"].append(vehicle_location) + # else: + # locations_today_dict[location.vehicle_id] = { + # "entry": None, + # "exit": None, + # "data": [vehicle_location] + # } + + # loops and breaks for e, geofence_event in enumerate(events_today): if geofence_event.event_type == "geofenceEntry": if "entry" not in locations_today_dict[geofence_event.vehicle_id]: # first entry From 1e5ab5377c301ec23583b527aef37b5c77644cd2 Mon Sep 17 00:00:00 2001 From: Rick Date: Tue, 30 Sep 2025 17:44:46 -0400 Subject: [PATCH 02/19] setup nests for shuttleIDs: locations, loops, and breaks --- server/routes.py | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/server/routes.py b/server/routes.py index e03a7215c..026c8af3d 100644 --- a/server/routes.py +++ b/server/routes.py @@ -231,23 +231,45 @@ def data_today(): ).order_by(GeofenceEvent.event_time.asc()).all() locations_today_dict = {} + # locations for location in locations_today: - vehicle_location = { + # _coordinate = (location.latitude, location.longitude), use _corrdinate[0] and [1] for later + # _closest_route_location = Stops.get_closest_point(location.latitude, location.longitude) + vechicle_location = { "latitude": location.latitude, "longitude": location.longitude, - "timestamp": location.timestamp, - "speed_mph": location.speed_mph, - "heading_degrees": location.heading_degrees, - "address_id": location.address_id + "closest_route_location": (0, 0), # update this, maybe Stops.get_closest_point(_current_coords) + "distance": 0, # update this, maybe Stops.haversine((location.latitude, location.longitude), _closest_route_location) + "closest_route": "some", # update this... + "closest_polyline": 0 # update this... } if location.vehicle_id in locations_today_dict: - locations_today_dict[location.vehicle_id]["data"].append(vehicle_location) + locations_today_dict[location.vehicle_id]["locations"].append(vechicle_location) else: locations_today_dict[location.vehicle_id] = { - "entry": None, - "exit": None, - "data": [vehicle_location] + "locations": {location.timestamp: vechicle_location}, + "loops": [], + "breaks": [] } + # for location in locations_today: + # vehicle_location = { + # "latitude": location.latitude, + # "longitude": location.longitude, + # "timestamp": location.timestamp, + # "speed_mph": location.speed_mph, + # "heading_degrees": location.heading_degrees, + # "address_id": location.address_id + # } + # if location.vehicle_id in locations_today_dict: + # locations_today_dict[location.vehicle_id]["data"].append(vehicle_location) + # else: + # locations_today_dict[location.vehicle_id] = { + # "entry": None, + # "exit": None, + # "data": [vehicle_location] + # } + + # loops and breaks for e, geofence_event in enumerate(events_today): if geofence_event.event_type == "geofenceEntry": if "entry" not in locations_today_dict[geofence_event.vehicle_id]: # first entry From 131a4bb76c0a0ee2b5e396299a41acf4ba9481f1 Mon Sep 17 00:00:00 2001 From: Rick Date: Tue, 21 Oct 2025 16:42:32 -0400 Subject: [PATCH 03/19] conditional additions --- data/aggregated_schedule.json | 1076 +++++++++++++++++++++++++++++++++ server/routes.py | 78 ++- 2 files changed, 1128 insertions(+), 26 deletions(-) create mode 100644 data/aggregated_schedule.json diff --git a/data/aggregated_schedule.json b/data/aggregated_schedule.json new file mode 100644 index 000000000..5c2be3e48 --- /dev/null +++ b/data/aggregated_schedule.json @@ -0,0 +1,1076 @@ +[ + { + "NORTH": [ + "9:00 AM", + "9:20 AM", + "9:40 AM", + "10:00 AM", + "10:20 AM", + "10:40 AM", + "11:00 AM", + "11:20 AM", + "11:40 AM", + "12:00 PM", + "12:10 PM", + "12:30 PM", + "12:50 PM", + "1:00 PM", + "1:10 PM", + "1:20 PM", + "1:30 PM", + "1:40 PM", + "1:50 PM", + "2:00 PM", + "2:10 PM", + "2:20 PM", + "2:30 PM", + "2:40 PM", + "2:50 PM", + "3:00 PM", + "3:10 PM", + "3:20 PM", + "3:40 PM", + "4:00 PM", + "4:10 PM", + "4:30 PM", + "4:50 PM", + "5:10 PM", + "5:30 PM", + "5:50 PM", + "6:10 PM", + "6:30 PM", + "6:50 PM", + "7:10 PM", + "7:30 PM", + "8:00 PM" + ], + "WEST": [ + "9:00 AM", + "9:20 AM", + "9:40 AM", + "10:00 AM", + "10:20 AM", + "10:40 AM", + "11:00 AM", + "11:20 AM", + "11:40 AM", + "12:00 PM", + "12:10 PM", + "12:30 PM", + "12:50 PM", + "1:00 PM", + "1:10 PM", + "1:20 PM", + "1:30 PM", + "1:40 PM", + "1:50 PM", + "2:00 PM", + "2:10 PM", + "2:20 PM", + "2:30 PM", + "2:40 PM", + "2:50 PM", + "3:00 PM", + "3:10 PM", + "3:20 PM", + "3:40 PM", + "4:00 PM", + "4:10 PM", + "4:30 PM", + "4:50 PM", + "5:10 PM", + "5:30 PM", + "5:50 PM", + "6:10 PM", + "6:30 PM", + "6:50 PM", + "7:10 PM", + "7:30 PM", + "8:00 PM" + ] + }, + { + "WEST": [ + "7:00 AM", + "7:10 AM", + "7:20 AM", + "7:30 AM", + "7:40 AM", + "7:50 AM", + "8:00 AM", + "8:10 AM", + "8:20 AM", + "8:30 AM", + "8:40 AM", + "8:50 AM", + "9:00 AM", + "9:10 AM", + "9:20 AM", + "9:25 AM", + "9:30 AM", + "9:40 AM", + "9:45 AM", + "9:50 AM", + "10:00 AM", + "10:10 AM", + "10:20 AM", + "10:30 AM", + "10:40 AM", + "10:45 AM", + "10:50 AM", + "11:00 AM", + "11:10 AM", + "11:20 AM", + "11:30 AM", + "11:40 AM", + "11:50 AM", + "12:00 PM", + "12:10 PM", + "12:20 PM", + "12:30 PM", + "12:40 PM", + "12:50 PM", + "1:00 PM", + "1:10 PM", + "1:20 PM", + "1:30 PM", + "1:40 PM", + "1:50 PM", + "2:00 PM", + "2:10 PM", + "2:15 PM", + "2:20 PM", + "2:30 PM", + "2:35 PM", + "2:40 PM", + "2:50 PM", + "3:00 PM", + "3:10 PM", + "3:20 PM", + "3:30 PM", + "3:35 PM", + "3:40 PM", + "3:50 PM", + "3:55 PM", + "4:00 PM", + "4:20 PM", + "4:35 PM", + "4:40 PM", + "5:00 PM", + "5:15 PM", + "5:20 PM", + "5:40 PM", + "5:55 PM", + "6:00 PM", + "6:20 PM", + "6:40 PM", + "7:00 PM", + "8:00 PM", + "8:20 PM", + "8:40 PM", + "9:00 PM", + "9:20 PM", + "9:40 PM", + "10:00 PM", + "10:20 PM", + "10:40 PM", + "11:00 PM", + "11:20 PM", + "11:40 PM", + "12:00 AM" + ], + "NORTH": [ + "7:00 AM", + "7:10 AM", + "7:20 AM", + "7:30 AM", + "7:40 AM", + "7:50 AM", + "8:00 AM", + "8:10 AM", + "8:20 AM", + "8:30 AM", + "8:40 AM", + "8:50 AM", + "9:00 AM", + "9:10 AM", + "9:20 AM", + "9:30 AM", + "9:40 AM", + "9:50 AM", + "10:00 AM", + "10:05 AM", + "10:10 AM", + "10:20 AM", + "10:25 AM", + "10:30 AM", + "10:40 AM", + "10:50 AM", + "11:00 AM", + "11:10 AM", + "11:20 AM", + "11:30 AM", + "11:40 AM", + "11:50 AM", + "12:00 PM", + "12:10 PM", + "12:20 PM", + "12:30 PM", + "12:40 PM", + "12:50 PM", + "1:00 PM", + "1:10 PM", + "1:20 PM", + "1:30 PM", + "1:40 PM", + "1:50 PM", + "2:00 PM", + "2:10 PM", + "2:20 PM", + "2:30 PM", + "2:40 PM", + "2:50 PM", + "2:55 PM", + "3:00 PM", + "3:10 PM", + "3:15 PM", + "3:20 PM", + "3:30 PM", + "3:40 PM", + "3:50 PM", + "4:00 PM", + "4:15 PM", + "4:20 PM", + "4:40 PM", + "4:55 PM", + "5:00 PM", + "5:20 PM", + "5:35 PM", + "5:40 PM", + "6:00 PM", + "6:15 PM", + "6:20 PM", + "6:40 PM", + "7:00 PM", + "8:00 PM", + "8:20 PM", + "8:40 PM", + "9:00 PM", + "9:20 PM", + "9:40 PM", + "10:00 PM", + "10:20 PM", + "10:40 PM", + "11:00 PM", + "11:20 PM", + "11:40 PM", + "12:00 AM" + ] + }, + { + "WEST": [ + "7:00 AM", + "7:10 AM", + "7:20 AM", + "7:30 AM", + "7:40 AM", + "7:50 AM", + "8:00 AM", + "8:10 AM", + "8:20 AM", + "8:30 AM", + "8:40 AM", + "8:50 AM", + "9:00 AM", + "9:10 AM", + "9:20 AM", + "9:25 AM", + "9:30 AM", + "9:40 AM", + "9:45 AM", + "9:50 AM", + "10:00 AM", + "10:10 AM", + "10:20 AM", + "10:30 AM", + "10:40 AM", + "10:45 AM", + "10:50 AM", + "11:00 AM", + "11:10 AM", + "11:20 AM", + "11:30 AM", + "11:40 AM", + "11:50 AM", + "12:00 PM", + "12:10 PM", + "12:20 PM", + "12:30 PM", + "12:40 PM", + "12:50 PM", + "1:00 PM", + "1:10 PM", + "1:20 PM", + "1:30 PM", + "1:40 PM", + "1:50 PM", + "2:00 PM", + "2:10 PM", + "2:15 PM", + "2:20 PM", + "2:30 PM", + "2:35 PM", + "2:40 PM", + "2:50 PM", + "3:00 PM", + "3:10 PM", + "3:20 PM", + "3:30 PM", + "3:35 PM", + "3:40 PM", + "3:50 PM", + "3:55 PM", + "4:00 PM", + "4:20 PM", + "4:35 PM", + "4:40 PM", + "5:00 PM", + "5:15 PM", + "5:20 PM", + "5:40 PM", + "5:55 PM", + "6:00 PM", + "6:20 PM", + "6:40 PM", + "7:00 PM", + "8:00 PM", + "8:20 PM", + "8:40 PM", + "9:00 PM", + "9:20 PM", + "9:40 PM", + "10:00 PM", + "10:20 PM", + "10:40 PM", + "11:00 PM", + "11:20 PM", + "11:40 PM", + "12:00 AM" + ], + "NORTH": [ + "7:00 AM", + "7:10 AM", + "7:20 AM", + "7:30 AM", + "7:40 AM", + "7:50 AM", + "8:00 AM", + "8:10 AM", + "8:20 AM", + "8:30 AM", + "8:40 AM", + "8:50 AM", + "9:00 AM", + "9:10 AM", + "9:20 AM", + "9:30 AM", + "9:40 AM", + "9:50 AM", + "10:00 AM", + "10:05 AM", + "10:10 AM", + "10:20 AM", + "10:25 AM", + "10:30 AM", + "10:40 AM", + "10:50 AM", + "11:00 AM", + "11:10 AM", + "11:20 AM", + "11:30 AM", + "11:40 AM", + "11:50 AM", + "12:00 PM", + "12:10 PM", + "12:20 PM", + "12:30 PM", + "12:40 PM", + "12:50 PM", + "1:00 PM", + "1:10 PM", + "1:20 PM", + "1:30 PM", + "1:40 PM", + "1:50 PM", + "2:00 PM", + "2:10 PM", + "2:20 PM", + "2:30 PM", + "2:40 PM", + "2:50 PM", + "2:55 PM", + "3:00 PM", + "3:10 PM", + "3:15 PM", + "3:20 PM", + "3:30 PM", + "3:40 PM", + "3:50 PM", + "4:00 PM", + "4:15 PM", + "4:20 PM", + "4:40 PM", + "4:55 PM", + "5:00 PM", + "5:20 PM", + "5:35 PM", + "5:40 PM", + "6:00 PM", + "6:15 PM", + "6:20 PM", + "6:40 PM", + "7:00 PM", + "8:00 PM", + "8:20 PM", + "8:40 PM", + "9:00 PM", + "9:20 PM", + "9:40 PM", + "10:00 PM", + "10:20 PM", + "10:40 PM", + "11:00 PM", + "11:20 PM", + "11:40 PM", + "12:00 AM" + ] + }, + { + "WEST": [ + "7:00 AM", + "7:10 AM", + "7:20 AM", + "7:30 AM", + "7:40 AM", + "7:50 AM", + "8:00 AM", + "8:10 AM", + "8:20 AM", + "8:30 AM", + "8:40 AM", + "8:50 AM", + "9:00 AM", + "9:10 AM", + "9:20 AM", + "9:25 AM", + "9:30 AM", + "9:40 AM", + "9:45 AM", + "9:50 AM", + "10:00 AM", + "10:10 AM", + "10:20 AM", + "10:30 AM", + "10:40 AM", + "10:45 AM", + "10:50 AM", + "11:00 AM", + "11:10 AM", + "11:20 AM", + "11:30 AM", + "11:40 AM", + "11:50 AM", + "12:00 PM", + "12:10 PM", + "12:20 PM", + "12:30 PM", + "12:40 PM", + "12:50 PM", + "1:00 PM", + "1:10 PM", + "1:20 PM", + "1:30 PM", + "1:40 PM", + "1:50 PM", + "2:00 PM", + "2:10 PM", + "2:15 PM", + "2:20 PM", + "2:30 PM", + "2:35 PM", + "2:40 PM", + "2:50 PM", + "3:00 PM", + "3:10 PM", + "3:20 PM", + "3:30 PM", + "3:35 PM", + "3:40 PM", + "3:50 PM", + "3:55 PM", + "4:00 PM", + "4:20 PM", + "4:35 PM", + "4:40 PM", + "5:00 PM", + "5:15 PM", + "5:20 PM", + "5:40 PM", + "5:55 PM", + "6:00 PM", + "6:20 PM", + "6:40 PM", + "7:00 PM", + "8:00 PM", + "8:20 PM", + "8:40 PM", + "9:00 PM", + "9:20 PM", + "9:40 PM", + "10:00 PM", + "10:20 PM", + "10:40 PM", + "11:00 PM", + "11:20 PM", + "11:40 PM", + "12:00 AM" + ], + "NORTH": [ + "7:00 AM", + "7:10 AM", + "7:20 AM", + "7:30 AM", + "7:40 AM", + "7:50 AM", + "8:00 AM", + "8:10 AM", + "8:20 AM", + "8:30 AM", + "8:40 AM", + "8:50 AM", + "9:00 AM", + "9:10 AM", + "9:20 AM", + "9:30 AM", + "9:40 AM", + "9:50 AM", + "10:00 AM", + "10:05 AM", + "10:10 AM", + "10:20 AM", + "10:25 AM", + "10:30 AM", + "10:40 AM", + "10:50 AM", + "11:00 AM", + "11:10 AM", + "11:20 AM", + "11:30 AM", + "11:40 AM", + "11:50 AM", + "12:00 PM", + "12:10 PM", + "12:20 PM", + "12:30 PM", + "12:40 PM", + "12:50 PM", + "1:00 PM", + "1:10 PM", + "1:20 PM", + "1:30 PM", + "1:40 PM", + "1:50 PM", + "2:00 PM", + "2:10 PM", + "2:20 PM", + "2:30 PM", + "2:40 PM", + "2:50 PM", + "2:55 PM", + "3:00 PM", + "3:10 PM", + "3:15 PM", + "3:20 PM", + "3:30 PM", + "3:40 PM", + "3:50 PM", + "4:00 PM", + "4:15 PM", + "4:20 PM", + "4:40 PM", + "4:55 PM", + "5:00 PM", + "5:20 PM", + "5:35 PM", + "5:40 PM", + "6:00 PM", + "6:15 PM", + "6:20 PM", + "6:40 PM", + "7:00 PM", + "8:00 PM", + "8:20 PM", + "8:40 PM", + "9:00 PM", + "9:20 PM", + "9:40 PM", + "10:00 PM", + "10:20 PM", + "10:40 PM", + "11:00 PM", + "11:20 PM", + "11:40 PM", + "12:00 AM" + ] + }, + { + "WEST": [ + "7:00 AM", + "7:10 AM", + "7:20 AM", + "7:30 AM", + "7:40 AM", + "7:50 AM", + "8:00 AM", + "8:10 AM", + "8:20 AM", + "8:30 AM", + "8:40 AM", + "8:50 AM", + "9:00 AM", + "9:10 AM", + "9:20 AM", + "9:25 AM", + "9:30 AM", + "9:40 AM", + "9:45 AM", + "9:50 AM", + "10:00 AM", + "10:10 AM", + "10:20 AM", + "10:30 AM", + "10:40 AM", + "10:45 AM", + "10:50 AM", + "11:00 AM", + "11:10 AM", + "11:20 AM", + "11:30 AM", + "11:40 AM", + "11:50 AM", + "12:00 PM", + "12:10 PM", + "12:20 PM", + "12:30 PM", + "12:40 PM", + "12:50 PM", + "1:00 PM", + "1:10 PM", + "1:20 PM", + "1:30 PM", + "1:40 PM", + "1:50 PM", + "2:00 PM", + "2:10 PM", + "2:15 PM", + "2:20 PM", + "2:30 PM", + "2:35 PM", + "2:40 PM", + "2:50 PM", + "3:00 PM", + "3:10 PM", + "3:20 PM", + "3:30 PM", + "3:35 PM", + "3:40 PM", + "3:50 PM", + "3:55 PM", + "4:00 PM", + "4:20 PM", + "4:35 PM", + "4:40 PM", + "5:00 PM", + "5:15 PM", + "5:20 PM", + "5:40 PM", + "5:55 PM", + "6:00 PM", + "6:20 PM", + "6:40 PM", + "7:00 PM", + "8:00 PM", + "8:20 PM", + "8:40 PM", + "9:00 PM", + "9:20 PM", + "9:40 PM", + "10:00 PM", + "10:20 PM", + "10:40 PM", + "11:00 PM", + "11:20 PM", + "11:40 PM", + "12:00 AM" + ], + "NORTH": [ + "7:00 AM", + "7:10 AM", + "7:20 AM", + "7:30 AM", + "7:40 AM", + "7:50 AM", + "8:00 AM", + "8:10 AM", + "8:20 AM", + "8:30 AM", + "8:40 AM", + "8:50 AM", + "9:00 AM", + "9:10 AM", + "9:20 AM", + "9:30 AM", + "9:40 AM", + "9:50 AM", + "10:00 AM", + "10:05 AM", + "10:10 AM", + "10:20 AM", + "10:25 AM", + "10:30 AM", + "10:40 AM", + "10:50 AM", + "11:00 AM", + "11:10 AM", + "11:20 AM", + "11:30 AM", + "11:40 AM", + "11:50 AM", + "12:00 PM", + "12:10 PM", + "12:20 PM", + "12:30 PM", + "12:40 PM", + "12:50 PM", + "1:00 PM", + "1:10 PM", + "1:20 PM", + "1:30 PM", + "1:40 PM", + "1:50 PM", + "2:00 PM", + "2:10 PM", + "2:20 PM", + "2:30 PM", + "2:40 PM", + "2:50 PM", + "2:55 PM", + "3:00 PM", + "3:10 PM", + "3:15 PM", + "3:20 PM", + "3:30 PM", + "3:40 PM", + "3:50 PM", + "4:00 PM", + "4:15 PM", + "4:20 PM", + "4:40 PM", + "4:55 PM", + "5:00 PM", + "5:20 PM", + "5:35 PM", + "5:40 PM", + "6:00 PM", + "6:15 PM", + "6:20 PM", + "6:40 PM", + "7:00 PM", + "8:00 PM", + "8:20 PM", + "8:40 PM", + "9:00 PM", + "9:20 PM", + "9:40 PM", + "10:00 PM", + "10:20 PM", + "10:40 PM", + "11:00 PM", + "11:20 PM", + "11:40 PM", + "12:00 AM" + ] + }, + { + "WEST": [ + "7:00 AM", + "7:10 AM", + "7:20 AM", + "7:30 AM", + "7:40 AM", + "7:50 AM", + "8:00 AM", + "8:10 AM", + "8:20 AM", + "8:30 AM", + "8:40 AM", + "8:50 AM", + "9:00 AM", + "9:10 AM", + "9:20 AM", + "9:25 AM", + "9:30 AM", + "9:40 AM", + "9:45 AM", + "9:50 AM", + "10:00 AM", + "10:10 AM", + "10:20 AM", + "10:30 AM", + "10:40 AM", + "10:45 AM", + "10:50 AM", + "11:00 AM", + "11:10 AM", + "11:20 AM", + "11:30 AM", + "11:40 AM", + "11:50 AM", + "12:00 PM", + "12:10 PM", + "12:20 PM", + "12:30 PM", + "12:40 PM", + "12:50 PM", + "1:00 PM", + "1:10 PM", + "1:20 PM", + "1:30 PM", + "1:40 PM", + "1:50 PM", + "2:00 PM", + "2:10 PM", + "2:15 PM", + "2:20 PM", + "2:30 PM", + "2:35 PM", + "2:40 PM", + "2:50 PM", + "3:00 PM", + "3:10 PM", + "3:20 PM", + "3:30 PM", + "3:35 PM", + "3:40 PM", + "3:50 PM", + "3:55 PM", + "4:00 PM", + "4:20 PM", + "4:35 PM", + "4:40 PM", + "5:00 PM", + "5:15 PM", + "5:20 PM", + "5:40 PM", + "5:55 PM", + "6:00 PM", + "6:20 PM", + "6:40 PM", + "7:00 PM", + "8:00 PM", + "8:20 PM", + "8:40 PM", + "9:00 PM", + "9:20 PM", + "9:40 PM", + "10:00 PM", + "10:20 PM", + "10:40 PM", + "11:00 PM", + "11:20 PM", + "11:40 PM", + "12:00 AM" + ], + "NORTH": [ + "7:00 AM", + "7:10 AM", + "7:20 AM", + "7:30 AM", + "7:40 AM", + "7:50 AM", + "8:00 AM", + "8:10 AM", + "8:20 AM", + "8:30 AM", + "8:40 AM", + "8:50 AM", + "9:00 AM", + "9:10 AM", + "9:20 AM", + "9:30 AM", + "9:40 AM", + "9:50 AM", + "10:00 AM", + "10:05 AM", + "10:10 AM", + "10:20 AM", + "10:25 AM", + "10:30 AM", + "10:40 AM", + "10:50 AM", + "11:00 AM", + "11:10 AM", + "11:20 AM", + "11:30 AM", + "11:40 AM", + "11:50 AM", + "12:00 PM", + "12:10 PM", + "12:20 PM", + "12:30 PM", + "12:40 PM", + "12:50 PM", + "1:00 PM", + "1:10 PM", + "1:20 PM", + "1:30 PM", + "1:40 PM", + "1:50 PM", + "2:00 PM", + "2:10 PM", + "2:20 PM", + "2:30 PM", + "2:40 PM", + "2:50 PM", + "2:55 PM", + "3:00 PM", + "3:10 PM", + "3:15 PM", + "3:20 PM", + "3:30 PM", + "3:40 PM", + "3:50 PM", + "4:00 PM", + "4:15 PM", + "4:20 PM", + "4:40 PM", + "4:55 PM", + "5:00 PM", + "5:20 PM", + "5:35 PM", + "5:40 PM", + "6:00 PM", + "6:15 PM", + "6:20 PM", + "6:40 PM", + "7:00 PM", + "8:00 PM", + "8:20 PM", + "8:40 PM", + "9:00 PM", + "9:20 PM", + "9:40 PM", + "10:00 PM", + "10:20 PM", + "10:40 PM", + "11:00 PM", + "11:20 PM", + "11:40 PM", + "12:00 AM" + ] + }, + { + "NORTH": [ + "9:00 AM", + "9:20 AM", + "9:40 AM", + "10:00 AM", + "10:20 AM", + "10:40 AM", + "11:00 AM", + "11:20 AM", + "11:40 AM", + "12:00 PM", + "12:20 PM", + "12:40 PM", + "1:30 PM", + "1:50 PM", + "2:10 PM", + "2:30 PM", + "2:50 PM", + "3:10 PM", + "3:30 PM", + "3:50 PM", + "4:10 PM", + "4:30 PM", + "4:50 PM", + "5:00 PM", + "5:20 PM", + "5:40 PM", + "6:00 PM", + "6:20 PM", + "6:40 PM", + "7:00 PM", + "7:20 PM", + "7:40 PM", + "8:30 PM", + "8:50 PM", + "9:10 PM", + "9:30 PM", + "9:50 PM", + "10:10 PM", + "10:30 PM", + "10:50 PM", + "11:10 PM", + "11:30 PM", + "11:45 PM", + "12:00 AM" + ], + "WEST": [ + "9:00 AM", + "9:20 AM", + "9:40 AM", + "10:00 AM", + "10:20 AM", + "10:40 AM", + "11:00 AM", + "11:20 AM", + "11:40 AM", + "12:00 PM", + "12:20 PM", + "12:40 PM", + "1:30 PM", + "1:50 PM", + "2:10 PM", + "2:30 PM", + "2:50 PM", + "3:10 PM", + "3:30 PM", + "3:50 PM", + "4:10 PM", + "4:30 PM", + "4:50 PM", + "5:00 PM", + "5:20 PM", + "5:40 PM", + "6:00 PM", + "6:20 PM", + "6:40 PM", + "7:00 PM", + "7:20 PM", + "7:40 PM", + "8:40 PM", + "9:00 PM", + "9:10 PM", + "9:30 PM", + "9:50 PM", + "10:10 PM", + "10:30 PM", + "10:50 PM", + "11:10 PM", + "11:30 PM", + "11:45 PM", + "12:00 AM" + ] + } +] \ No newline at end of file diff --git a/server/routes.py b/server/routes.py index 026c8af3d..ec9dcc7fc 100644 --- a/server/routes.py +++ b/server/routes.py @@ -251,32 +251,58 @@ def data_today(): "loops": [], "breaks": [] } - # for location in locations_today: - # vehicle_location = { - # "latitude": location.latitude, - # "longitude": location.longitude, - # "timestamp": location.timestamp, - # "speed_mph": location.speed_mph, - # "heading_degrees": location.heading_degrees, - # "address_id": location.address_id - # } - # if location.vehicle_id in locations_today_dict: - # locations_today_dict[location.vehicle_id]["data"].append(vehicle_location) - # else: - # locations_today_dict[location.vehicle_id] = { - # "entry": None, - # "exit": None, - # "data": [vehicle_location] - # } - - # loops and breaks - for e, geofence_event in enumerate(events_today): - if geofence_event.event_type == "geofenceEntry": - if "entry" not in locations_today_dict[geofence_event.vehicle_id]: # first entry - locations_today_dict[geofence_event.vehicle_id]["entry"] = geofence_event.event_time - elif geofence_event.event_type == "geofenceExit": - if "entry" in locations_today_dict[geofence_event.vehicle_id]: # makes sure that the vehicle already entered - locations_today_dict[geofence_event.vehicle_id]["exit"] = geofence_event.event_time + + # shuttles start in break state + shuttle_state = {vehicle_id: "break" for vehicle_id in locations_today_dict.keys()} + + # geofence events to track loops and breaks + for geoEvent in events_today: + vehicle_id = geoEvent.vehicle_id + + # entering and leaving union conditionals + if geoEvent.event_type == "geofenceEntry" and geoEvent.address_name == "STUDENT_UNION": + # if entering union and ending loop, start break + if shuttle_state[vehicle_id] == "loop": + shuttle_state[vehicle_id] = "break" + # set end time for previous loop + if locations_today_dict[vehicle_id]["loops"]: + locations_today_dict[vehicle_id]["loops"][-1]["end"] = geoEvent.event_time + # start new break + locations_today_dict[vehicle_id]["breaks"].append({ + "start": geoEvent.event_time, + "end": None, + "locations": [] + }) + elif geoEvent.event_type == "geofenceExit" and geoEvent.address_name == "STUDENT_UNION": + # if exiting union and ending break, start loop + if shuttle_state[vehicle_id] == "break": + shuttle_state[vehicle_id] = "loop" + # set end time for previous break + if locations_today_dict[vehicle_id]["breaks"]: + locations_today_dict[vehicle_id]["breaks"][-1]["end"] = geoEvent.event_time + # start new loop + locations_today_dict[vehicle_id]["loops"].append({ + "start": geoEvent.event_time, + "end": None, + "locations": [] + }) + + # add current location to updatedloop or break + locations_today_dict[vehicle_id][shuttle_state[vehicle_id]]["locations"].append(geoEvent.event_time) + if shuttle_state[vehicle_id] == "break": continue + # un-routed and stopped conditionals + coords = (geoEvent.latitude, geoEvent.longitude) + closest_polyline = Stops.get_closest_point(coords) + if (closest_polyline[0]**2 + closest_polyline[1]**2)**0.5 > 0.0002: + # unrouted + if shuttle_state[vehicle_id] == "loop": + shuttle_state[vehicle_id] = "break" + elif shuttle_state[vehicle_id] == "break": + shuttle_state[vehicle_id] = "loop" + else: + # stopped + if shuttle_state[vehicle_id] == "loop": + return jsonify(locations_today_dict) From 498adaf66926bd3f99bcd219fa9d024f28d2cb15 Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 7 Nov 2025 11:38:10 -0500 Subject: [PATCH 04/19] is_at_stop() fixed for vectorization issue, previously a 1D input into harvensine_vectorized() which wants a 2D parameter --- data/stops.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/data/stops.py b/data/stops.py index dafd25620..2ad863e11 100644 --- a/data/stops.py +++ b/data/stops.py @@ -30,7 +30,7 @@ def get_closest_point(cls, origin_point): """ Find the closest point on any polyline to the given origin point. :param origin_point: A tuple or list with (latitude, longitude) coordinates. - :return: A tuple with the closest point (latitude, longitude), distance to that point, + :return: A tuple with the distance to the closest point, closest point (latitude, longitude), route name, and polyline index. """ point = np.array(origin_point) @@ -90,12 +90,11 @@ def is_at_stop(cls, origin_point, threshold=0.020): the stop name if close enough, otherwise None). """ for route_name, route in cls.routes_data.items(): - for stop in route.get('STOPS', []): - stop_point = np.array(route[stop]['COORDINATES']) - - distance = haversine(tuple(origin_point), tuple(stop_point)) - if distance < threshold: - return route_name, stop + for stop_name in route.get('STOPS', []): + stop_point = route[stop_name]['COORDINATES'] + # use scalar haversine to avoid vectorized shape issues + if haversine(origin_point, stop_point) < threshold: + return route_name, stop_name return None, None def haversine(coord1, coord2): @@ -144,9 +143,8 @@ def haversine_vectorized(coords1, coords2): distances : ndarray, shape (N,) Great-circle distances in kilometers. """ - # Accept either single (lat,lon) pairs or arrays of pairs. Normalize to 2-D arrays. - coords1 = np.atleast_2d(np.asarray(coords1, dtype=float)) - coords2 = np.atleast_2d(np.asarray(coords2, dtype=float)) + coords1 = np.asarray(coords1, dtype=float) + coords2 = np.asarray(coords2, dtype=float) # Earth radius in kilometers R = 6371.0 From 1340fdb4dd208a001dc2066a9c5949d94a87c941 Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 7 Nov 2025 16:31:12 -0500 Subject: [PATCH 05/19] normalized return types for get_closest_point() --- data/stops.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/data/stops.py b/data/stops.py index 2ad863e11..1362c2c03 100644 --- a/data/stops.py +++ b/data/stops.py @@ -77,7 +77,11 @@ def get_closest_point(cls, origin_point): if len(closest_routes) > 1 and haversine(closest_routes[0][1], closest_routes[1][1]) < 0.020: # If not significantly closer, return None to indicate ambiguity return None, None, None, None - return closest_routes[0] + + # normalizing return types (float vs Numpy float64 resolution) + _best_distance, _best_point, _best_route, _best_polyline_index = closest_routes[0] + _coord = (float(_best_point[0]), float(_best_point[1])) + return float(_best_distance), _coord, _best_route, int(_best_polyline_index) return None, None, None, None @classmethod @@ -92,7 +96,6 @@ def is_at_stop(cls, origin_point, threshold=0.020): for route_name, route in cls.routes_data.items(): for stop_name in route.get('STOPS', []): stop_point = route[stop_name]['COORDINATES'] - # use scalar haversine to avoid vectorized shape issues if haversine(origin_point, stop_point) < threshold: return route_name, stop_name return None, None From 98dce5233e02165e761c802ab37026858a479d47 Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 7 Nov 2025 18:39:10 -0500 Subject: [PATCH 06/19] changes made, needs backend testing --- server/routes.py | 156 ++++++++++++++++++++++++++--------------------- 1 file changed, 86 insertions(+), 70 deletions(-) diff --git a/server/routes.py b/server/routes.py index ec9dcc7fc..4fdac3945 100644 --- a/server/routes.py +++ b/server/routes.py @@ -1,5 +1,6 @@ from flask import Blueprint, request, jsonify, send_from_directory, current_app -from . import db, cache +from numpy import datetime_as_string +from . import db from .models import Vehicle, GeofenceEvent, VehicleLocation from pathlib import Path from sqlalchemy import func, and_ @@ -9,6 +10,7 @@ from hashlib import sha256 import hmac import logging + from .time_utils import get_campus_start_of_day logger = logging.getLogger(__name__) @@ -26,7 +28,6 @@ def serve_react(): return send_from_directory(root_dir, 'index.html') @bp.route('/api/locations', methods=['GET']) -@cache.cached(timeout=300, key_prefix="vehicle_locations") def get_locations(): """ Returns the latest location for each vehicle currently inside the geofence. @@ -200,10 +201,6 @@ def webhook(): ) db.session.commit() - - # Invalidate Cache - cache.delete('vehicles_in_geofence') - return jsonify({'status': 'success'}), 200 except Exception as e: @@ -231,79 +228,98 @@ def data_today(): ).order_by(GeofenceEvent.event_time.asc()).all() locations_today_dict = {} - # locations + shuttle_state = {} # helper, {vehicle_id -> string state} + shuttle_prev = {} # helper, {vehicle_id -> [datetime time_of_day, prev_latitude, prev_longitude]} + def second_difference(_veh_id): + """ + Helper function, converting the string JSON format to integer seconds + :param timestamp: (string) in the format "HH:MM:SS" + :return: (integer) in seconds of the day (out of 24 hours) + """ + return 0; for location in locations_today: - # _coordinate = (location.latitude, location.longitude), use _corrdinate[0] and [1] for later - # _closest_route_location = Stops.get_closest_point(location.latitude, location.longitude) - vechicle_location = { + # RELATED DATA: + # tuple with the distance to closest point, closest point (latitude, longitude), route name, and polyline index + _closest_point = Stops.get_closest_point((location.latitude, location.longitude)) + # tuple with (route name, stop name) if close enough, else None. + _at_stop = Stops.is_at_stop((location.latitude, location.longitude)) + # datetime to string + _timestamp = location.timestamp.strftime("%H:%M:%S") + + # LOCATIONS: + # setup dict nesting + vehicle_location = { "latitude": location.latitude, "longitude": location.longitude, - "closest_route_location": (0, 0), # update this, maybe Stops.get_closest_point(_current_coords) - "distance": 0, # update this, maybe Stops.haversine((location.latitude, location.longitude), _closest_route_location) - "closest_route": "some", # update this... - "closest_polyline": 0 # update this... + "closest_route_location": _closest_point[1], + "distance": _closest_point[0], + "closest_route": _closest_point[2], + "closest_polyline": _closest_point[3], } - if location.vehicle_id in locations_today_dict: - locations_today_dict[location.vehicle_id]["locations"].append(vechicle_location) - else: + # initialization: adding a new vehicle to the dict + if location.vehicle_id not in locations_today_dict: locations_today_dict[location.vehicle_id] = { - "locations": {location.timestamp: vechicle_location}, + "locations": {_timestamp: vehicle_location}, "loops": [], "breaks": [] } - - # shuttles start in break state - shuttle_state = {vehicle_id: "break" for vehicle_id in locations_today_dict.keys()} - - # geofence events to track loops and breaks - for geoEvent in events_today: - vehicle_id = geoEvent.vehicle_id - - # entering and leaving union conditionals - if geoEvent.event_type == "geofenceEntry" and geoEvent.address_name == "STUDENT_UNION": - # if entering union and ending loop, start break - if shuttle_state[vehicle_id] == "loop": - shuttle_state[vehicle_id] = "break" - # set end time for previous loop - if locations_today_dict[vehicle_id]["loops"]: - locations_today_dict[vehicle_id]["loops"][-1]["end"] = geoEvent.event_time - # start new break - locations_today_dict[vehicle_id]["breaks"].append({ - "start": geoEvent.event_time, - "end": None, - "locations": [] - }) - elif geoEvent.event_type == "geofenceExit" and geoEvent.address_name == "STUDENT_UNION": - # if exiting union and ending break, start loop - if shuttle_state[vehicle_id] == "break": - shuttle_state[vehicle_id] = "loop" - # set end time for previous break - if locations_today_dict[vehicle_id]["breaks"]: - locations_today_dict[vehicle_id]["breaks"][-1]["end"] = geoEvent.event_time - # start new loop - locations_today_dict[vehicle_id]["loops"].append({ - "start": geoEvent.event_time, - "end": None, - "locations": [] - }) - - # add current location to updatedloop or break - locations_today_dict[vehicle_id][shuttle_state[vehicle_id]]["locations"].append(geoEvent.event_time) - if shuttle_state[vehicle_id] == "break": continue - # un-routed and stopped conditionals - coords = (geoEvent.latitude, geoEvent.longitude) - closest_polyline = Stops.get_closest_point(coords) - if (closest_polyline[0]**2 + closest_polyline[1]**2)**0.5 > 0.0002: - # unrouted - if shuttle_state[vehicle_id] == "loop": - shuttle_state[vehicle_id] = "break" - elif shuttle_state[vehicle_id] == "break": - shuttle_state[vehicle_id] = "loop" + # start first break as "entry" + shuttle_state[location.vehicle_id] = "entry" + locations_today_dict[location.vehicle_id]["breaks"].append({ + "start": _timestamp, + "end": None, + "locations": [_timestamp] + }) + continue else: - # stopped - if shuttle_state[vehicle_id] == "loop": - - + locations_today_dict[location.vehicle_id]["locations"][_timestamp] = vehicle_location + + # LOOPS/BREAKS: + # Helper Logic, for if shuttle is stopped for > 5 minutes + _stopped_for_5_minutes = False + # initialization: new vehicle + if location.vehicle_id not in shuttle_prev: + shuttle_prev[location.vehicle_id] = [location.timestamp, location.latitude, location.longitude] + # check: shuttle not moved + else: + # to update: technically shouldn't subtract lat and lon + # threshold of 0.00008 determined from \shubble\test-server\server.py: mock_feed() + if (abs(location.latitude - shuttle_prev[location.vehicle_id][1]) < 0.00008) and (abs(location.longitude - shuttle_prev[location.vehicle_id][2]) < 0.00008): + if (location.timestamp - shuttle_prev[location.vehicle_id][0]).total_seconds() > 300: + _stopped_for_5_minutes = True + else: + shuttle_prev[location.vehicle_id] = [location.timestamp, location.latitude, location.longitude] + + # initialization: "entry" state + if (shuttle_state[location.vehicle_id] == "entry" and _at_stop[1] == "STUDENT_UNION"): + shuttle_state[location.vehicle_id] = "break" + # check: end break & start loop + # if - exiting union from a break + elif (shuttle_state[location.vehicle_id] == "break" and _at_stop[1] != "STUDENT_UNION"): + # end break + locations_today_dict[location.vehicle_id]["breaks"][-1]["end"] = _timestamp + shuttle_state[location.vehicle_id] = "loop" + # start new loop + locations_today_dict[location.vehicle_id]["loops"].append({ + "start": _timestamp, + "end": None, + "locations": [_timestamp] + }) + # check: end loop & start break + # if - entering student union from loop + # elif - distance > 0.0002 from nearest route (polyline) + # elif - shuttle been stopped for > 5 minutes + elif (shuttle_state[location.vehicle_id] == "loop" and _at_stop[1] == "STUDENT_UNION") or (_closest_point[0] is not None and _closest_point[0] > 0.0002) or (_stopped_for_5_minutes): + # end loop + locations_today_dict[location.vehicle_id]["loops"][-1]["end"] = _timestamp + shuttle_state[location.vehicle_id] = "break" + # start new break + locations_today_dict[location.vehicle_id]["breaks"].append({ + "start": _timestamp, + "end": None, + "locations": [_timestamp] + }) + return jsonify(locations_today_dict) @bp.route('/api/routes', methods=['GET']) From 55e23985e3090fd3811d59f79194ffb026ccbee6 Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 7 Nov 2025 19:17:56 -0500 Subject: [PATCH 07/19] followup: cleaning unecessary code --- server/routes.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/server/routes.py b/server/routes.py index 4fdac3945..e69156ae0 100644 --- a/server/routes.py +++ b/server/routes.py @@ -230,13 +230,6 @@ def data_today(): locations_today_dict = {} shuttle_state = {} # helper, {vehicle_id -> string state} shuttle_prev = {} # helper, {vehicle_id -> [datetime time_of_day, prev_latitude, prev_longitude]} - def second_difference(_veh_id): - """ - Helper function, converting the string JSON format to integer seconds - :param timestamp: (string) in the format "HH:MM:SS" - :return: (integer) in seconds of the day (out of 24 hours) - """ - return 0; for location in locations_today: # RELATED DATA: # tuple with the distance to closest point, closest point (latitude, longitude), route name, and polyline index From 8e541541e35f0a3ae43816a9bf20902ee3a690b0 Mon Sep 17 00:00:00 2001 From: Rick Date: Tue, 11 Nov 2025 17:33:13 -0500 Subject: [PATCH 08/19] checkpoint --- server/routes.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/server/routes.py b/server/routes.py index e69156ae0..11b669801 100644 --- a/server/routes.py +++ b/server/routes.py @@ -220,13 +220,6 @@ def data_today(): ) ).order_by(VehicleLocation.timestamp.asc()).all() - events_today = db.session.query(GeofenceEvent).filter( - and_( - GeofenceEvent.event_time >= start_of_day, - GeofenceEvent.event_time <= now - ) - ).order_by(GeofenceEvent.event_time.asc()).all() - locations_today_dict = {} shuttle_state = {} # helper, {vehicle_id -> string state} shuttle_prev = {} # helper, {vehicle_id -> [datetime time_of_day, prev_latitude, prev_longitude]} @@ -277,7 +270,7 @@ def data_today(): else: # to update: technically shouldn't subtract lat and lon # threshold of 0.00008 determined from \shubble\test-server\server.py: mock_feed() - if (abs(location.latitude - shuttle_prev[location.vehicle_id][1]) < 0.00008) and (abs(location.longitude - shuttle_prev[location.vehicle_id][2]) < 0.00008): + if (abs(location.latitude - shuttle_prev[location.vehicle_id][1]) < 0.002) and (abs(location.longitude - shuttle_prev[location.vehicle_id][2]) < 0.002): if (location.timestamp - shuttle_prev[location.vehicle_id][0]).total_seconds() > 300: _stopped_for_5_minutes = True else: @@ -302,7 +295,7 @@ def data_today(): # if - entering student union from loop # elif - distance > 0.0002 from nearest route (polyline) # elif - shuttle been stopped for > 5 minutes - elif (shuttle_state[location.vehicle_id] == "loop" and _at_stop[1] == "STUDENT_UNION") or (_closest_point[0] is not None and _closest_point[0] > 0.0002) or (_stopped_for_5_minutes): + elif (shuttle_state[location.vehicle_id] == "loop" and _at_stop[1] == "STUDENT_UNION") or (_closest_point[0] is not None and _closest_point[0] > 0.2) or (_stopped_for_5_minutes): # end loop locations_today_dict[location.vehicle_id]["loops"][-1]["end"] = _timestamp shuttle_state[location.vehicle_id] = "break" From 352185de50383074a451c0e686c4f8ae6b64f290 Mon Sep 17 00:00:00 2001 From: Rick Date: Wed, 3 Dec 2025 11:04:46 -0500 Subject: [PATCH 09/19] updated logic, mostly threshold testing next --- data/stops.py | 4 ++-- server/routes.py | 54 +++++++++++++++++++++++------------------------- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/data/stops.py b/data/stops.py index 1362c2c03..d170fffbb 100644 --- a/data/stops.py +++ b/data/stops.py @@ -26,7 +26,7 @@ class Stops: polylines[route_name].append(np.array(polyline)) @classmethod - def get_closest_point(cls, origin_point): + def get_closest_point(cls, origin_point, threshold=0.020): """ Find the closest point on any polyline to the given origin point. :param origin_point: A tuple or list with (latitude, longitude) coordinates. @@ -74,7 +74,7 @@ def get_closest_point(cls, origin_point): if closest_data: closest_routes = sorted(closest_data, key=lambda x: x[0]) # Check if closest route is significantly closer than others - if len(closest_routes) > 1 and haversine(closest_routes[0][1], closest_routes[1][1]) < 0.020: + if len(closest_routes) > 1 and haversine(closest_routes[0][1], closest_routes[1][1]) < threshold: # If not significantly closer, return None to indicate ambiguity return None, None, None, None diff --git a/server/routes.py b/server/routes.py index 11b669801..379ac282c 100644 --- a/server/routes.py +++ b/server/routes.py @@ -221,14 +221,16 @@ def data_today(): ).order_by(VehicleLocation.timestamp.asc()).all() locations_today_dict = {} - shuttle_state = {} # helper, {vehicle_id -> string state} - shuttle_prev = {} # helper, {vehicle_id -> [datetime time_of_day, prev_latitude, prev_longitude]} + threshold_noise = 0.004 # locational noise, threshold of 0.00008 determined from \shubble\test-server\server.py: mock_feed() + threshold_atStop = 0.05 # at a stop, originally 0.02 + shuttle_state = {} # helper, {"vehicle_id" -> "state"} + shuttle_prev = {} # helper, {"vehicle_id" -> [datetime.time_of_day, float.prev_latitude, float.prev_longitude]} for location in locations_today: # RELATED DATA: # tuple with the distance to closest point, closest point (latitude, longitude), route name, and polyline index _closest_point = Stops.get_closest_point((location.latitude, location.longitude)) # tuple with (route name, stop name) if close enough, else None. - _at_stop = Stops.is_at_stop((location.latitude, location.longitude)) + _at_stop = Stops.is_at_stop((location.latitude, location.longitude), threshold_atStop) # datetime to string _timestamp = location.timestamp.strftime("%H:%M:%S") @@ -241,6 +243,7 @@ def data_today(): "distance": _closest_point[0], "closest_route": _closest_point[2], "closest_polyline": _closest_point[3], + "at_stop": _at_stop[1] } # initialization: adding a new vehicle to the dict if location.vehicle_id not in locations_today_dict: @@ -252,8 +255,6 @@ def data_today(): # start first break as "entry" shuttle_state[location.vehicle_id] = "entry" locations_today_dict[location.vehicle_id]["breaks"].append({ - "start": _timestamp, - "end": None, "locations": [_timestamp] }) continue @@ -261,50 +262,47 @@ def data_today(): locations_today_dict[location.vehicle_id]["locations"][_timestamp] = vehicle_location # LOOPS/BREAKS: - # Helper Logic, for if shuttle is stopped for > 5 minutes - _stopped_for_5_minutes = False - # initialization: new vehicle + is_at_union = (_at_stop[1] == "STUDENT_UNION") and (_closest_point[2] != "WEST" or _closest_point[2] != "NORTH") + is_off_route = (_closest_point[0] is not None) and (_closest_point[0] > 0.2) + is_stopped = False + # determining is_stopped if location.vehicle_id not in shuttle_prev: shuttle_prev[location.vehicle_id] = [location.timestamp, location.latitude, location.longitude] - # check: shuttle not moved else: # to update: technically shouldn't subtract lat and lon - # threshold of 0.00008 determined from \shubble\test-server\server.py: mock_feed() - if (abs(location.latitude - shuttle_prev[location.vehicle_id][1]) < 0.002) and (abs(location.longitude - shuttle_prev[location.vehicle_id][2]) < 0.002): + if (abs(location.latitude - shuttle_prev[location.vehicle_id][1]) < threshold_noise) and (abs(location.longitude - shuttle_prev[location.vehicle_id][2]) < threshold_noise): if (location.timestamp - shuttle_prev[location.vehicle_id][0]).total_seconds() > 300: - _stopped_for_5_minutes = True + is_stopped = True else: shuttle_prev[location.vehicle_id] = [location.timestamp, location.latitude, location.longitude] - + # initialization: "entry" state - if (shuttle_state[location.vehicle_id] == "entry" and _at_stop[1] == "STUDENT_UNION"): + if (shuttle_state[location.vehicle_id] == "entry") and is_at_union: shuttle_state[location.vehicle_id] = "break" # check: end break & start loop - # if - exiting union from a break - elif (shuttle_state[location.vehicle_id] == "break" and _at_stop[1] != "STUDENT_UNION"): + elif (shuttle_state[location.vehicle_id] == "break") and not (is_at_union or is_off_route or is_stopped): # end break - locations_today_dict[location.vehicle_id]["breaks"][-1]["end"] = _timestamp shuttle_state[location.vehicle_id] = "loop" # start new loop locations_today_dict[location.vehicle_id]["loops"].append({ - "start": _timestamp, - "end": None, - "locations": [_timestamp] + "locations": [] }) # check: end loop & start break - # if - entering student union from loop - # elif - distance > 0.0002 from nearest route (polyline) - # elif - shuttle been stopped for > 5 minutes - elif (shuttle_state[location.vehicle_id] == "loop" and _at_stop[1] == "STUDENT_UNION") or (_closest_point[0] is not None and _closest_point[0] > 0.2) or (_stopped_for_5_minutes): + elif (shuttle_state[location.vehicle_id] == "loop" and is_at_union): # end loop - locations_today_dict[location.vehicle_id]["loops"][-1]["end"] = _timestamp shuttle_state[location.vehicle_id] = "break" # start new break locations_today_dict[location.vehicle_id]["breaks"].append({ - "start": _timestamp, - "end": None, - "locations": [_timestamp] + "locations": [] }) + + + # update break/loop locations + if(shuttle_state[location.vehicle_id] == "break" or shuttle_state[location.vehicle_id] == "entry"): + locations_today_dict[location.vehicle_id]["breaks"][-1]["locations"].append(_timestamp) + elif(shuttle_state[location.vehicle_id] == "loop"): + locations_today_dict[location.vehicle_id]["loops"][-1]["locations"].append(_timestamp) + return jsonify(locations_today_dict) From 798aa2feaec0efbec8253180bcd8b348f82a6b31 Mon Sep 17 00:00:00 2001 From: Rick Date: Tue, 9 Dec 2025 17:33:53 -0500 Subject: [PATCH 10/19] Added documentation, DATA_TODAY.md --- DATA_TODAY.md | 58 ++++++++++++++++++++++++++++++++++++++++++++++++ server/routes.py | 1 + 2 files changed, 59 insertions(+) create mode 100644 DATA_TODAY.md diff --git a/DATA_TODAY.md b/DATA_TODAY.md new file mode 100644 index 000000000..5fca5a7dc --- /dev/null +++ b/DATA_TODAY.md @@ -0,0 +1,58 @@ +# data_today() + +### Reference + +Regarding the `data_today()` function in path `C:\...\shubble\server\routes.py`. + +### Outputted JSON Format: + +```bash +{ + "shuttleID1": { + "locations": { + "16:25:53": { + "at_stop": null, + "closest_polyline": 1, + "closest_route": "NORTH", + "closest_route_location": [42.7379143365656, -73.6675641148277], + "distance": 0.0100496703554305, + "latitude": 42.7380042100651, + "longitude": -73.6675511200586 + }, + "16:26:01": { ... }, + "16:26:08": { ... } + // ... + }, + "breaks": [ + { + "locations": [ + "16:25:53", // break start time + "16:26:01", + "16:26:08", + "16:26:15", + "16:26:22" // break end time + ] + }, + { + "locations": [ ... ] + } + ], + "loops": [ + { + "locations": [ + "16:28:08", // loop start time + "16:28:16", + "16:28:23", + "16:28:30", + "16:28:37" // loop end time + ] + }, + { + "locations": [ ... ] + } + ] + }, + "shuttleID2": { ... }, + "shuttleID3": { ... } + // ... +``` \ No newline at end of file diff --git a/server/routes.py b/server/routes.py index 379ac282c..f39aa6c0d 100644 --- a/server/routes.py +++ b/server/routes.py @@ -209,6 +209,7 @@ def webhook(): logger.exception(f'Error processing webhook data: {e}') return jsonify({'status': 'error', 'message': str(e)}), 500 +# see DATA_TODAY.md for more information @bp.route('/api/today', methods=['GET']) def data_today(): now = datetime.now(timezone.utc) From 35ec369f09d2abe80322c3eec7bf68517d055f43 Mon Sep 17 00:00:00 2001 From: Rick Date: Tue, 9 Dec 2025 20:27:09 -0500 Subject: [PATCH 11/19] updated future work in DATA_TODAY.md --- DATA_TODAY.md | 58 --------------------------------------------------- 1 file changed, 58 deletions(-) delete mode 100644 DATA_TODAY.md diff --git a/DATA_TODAY.md b/DATA_TODAY.md deleted file mode 100644 index 5fca5a7dc..000000000 --- a/DATA_TODAY.md +++ /dev/null @@ -1,58 +0,0 @@ -# data_today() - -### Reference - -Regarding the `data_today()` function in path `C:\...\shubble\server\routes.py`. - -### Outputted JSON Format: - -```bash -{ - "shuttleID1": { - "locations": { - "16:25:53": { - "at_stop": null, - "closest_polyline": 1, - "closest_route": "NORTH", - "closest_route_location": [42.7379143365656, -73.6675641148277], - "distance": 0.0100496703554305, - "latitude": 42.7380042100651, - "longitude": -73.6675511200586 - }, - "16:26:01": { ... }, - "16:26:08": { ... } - // ... - }, - "breaks": [ - { - "locations": [ - "16:25:53", // break start time - "16:26:01", - "16:26:08", - "16:26:15", - "16:26:22" // break end time - ] - }, - { - "locations": [ ... ] - } - ], - "loops": [ - { - "locations": [ - "16:28:08", // loop start time - "16:28:16", - "16:28:23", - "16:28:30", - "16:28:37" // loop end time - ] - }, - { - "locations": [ ... ] - } - ] - }, - "shuttleID2": { ... }, - "shuttleID3": { ... } - // ... -``` \ No newline at end of file From 8292904e0580f7793d758a974cc1a569e2bee27f Mon Sep 17 00:00:00 2001 From: Rick Date: Tue, 9 Dec 2025 20:29:58 -0500 Subject: [PATCH 12/19] edited for clarity --- CONTRIBUTING.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5882f7adb..7feaadc58 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -71,19 +71,19 @@ https://www.docker.com/products/docker-desktop/ Go to this link to run Redis on docker: https://hub.docker.com/_/redis -Press on "run in docker desktop" +Press on "run in docker desktop". -This should open up your docker desktop application and run it. On this display you should see a localhost port that redis is running on. (e.g https://localhost:32678) +This should open up your docker desktop application and run it. On this display you should see a localhost port that redis is running on (e.g https://localhost:32796): image -Copy this localhost with the port and put the following in your env +Copy this localhost with the port and put the following in your `.env`: ``` -REDIS_URL=redis://localhost:{port} +REDIS_URL=redis://localhost:{"port"} ``` -replacing port with your actual port +Replace the "port" with your actual port (e.g 32796) and save changes. Now, the docker should work after you press run. # Running the frontend @@ -137,6 +137,7 @@ This will start the worker process that handles background tasks, such as updati To test the backend, Shubble provides another Flask app that mimics the Samsara API. The test app enables users to trigger shuttle entry, exit, and location updates without needing to set up a real Samsara account or API keys. This is useful for development and testing purposes. **Note**: even if you're not developing the backend, you may still want to run the test to populate Shubble with data. Like Shubble, the test app is built using Flask and React. Therefore, you must build the frontend before running the test app. + To build the frontend for the test app, `cd` to the `/test-client` directory and run: ```bash From 8b2658f02d06b0477ec024b258c79453ce8e6b0c Mon Sep 17 00:00:00 2001 From: Rick Date: Tue, 9 Dec 2025 21:26:58 -0500 Subject: [PATCH 13/19] code simplification, and added documentation --- DATA_TODAY.md | 67 ++++++++++++++++++++++++++++++++++++++++++++ server/DATA_TODAY.md | 67 ++++++++++++++++++++++++++++++++++++++++++++ server/routes.py | 27 +++++++++--------- 3 files changed, 147 insertions(+), 14 deletions(-) create mode 100644 DATA_TODAY.md create mode 100644 server/DATA_TODAY.md diff --git a/DATA_TODAY.md b/DATA_TODAY.md new file mode 100644 index 000000000..5a6d0763b --- /dev/null +++ b/DATA_TODAY.md @@ -0,0 +1,67 @@ +# data_today() + +### Reference + +Regarding the `data_today()` function in path `C:\...\shubble\server\routes.py`. + +### Outputted JSON Format: + +```bash +{ + "shuttleID1": { + "locations": { + "16:25:53": { + "at_stop": null, + "closest_polyline": 1, + "closest_route": "NORTH", + "closest_route_location": [42.7379143365656, -73.6675641148277], + "distance": 0.0100496703554305, + "latitude": 42.7380042100651, + "longitude": -73.6675511200586 + }, + "16:26:01": { ... }, + "16:26:08": { ... } + // ... + }, + "breaks": [ + { + "locations": [ + "16:25:53", // break start time + "16:26:01", + "16:26:08", + "16:26:15", + "16:26:22" // break end time + ] + }, + { + "locations": [ ... ] + } + ], + "loops": [ + { + "locations": [ + "16:28:08", // loop start time + "16:28:16", + "16:28:23", + "16:28:30", + "16:28:37" // loop end time + ] + }, + { + "locations": [ ... ] + } + ] + }, + "shuttleID2": { ... }, + "shuttleID3": { ... } + // ... +``` + +### Future Work + +Regarding +* _Priority:_ To fully close Issue #59, Domain Staging needs to be completed. + +Regarding the smaller tweaks and changes: +* Tweak the thresholds, listed at the top of the `data_today()` function, via testing (on the test-server and staging domain). Keep in-mind that some of these thresholds may also be used across different files, meaning it may be better to update it everywhere and/or implement a universally shared value. +* Determining whether or not the shuttle is stopped (boolean `is_stopped`) is calculated by subtracting latitude and longitude. This improper method of subtraction is done in other files, so a comprehensive fix across everything would be ideal. \ No newline at end of file diff --git a/server/DATA_TODAY.md b/server/DATA_TODAY.md new file mode 100644 index 000000000..5a6d0763b --- /dev/null +++ b/server/DATA_TODAY.md @@ -0,0 +1,67 @@ +# data_today() + +### Reference + +Regarding the `data_today()` function in path `C:\...\shubble\server\routes.py`. + +### Outputted JSON Format: + +```bash +{ + "shuttleID1": { + "locations": { + "16:25:53": { + "at_stop": null, + "closest_polyline": 1, + "closest_route": "NORTH", + "closest_route_location": [42.7379143365656, -73.6675641148277], + "distance": 0.0100496703554305, + "latitude": 42.7380042100651, + "longitude": -73.6675511200586 + }, + "16:26:01": { ... }, + "16:26:08": { ... } + // ... + }, + "breaks": [ + { + "locations": [ + "16:25:53", // break start time + "16:26:01", + "16:26:08", + "16:26:15", + "16:26:22" // break end time + ] + }, + { + "locations": [ ... ] + } + ], + "loops": [ + { + "locations": [ + "16:28:08", // loop start time + "16:28:16", + "16:28:23", + "16:28:30", + "16:28:37" // loop end time + ] + }, + { + "locations": [ ... ] + } + ] + }, + "shuttleID2": { ... }, + "shuttleID3": { ... } + // ... +``` + +### Future Work + +Regarding +* _Priority:_ To fully close Issue #59, Domain Staging needs to be completed. + +Regarding the smaller tweaks and changes: +* Tweak the thresholds, listed at the top of the `data_today()` function, via testing (on the test-server and staging domain). Keep in-mind that some of these thresholds may also be used across different files, meaning it may be better to update it everywhere and/or implement a universally shared value. +* Determining whether or not the shuttle is stopped (boolean `is_stopped`) is calculated by subtracting latitude and longitude. This improper method of subtraction is done in other files, so a comprehensive fix across everything would be ideal. \ No newline at end of file diff --git a/server/routes.py b/server/routes.py index f39aa6c0d..fa1288c05 100644 --- a/server/routes.py +++ b/server/routes.py @@ -221,17 +221,19 @@ def data_today(): ) ).order_by(VehicleLocation.timestamp.asc()).all() - locations_today_dict = {} - threshold_noise = 0.004 # locational noise, threshold of 0.00008 determined from \shubble\test-server\server.py: mock_feed() + locations_today_dict = {} # returned by the function in JSON format + threshold_noise = 0.004 # locational noise, threshold of 0.00008 determined from \shubble\test-server\server.py: mock_feed() threshold_atStop = 0.05 # at a stop, originally 0.02 - shuttle_state = {} # helper, {"vehicle_id" -> "state"} - shuttle_prev = {} # helper, {"vehicle_id" -> [datetime.time_of_day, float.prev_latitude, float.prev_longitude]} + shuttle_state = {} # helper, {"vehicle_id" -> "state"} + shuttle_prev = {} # helper, {"vehicle_id" -> [datetime.time_of_day, float.prev_latitude, float.prev_longitude]} for location in locations_today: # RELATED DATA: # tuple with the distance to closest point, closest point (latitude, longitude), route name, and polyline index _closest_point = Stops.get_closest_point((location.latitude, location.longitude)) # tuple with (route name, stop name) if close enough, else None. - _at_stop = Stops.is_at_stop((location.latitude, location.longitude), threshold_atStop) + _at_stop = Stops.is_at_stop((location.latitude, location.longitude), threshold_atStop)[1] + if _at_stop is None: + _at_stop = "NONE" # datetime to string _timestamp = location.timestamp.strftime("%H:%M:%S") @@ -244,29 +246,27 @@ def data_today(): "distance": _closest_point[0], "closest_route": _closest_point[2], "closest_polyline": _closest_point[3], - "at_stop": _at_stop[1] + "at_stop": "" } # initialization: adding a new vehicle to the dict if location.vehicle_id not in locations_today_dict: locations_today_dict[location.vehicle_id] = { "locations": {_timestamp: vehicle_location}, "loops": [], - "breaks": [] + "breaks": [{"locations": [_timestamp]}] } - # start first break as "entry" + # update helpers, start the first break as "entry" shuttle_state[location.vehicle_id] = "entry" - locations_today_dict[location.vehicle_id]["breaks"].append({ - "locations": [_timestamp] - }) + shuttle_prev[location.vehicle_id] = [location.timestamp, location.latitude, location.longitude] continue else: locations_today_dict[location.vehicle_id]["locations"][_timestamp] = vehicle_location # LOOPS/BREAKS: - is_at_union = (_at_stop[1] == "STUDENT_UNION") and (_closest_point[2] != "WEST" or _closest_point[2] != "NORTH") + is_at_union = (_at_stop[1] == "STUDENT_UNION") and (_closest_point[2] != "WEST" and _closest_point[2] != "NORTH") is_off_route = (_closest_point[0] is not None) and (_closest_point[0] > 0.2) is_stopped = False - # determining is_stopped + # determining bool.is_stopped if location.vehicle_id not in shuttle_prev: shuttle_prev[location.vehicle_id] = [location.timestamp, location.latitude, location.longitude] else: @@ -297,7 +297,6 @@ def data_today(): "locations": [] }) - # update break/loop locations if(shuttle_state[location.vehicle_id] == "break" or shuttle_state[location.vehicle_id] == "entry"): locations_today_dict[location.vehicle_id]["breaks"][-1]["locations"].append(_timestamp) From 349b97b56524e9abee2bdc966d77ff9e23ab74a4 Mon Sep 17 00:00:00 2001 From: Rick Date: Tue, 9 Dec 2025 21:53:29 -0500 Subject: [PATCH 14/19] Delete duplicate - server/DATA_TODAY.md accidental duplidate --- server/DATA_TODAY.md | 67 -------------------------------------------- 1 file changed, 67 deletions(-) delete mode 100644 server/DATA_TODAY.md diff --git a/server/DATA_TODAY.md b/server/DATA_TODAY.md deleted file mode 100644 index 5a6d0763b..000000000 --- a/server/DATA_TODAY.md +++ /dev/null @@ -1,67 +0,0 @@ -# data_today() - -### Reference - -Regarding the `data_today()` function in path `C:\...\shubble\server\routes.py`. - -### Outputted JSON Format: - -```bash -{ - "shuttleID1": { - "locations": { - "16:25:53": { - "at_stop": null, - "closest_polyline": 1, - "closest_route": "NORTH", - "closest_route_location": [42.7379143365656, -73.6675641148277], - "distance": 0.0100496703554305, - "latitude": 42.7380042100651, - "longitude": -73.6675511200586 - }, - "16:26:01": { ... }, - "16:26:08": { ... } - // ... - }, - "breaks": [ - { - "locations": [ - "16:25:53", // break start time - "16:26:01", - "16:26:08", - "16:26:15", - "16:26:22" // break end time - ] - }, - { - "locations": [ ... ] - } - ], - "loops": [ - { - "locations": [ - "16:28:08", // loop start time - "16:28:16", - "16:28:23", - "16:28:30", - "16:28:37" // loop end time - ] - }, - { - "locations": [ ... ] - } - ] - }, - "shuttleID2": { ... }, - "shuttleID3": { ... } - // ... -``` - -### Future Work - -Regarding -* _Priority:_ To fully close Issue #59, Domain Staging needs to be completed. - -Regarding the smaller tweaks and changes: -* Tweak the thresholds, listed at the top of the `data_today()` function, via testing (on the test-server and staging domain). Keep in-mind that some of these thresholds may also be used across different files, meaning it may be better to update it everywhere and/or implement a universally shared value. -* Determining whether or not the shuttle is stopped (boolean `is_stopped`) is calculated by subtracting latitude and longitude. This improper method of subtraction is done in other files, so a comprehensive fix across everything would be ideal. \ No newline at end of file From 4f75d075573218f703e44eea4c98a50f4fc4f51c Mon Sep 17 00:00:00 2001 From: Rick Date: Thu, 11 Dec 2025 23:53:13 -0500 Subject: [PATCH 15/19] Pull Request Fix: parenthesis cleanup, restore deleted line --- server/routes.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/server/routes.py b/server/routes.py index 24d2a62b7..13cbd18fc 100644 --- a/server/routes.py +++ b/server/routes.py @@ -30,6 +30,7 @@ def serve_react(): return send_from_directory(root_dir, 'index.html') @bp.route('/api/locations', methods=['GET']) +@cache.cached(timeout=300, key_prefix="vehicle_locations") def get_locations(): """ Returns the latest location for each vehicle currently inside the geofence. @@ -265,25 +266,25 @@ def data_today(): locations_today_dict[location.vehicle_id]["locations"][_timestamp] = vehicle_location # LOOPS/BREAKS: - is_at_union = (_at_stop[1] == "STUDENT_UNION") and (_closest_point[2] != "WEST" and _closest_point[2] != "NORTH") - is_off_route = (_closest_point[0] is not None) and (_closest_point[0] > 0.2) + is_at_union = _at_stop[1] == "STUDENT_UNION" and (_closest_point[2] != "WEST" and _closest_point[2] != "NORTH") + is_off_route = _closest_point[0] is not None and _closest_point[0] > 0.2 is_stopped = False # determining bool.is_stopped if location.vehicle_id not in shuttle_prev: shuttle_prev[location.vehicle_id] = [location.timestamp, location.latitude, location.longitude] else: # to update: technically shouldn't subtract lat and lon - if (abs(location.latitude - shuttle_prev[location.vehicle_id][1]) < threshold_noise) and (abs(location.longitude - shuttle_prev[location.vehicle_id][2]) < threshold_noise): + if abs(location.latitude - shuttle_prev[location.vehicle_id][1]) < threshold_noise and abs(location.longitude - shuttle_prev[location.vehicle_id][2]) < threshold_noise: if (location.timestamp - shuttle_prev[location.vehicle_id][0]).total_seconds() > 300: is_stopped = True else: shuttle_prev[location.vehicle_id] = [location.timestamp, location.latitude, location.longitude] # initialization: "entry" state - if (shuttle_state[location.vehicle_id] == "entry") and is_at_union: + if shuttle_state[location.vehicle_id] == "entry" and is_at_union: shuttle_state[location.vehicle_id] = "break" # check: end break & start loop - elif (shuttle_state[location.vehicle_id] == "break") and not (is_at_union or is_off_route or is_stopped): + elif shuttle_state[location.vehicle_id] == "break" and not (is_at_union or is_off_route or is_stopped): # end break shuttle_state[location.vehicle_id] = "loop" # start new loop @@ -291,7 +292,7 @@ def data_today(): "locations": [] }) # check: end loop & start break - elif (shuttle_state[location.vehicle_id] == "loop" and is_at_union): + elif shuttle_state[location.vehicle_id] == "loop" and is_at_union: # end loop shuttle_state[location.vehicle_id] = "break" # start new break @@ -300,9 +301,9 @@ def data_today(): }) # update break/loop locations - if(shuttle_state[location.vehicle_id] == "break" or shuttle_state[location.vehicle_id] == "entry"): + if shuttle_state[location.vehicle_id] == "break" or shuttle_state[location.vehicle_id] == "entry": locations_today_dict[location.vehicle_id]["breaks"][-1]["locations"].append(_timestamp) - elif(shuttle_state[location.vehicle_id] == "loop"): + elif shuttle_state[location.vehicle_id] == "loop": locations_today_dict[location.vehicle_id]["loops"][-1]["locations"].append(_timestamp) From b5e4bda871aad4bdb5b2784742b11c5ad2801709 Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 12 Dec 2025 02:33:44 -0500 Subject: [PATCH 16/19] corrected a coordinate approximation, restored a deleted import, moved documentation to Issue #262 --- DATA_TODAY.md | 67 ------------------------------------------------ server/routes.py | 18 ++++++------- 2 files changed, 9 insertions(+), 76 deletions(-) delete mode 100644 DATA_TODAY.md diff --git a/DATA_TODAY.md b/DATA_TODAY.md deleted file mode 100644 index 5a6d0763b..000000000 --- a/DATA_TODAY.md +++ /dev/null @@ -1,67 +0,0 @@ -# data_today() - -### Reference - -Regarding the `data_today()` function in path `C:\...\shubble\server\routes.py`. - -### Outputted JSON Format: - -```bash -{ - "shuttleID1": { - "locations": { - "16:25:53": { - "at_stop": null, - "closest_polyline": 1, - "closest_route": "NORTH", - "closest_route_location": [42.7379143365656, -73.6675641148277], - "distance": 0.0100496703554305, - "latitude": 42.7380042100651, - "longitude": -73.6675511200586 - }, - "16:26:01": { ... }, - "16:26:08": { ... } - // ... - }, - "breaks": [ - { - "locations": [ - "16:25:53", // break start time - "16:26:01", - "16:26:08", - "16:26:15", - "16:26:22" // break end time - ] - }, - { - "locations": [ ... ] - } - ], - "loops": [ - { - "locations": [ - "16:28:08", // loop start time - "16:28:16", - "16:28:23", - "16:28:30", - "16:28:37" // loop end time - ] - }, - { - "locations": [ ... ] - } - ] - }, - "shuttleID2": { ... }, - "shuttleID3": { ... } - // ... -``` - -### Future Work - -Regarding -* _Priority:_ To fully close Issue #59, Domain Staging needs to be completed. - -Regarding the smaller tweaks and changes: -* Tweak the thresholds, listed at the top of the `data_today()` function, via testing (on the test-server and staging domain). Keep in-mind that some of these thresholds may also be used across different files, meaning it may be better to update it everywhere and/or implement a universally shared value. -* Determining whether or not the shuttle is stopped (boolean `is_stopped`) is calculated by subtracting latitude and longitude. This improper method of subtraction is done in other files, so a comprehensive fix across everything would be ideal. \ No newline at end of file diff --git a/server/routes.py b/server/routes.py index 13cbd18fc..9e4b64abf 100644 --- a/server/routes.py +++ b/server/routes.py @@ -1,12 +1,12 @@ from flask import Blueprint, request, jsonify, send_from_directory, current_app from numpy import datetime_as_string -from . import db +from . import db, cache from .models import Vehicle, GeofenceEvent, VehicleLocation from pathlib import Path from sqlalchemy import func, and_ from sqlalchemy.dialects import postgresql from datetime import datetime, date, timezone -from data.stops import Stops +from data.stops import Stops, haversine from data.schedules import Schedule from hashlib import sha256 import hmac @@ -224,11 +224,11 @@ def data_today(): ) ).order_by(VehicleLocation.timestamp.asc()).all() - locations_today_dict = {} # returned by the function in JSON format - threshold_noise = 0.004 # locational noise, threshold of 0.00008 determined from \shubble\test-server\server.py: mock_feed() - threshold_atStop = 0.05 # at a stop, originally 0.02 - shuttle_state = {} # helper, {"vehicle_id" -> "state"} - shuttle_prev = {} # helper, {"vehicle_id" -> [datetime.time_of_day, float.prev_latitude, float.prev_longitude]} + locations_today_dict = {} # returned by the function in JSON format + threshold_noise_km = 0.01 # locational noise, threshold in kilometers (~10 meters), determined from \shubble\test-server\server.py: mock_feed() + threshold_atStop = 0.05 # at a stop, originally 0.02 + shuttle_state = {} # helper, {"vehicle_id" -> "state"} + shuttle_prev = {} # helper, {"vehicle_id" -> [datetime.time_of_day, float.prev_latitude, float.prev_longitude]} for location in locations_today: # RELATED DATA: # tuple with the distance to closest point, closest point (latitude, longitude), route name, and polyline index @@ -273,8 +273,8 @@ def data_today(): if location.vehicle_id not in shuttle_prev: shuttle_prev[location.vehicle_id] = [location.timestamp, location.latitude, location.longitude] else: - # to update: technically shouldn't subtract lat and lon - if abs(location.latitude - shuttle_prev[location.vehicle_id][1]) < threshold_noise and abs(location.longitude - shuttle_prev[location.vehicle_id][2]) < threshold_noise: + distance_km = haversine((location.latitude, location.longitude),(shuttle_prev[location.vehicle_id][1], shuttle_prev[location.vehicle_id][2])) + if distance_km < threshold_noise_km: if (location.timestamp - shuttle_prev[location.vehicle_id][0]).total_seconds() > 300: is_stopped = True else: From b0676b8304faf2338adca458f1271f33a05e2e13 Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 12 Dec 2025 15:06:59 -0500 Subject: [PATCH 17/19] fixed `_at_stop` variable --- server/routes.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/server/routes.py b/server/routes.py index 9e4b64abf..eaed409e5 100644 --- a/server/routes.py +++ b/server/routes.py @@ -225,18 +225,16 @@ def data_today(): ).order_by(VehicleLocation.timestamp.asc()).all() locations_today_dict = {} # returned by the function in JSON format - threshold_noise_km = 0.01 # locational noise, threshold in kilometers (~10 meters), determined from \shubble\test-server\server.py: mock_feed() - threshold_atStop = 0.05 # at a stop, originally 0.02 - shuttle_state = {} # helper, {"vehicle_id" -> "state"} - shuttle_prev = {} # helper, {"vehicle_id" -> [datetime.time_of_day, float.prev_latitude, float.prev_longitude]} + threshold_noise_km = 0.01 # threshold/constant: locational noise in kilometers, determined from \shubble\test-server\server.py: mock_feed() + threshold_atStop = 0.05 # threshold/constant: at a stop in kilometers, originally = 0.02 + shuttle_state = {} # helper: {"vehicle_id" -> "state"} + shuttle_prev = {} # helper: {"vehicle_id" -> [datetime.time_of_day, float.prev_latitude, float.prev_longitude]} for location in locations_today: # RELATED DATA: # tuple with the distance to closest point, closest point (latitude, longitude), route name, and polyline index _closest_point = Stops.get_closest_point((location.latitude, location.longitude)) # tuple with (route name, stop name) if close enough, else None. _at_stop = Stops.is_at_stop((location.latitude, location.longitude), threshold_atStop)[1] - if _at_stop is None: - _at_stop = "NONE" # datetime to string _timestamp = location.timestamp.strftime("%H:%M:%S") @@ -249,7 +247,8 @@ def data_today(): "distance": _closest_point[0], "closest_route": _closest_point[2], "closest_polyline": _closest_point[3], - "at_stop": "" + "at_stop": _at_stop, + "status": "" } # initialization: adding a new vehicle to the dict if location.vehicle_id not in locations_today_dict: @@ -266,7 +265,7 @@ def data_today(): locations_today_dict[location.vehicle_id]["locations"][_timestamp] = vehicle_location # LOOPS/BREAKS: - is_at_union = _at_stop[1] == "STUDENT_UNION" and (_closest_point[2] != "WEST" and _closest_point[2] != "NORTH") + is_at_union = _at_stop == "STUDENT_UNION" and (_closest_point[2] != "WEST" and _closest_point[2] != "NORTH") is_off_route = _closest_point[0] is not None and _closest_point[0] > 0.2 is_stopped = False # determining bool.is_stopped @@ -303,9 +302,10 @@ def data_today(): # update break/loop locations if shuttle_state[location.vehicle_id] == "break" or shuttle_state[location.vehicle_id] == "entry": locations_today_dict[location.vehicle_id]["breaks"][-1]["locations"].append(_timestamp) + locations_today_dict[location.vehicle_id]["locations"][_timestamp]["status"] = "break" elif shuttle_state[location.vehicle_id] == "loop": locations_today_dict[location.vehicle_id]["loops"][-1]["locations"].append(_timestamp) - + locations_today_dict[location.vehicle_id]["locations"][_timestamp]["status"] = "loop" return jsonify(locations_today_dict) From 09bb5b4fb81bf652588060d11eea68cba8dca4e5 Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 12 Dec 2025 17:29:11 -0500 Subject: [PATCH 18/19] added `state`, changes from boolean logic to states, threshold adju --- server/routes.py | 59 +++++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/server/routes.py b/server/routes.py index eaed409e5..5d5a60fe5 100644 --- a/server/routes.py +++ b/server/routes.py @@ -225,9 +225,9 @@ def data_today(): ).order_by(VehicleLocation.timestamp.asc()).all() locations_today_dict = {} # returned by the function in JSON format - threshold_noise_km = 0.01 # threshold/constant: locational noise in kilometers, determined from \shubble\test-server\server.py: mock_feed() - threshold_atStop = 0.05 # threshold/constant: at a stop in kilometers, originally = 0.02 - shuttle_state = {} # helper: {"vehicle_id" -> "state"} + threshold_noise_km = 0.035 # constant-threshold: locational noise in kilometers, determined from \shubble\test-server\server.py: mock_feed() + threshold_atStop = 0.05 # constant-threshold: at a stop in kilometers, originally = 0.02 + seconds_stopped = 300 # constant: seconds stopped at a stop shuttle_prev = {} # helper: {"vehicle_id" -> [datetime.time_of_day, float.prev_latitude, float.prev_longitude]} for location in locations_today: # RELATED DATA: @@ -241,13 +241,13 @@ def data_today(): # LOCATIONS: # setup dict nesting vehicle_location = { - "latitude": location.latitude, - "longitude": location.longitude, + "at_stop": _at_stop, + "closest_polyline": _closest_point[3], + "closest_route": _closest_point[2], "closest_route_location": _closest_point[1], "distance": _closest_point[0], - "closest_route": _closest_point[2], - "closest_polyline": _closest_point[3], - "at_stop": _at_stop, + "latitude": location.latitude, + "longitude": location.longitude, "status": "" } # initialization: adding a new vehicle to the dict @@ -258,52 +258,55 @@ def data_today(): "breaks": [{"locations": [_timestamp]}] } # update helpers, start the first break as "entry" - shuttle_state[location.vehicle_id] = "entry" + locations_today_dict[location.vehicle_id]["locations"][_timestamp]["status"] = "entry" shuttle_prev[location.vehicle_id] = [location.timestamp, location.latitude, location.longitude] - continue else: locations_today_dict[location.vehicle_id]["locations"][_timestamp] = vehicle_location # LOOPS/BREAKS: is_at_union = _at_stop == "STUDENT_UNION" and (_closest_point[2] != "WEST" and _closest_point[2] != "NORTH") - is_off_route = _closest_point[0] is not None and _closest_point[0] > 0.2 - is_stopped = False - # determining bool.is_stopped + # status = off route: assuming that the shuttle is leaving campus + if _closest_point[0] is not None and _closest_point[0] > 0.2: + locations_today_dict[location.vehicle_id]["locations"][_timestamp]["status"] = "off_route" + # status = idle: if the shuttle is stopped for too long if location.vehicle_id not in shuttle_prev: shuttle_prev[location.vehicle_id] = [location.timestamp, location.latitude, location.longitude] else: distance_km = haversine((location.latitude, location.longitude),(shuttle_prev[location.vehicle_id][1], shuttle_prev[location.vehicle_id][2])) if distance_km < threshold_noise_km: - if (location.timestamp - shuttle_prev[location.vehicle_id][0]).total_seconds() > 300: - is_stopped = True + if (location.timestamp - shuttle_prev[location.vehicle_id][0]).total_seconds() > seconds_stopped: + locations_today_dict[location.vehicle_id]["locations"][_timestamp]["status"] = "idle" else: shuttle_prev[location.vehicle_id] = [location.timestamp, location.latitude, location.longitude] - # initialization: "entry" state - if shuttle_state[location.vehicle_id] == "entry" and is_at_union: - shuttle_state[location.vehicle_id] = "break" - # check: end break & start loop - elif shuttle_state[location.vehicle_id] == "break" and not (is_at_union or is_off_route or is_stopped): - # end break - shuttle_state[location.vehicle_id] = "loop" - # start new loop + # placholder, for readability + _status = locations_today_dict[location.vehicle_id]["locations"][_timestamp]["status"] + # check: shuttle has entered the union for the first time + # start break + if (_status == "entry" or _status == "idle" or _status == "off_route") and is_at_union: + locations_today_dict[location.vehicle_id]["locations"][_timestamp]["status"] = _status = "break" + # check: shuttle is starting to loop WEST or NORTH + # end break & start loop + elif (_status == "break" and not is_at_union) and not (_status == "idle" or _status == "off_route"): + locations_today_dict[location.vehicle_id]["locations"][_timestamp]["status"] = _status = "loop" locations_today_dict[location.vehicle_id]["loops"].append({ "locations": [] }) - # check: end loop & start break - elif shuttle_state[location.vehicle_id] == "loop" and is_at_union: + # check: shuttle is back at the union after looping + # end loop & start break + elif (_status == "loop" and is_at_union): # end loop - shuttle_state[location.vehicle_id] = "break" + locations_today_dict[location.vehicle_id]["locations"][_timestamp]["status"] = _status = "break" # start new break locations_today_dict[location.vehicle_id]["breaks"].append({ "locations": [] }) # update break/loop locations - if shuttle_state[location.vehicle_id] == "break" or shuttle_state[location.vehicle_id] == "entry": + if _status == "break" or _status == "entry" or _status == "idle" or _status == "off_route": locations_today_dict[location.vehicle_id]["breaks"][-1]["locations"].append(_timestamp) locations_today_dict[location.vehicle_id]["locations"][_timestamp]["status"] = "break" - elif shuttle_state[location.vehicle_id] == "loop": + elif _status == "loop": locations_today_dict[location.vehicle_id]["loops"][-1]["locations"].append(_timestamp) locations_today_dict[location.vehicle_id]["locations"][_timestamp]["status"] = "loop" From 02851bcdc963db33f339b82082239ab7c08ccc74 Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 12 Dec 2025 17:51:33 -0500 Subject: [PATCH 19/19] fixed shuttle states, updates in locations nest properly --- server/routes.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/server/routes.py b/server/routes.py index 5d5a60fe5..cfd3b3857 100644 --- a/server/routes.py +++ b/server/routes.py @@ -229,6 +229,7 @@ def data_today(): threshold_atStop = 0.05 # constant-threshold: at a stop in kilometers, originally = 0.02 seconds_stopped = 300 # constant: seconds stopped at a stop shuttle_prev = {} # helper: {"vehicle_id" -> [datetime.time_of_day, float.prev_latitude, float.prev_longitude]} + shuttle_state = {} # helper: {"vehicle_id" -> "entry" | "break" | "loop"} for location in locations_today: # RELATED DATA: # tuple with the distance to closest point, closest point (latitude, longitude), route name, and polyline index @@ -257,13 +258,17 @@ def data_today(): "loops": [], "breaks": [{"locations": [_timestamp]}] } - # update helpers, start the first break as "entry" + # update helpers, start the first state as "entry" (treated as break in output) locations_today_dict[location.vehicle_id]["locations"][_timestamp]["status"] = "entry" shuttle_prev[location.vehicle_id] = [location.timestamp, location.latitude, location.longitude] + shuttle_state[location.vehicle_id] = "entry" else: locations_today_dict[location.vehicle_id]["locations"][_timestamp] = vehicle_location # LOOPS/BREAKS: + # Get previous per-vehicle state (default to "entry" if missing) + state = shuttle_state.get(location.vehicle_id, "entry") + is_at_union = _at_stop == "STUDENT_UNION" and (_closest_point[2] != "WEST" and _closest_point[2] != "NORTH") # status = off route: assuming that the shuttle is leaving campus if _closest_point[0] is not None and _closest_point[0] > 0.2: @@ -275,38 +280,46 @@ def data_today(): distance_km = haversine((location.latitude, location.longitude),(shuttle_prev[location.vehicle_id][1], shuttle_prev[location.vehicle_id][2])) if distance_km < threshold_noise_km: if (location.timestamp - shuttle_prev[location.vehicle_id][0]).total_seconds() > seconds_stopped: + # shuttle has been effectively stopped for a long time; treat as empty/on entry locations_today_dict[location.vehicle_id]["locations"][_timestamp]["status"] = "idle" + state = "entry" else: shuttle_prev[location.vehicle_id] = [location.timestamp, location.latitude, location.longitude] - # placholder, for readability + # placeholder for this timestamp's raw status tag _status = locations_today_dict[location.vehicle_id]["locations"][_timestamp]["status"] + is_idle = _status == "idle" + is_off_route = _status == "off_route" + # check: shuttle has entered the union for the first time # start break - if (_status == "entry" or _status == "idle" or _status == "off_route") and is_at_union: - locations_today_dict[location.vehicle_id]["locations"][_timestamp]["status"] = _status = "break" + if (state == "entry" or is_idle or is_off_route) and is_at_union: + state = "break" # check: shuttle is starting to loop WEST or NORTH # end break & start loop - elif (_status == "break" and not is_at_union) and not (_status == "idle" or _status == "off_route"): - locations_today_dict[location.vehicle_id]["locations"][_timestamp]["status"] = _status = "loop" + elif state == "break" and not is_at_union and not (is_idle or is_off_route): + state = "loop" locations_today_dict[location.vehicle_id]["loops"].append({ "locations": [] }) # check: shuttle is back at the union after looping # end loop & start break - elif (_status == "loop" and is_at_union): + elif state == "loop" and is_at_union: # end loop - locations_today_dict[location.vehicle_id]["locations"][_timestamp]["status"] = _status = "break" + state = "break" # start new break locations_today_dict[location.vehicle_id]["breaks"].append({ "locations": [] }) - + + # persist updated state + shuttle_state[location.vehicle_id] = state + # update break/loop locations - if _status == "break" or _status == "entry" or _status == "idle" or _status == "off_route": + if state == "break" or state == "entry": locations_today_dict[location.vehicle_id]["breaks"][-1]["locations"].append(_timestamp) locations_today_dict[location.vehicle_id]["locations"][_timestamp]["status"] = "break" - elif _status == "loop": + elif state == "loop": locations_today_dict[location.vehicle_id]["loops"][-1]["locations"].append(_timestamp) locations_today_dict[location.vehicle_id]["locations"][_timestamp]["status"] = "loop"