diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ed0ae60ea..99f3ccacf 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 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/data/stops.py b/data/stops.py index dafd25620..d170fffbb 100644 --- a/data/stops.py +++ b/data/stops.py @@ -26,11 +26,11 @@ 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. - :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) @@ -74,10 +74,14 @@ 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 - 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 @@ -90,12 +94,10 @@ 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'] + if haversine(origin_point, stop_point) < threshold: + return route_name, stop_name return None, None def haversine(coord1, coord2): @@ -144,9 +146,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 diff --git a/server/routes.py b/server/routes.py index 8fb844c1c..cfd3b3857 100644 --- a/server/routes.py +++ b/server/routes.py @@ -1,15 +1,17 @@ from flask import Blueprint, request, jsonify, send_from_directory, current_app +from numpy import datetime_as_string 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 import logging + from .time_utils import get_campus_start_of_day logger = logging.getLogger(__name__) @@ -202,10 +204,6 @@ def webhook(): ) db.session.commit() - - # Invalidate Cache - cache.delete('vehicles_in_geofence') - return jsonify({'status': 'success'}), 200 except Exception as e: @@ -214,6 +212,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) @@ -225,39 +224,105 @@ 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 = {} + locations_today_dict = {} # returned by the function in JSON format + 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]} + 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 + _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] + # datetime to string + _timestamp = location.timestamp.strftime("%H:%M:%S") + + # LOCATIONS: + # setup dict nesting vehicle_location = { + "at_stop": _at_stop, + "closest_polyline": _closest_point[3], + "closest_route": _closest_point[2], + "closest_route_location": _closest_point[1], + "distance": _closest_point[0], "latitude": location.latitude, "longitude": location.longitude, - "timestamp": location.timestamp, - "speed_mph": location.speed_mph, - "heading_degrees": location.heading_degrees, - "address_id": location.address_id + "status": "" } - if location.vehicle_id in locations_today_dict: - locations_today_dict[location.vehicle_id]["data"].append(vehicle_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] = { - "entry": None, - "exit": None, - "data": [vehicle_location] + "locations": {_timestamp: vehicle_location}, + "loops": [], + "breaks": [{"locations": [_timestamp]}] } - 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 - + # 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: + 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() > 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] + + # 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 (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 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 state == "loop" and is_at_union: + # end loop + 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 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 state == "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) @bp.route('/api/routes', methods=['GET'])