Compare commits

..

No commits in common. "main" and "feedback" have entirely different histories.

1944 changed files with 25 additions and 485999 deletions

1
.gitignore vendored
View file

@ -1 +0,0 @@
.vscode/settings.json

14
.vscode/cspell.json vendored
View file

@ -1,14 +0,0 @@
{
"version": "0.2",
"ignorePaths": [
"src/client/data/**"
],
"dictionaryDefinitions": [],
"dictionaries": [],
"words": [
"dotw",
"Occt"
],
"ignoreWords": [],
"import": []
}

23
.vscode/launch.json vendored
View file

@ -1,23 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "client (windows)",
"cwd": "src\\client",
"request": "launch",
"type": "dart",
"args": ["-d", "windows"]
},
{
"name": "client (web)",
"cwd": "src\\client",
"request": "launch",
"type": "dart",
"args": ["-d", "chrome", "--dart-define-from-file=.env"],
"program": "lib/main.dart",
},
]
}

41
LICENSE
View file

@ -1,41 +0,0 @@
Copyright (c) 2025 Spencer Powell & Levi Lesches
At time of writing this software is not licensed under a permissive license.
If you are Professor Steven Moore of Binghamton University you may access the code as it exists on May 5th 2025 under an MIT license for the purposes of grading.
A copy of the MIT license can be found at https://choosealicense.com/licenses/mit/ or at the bottom of this file.
If you are anyone else you ARE NOT granted permission to read, use, modify, copy, merge, publich, distribute, sublicense or sell the software or any other rights to this software.
Versions of this software written/published after May 5th 2025 may fall under a different license.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-----------------------------------------------------
Example of MIT License THIS IS NOT THE LICENSE THIS SOFTWARE FALLS UNDER barring relicensing.
Copyright (c) [year] [fullname]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,60 +1,44 @@
[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/vfKrPwQS) # << Project Title >>
# Binghamton Better Bus (BBB) v2
## CS 445 Final Project ## CS 445 Final Project
### Spring, 2025 ### << Semester, Year >>
### Team: Team 4 ### Team: << team name >>
<< List Team Members >>
- Spencer Powell
- Levi Lesches
## Getting Started ## Getting Started
<<One paragraph of project description goes here>>
This project is a website which allows for picking a source and destination and getting the ideal bus route between the 2 given static information.
### Roadmap ### Roadmap
<<
A list of features, function or non-functional, you would like to add in the future if you had time, i.e. Phase 2 stuff A list of features, function or non-functional, you would like to add in the future if you had time, i.e. Phase 2 stuff
- [ ] Add Changelog
- [ ] Make use of time/direction information available in static scheduling information - [ ] Add back to top links
- [ ] Place our hardware on OCCT buses to remove ETA Spot dependence/legal question mark - [ ] Add Additional Templates w/ Examples
- [ ] Design/build hardware - [ ] Add "components" document to easily copy & paste sections of the readme
- Keyword reminder for mobile data: IoT data plan and/or M2M >>
- [ ] Negotiate placing hardware on OCCT buses
- [ ] Chat with BCT about getting their live bus info
- [ ] Use machine learning to predict bus locations based on current position, current time of day and current route,
## SRS ## SRS
[document](url to google doc)
[doc](https://docs.google.com/document/d/1kSWMxsK0NakhvHRQnNx0wRlL6wDgYGO-tA7JB_4qldE/edit?usp=sharing)
### Prerequisites ### Prerequisites
* [Docker](https://www.docker.com/) * [Docker](https://www.docker.com/)
* Docker Compose * <<any additional software. Be specific about versions.>>
* [Just](https://github.com/casey/just)
### Installing ### Installing
<<
cd into the `src` directory and run `just setup` then run `docker-compose build`. A step by step series of examples that tell you how to get a development env running
Clearly outline each step and repeat until the environment is set up.
To run just run `docker-compose up` in the `src` directory, the site will be up on `localhost:8080` End with an example of getting some output from the system, such as a menu or prompt
>>
## Built With ## Built With
<< list all frameworks and modules used here >>
* [Deno](https://deno.com/) * [requests](https://docs.python-requests.org/en/latest/user/quickstart/#make-a-request) - request for humans
- [Acorn](https://oakserver.org/acorn)
* [Dart](https://dart.dev/)
* [Flutter](https://flutter.dev/)
- Google maps
* [Caddy](https://caddyserver.com/)
## License ## License
<< Add a [license](https://choosealicense.com/) >>
DIY license written out which grants MIT rights to professor moore for the version of this submitted for grading and no rights to anyone else.
## Acknowledgments ## Acknowledgments
* Hat tip to anyone whose code was used
* Claude and chatGPT were used for the creation of scripts for certain rote data conversion tasks * Inspiration
* This project would not exist with the data it has if not for prior instances of attempts to make this idea by US, of particular note is the usage of GTFS data which was only learned of in a prior attempt with Lucy Loerker * etc

BIN
demo.mp4

Binary file not shown.

View file

@ -1,130 +0,0 @@
## Client Meeting Notes
**Project Name:** [Your Project Name]
**Date:** [MM/DD/YYYY]
**Client Name:** [Clients Name]
**Interviewed By:** [Your Name(s)]
---
### **Client Overview**
- **Brief description of the client and their background:**
(Example: Kevin is a Dungeon Master who wants a tool to help manage his D&D campaigns.)
- **Clients main goal for the software:**
(Example: The tool should help track player turns, roll dice automatically, and send notifications.)
---
### **Interview Questions & Responses**
#### **1. What are the costs vs. budget?**
**Clients Response:**
(Example: "I dont want to spend much, ideally free, but Id consider paying a one-time fee.")
**Clarification & Notes:**
(Example: The client is looking for a free tool but may accept optional paid features.)
---
#### **2. What is the timeframe for this project?**
**Clients Response:**
(Example: "I need this ready before my next campaign starts in three months.")
**Clarification & Notes:**
(Example: A strict deadline, must focus on core features first.)
---
#### **3. What problem are you trying to solve?**
**Clients Response:**
(Example: "Managing initiative order and tracking actions is too slow.")
**Clarification & Notes:**
- The real problem is **speed and efficiency** during gameplay, not just "initiative tracking."
- Consider features like **automation** and **UI improvements** to streamline this.
---
#### **4. What is the most important feature?**
**Clients Response:**
(Example: "Initiative tracking needs to be easy and automatic.")
**Clarification & Notes:**
- This feature should be the **core focus** of the software.
- Other features (e.g., dice rolling) are secondary.
---
#### **5. Is there a similar product on the market?**
**Clients Response:**
(Example: "Yes, but they are either too complex or too expensive.")
**Clarification & Notes:**
- Competitor analysis needed.
- Find a way to differentiate: **Simpler UI? Free version? Customizable?**
---
#### **6. Are there any existing products or technologies that will need to be integrated?**
**Clients Response:**
(Example: "It would be nice if it worked with Roll20 or D&D Beyond.")
**Clarification & Notes:**
- Check for **APIs** for possible integrations.
- Consider a **manual import/export** feature if full integration isnt feasible.
---
#### **7. What does success look like for this project?**
**Clients Response:**
(Example: "If my players can see whose turn it is without me saying anything, thats a win.")
**Clarification & Notes:**
- Focus on **visual clarity and notifications**.
---
#### **8. What risks are associated with a malicious user?**
**Clients Response:**
(Example: "Not much, unless players try to cheat dice rolls.")
**Clarification & Notes:**
- Consider **server-side dice validation** to prevent cheating.
---
#### **9. What is the expected lifetime of this software?**
**Clients Response:**
(Example: "I want to use it for many campaigns, so it should last for years.")
**Clarification & Notes:**
- Future **maintenance and updates** should be considered.
---
#### **10. What additional features would you like?**
**Clients Response:**
(Example: "A way to send private messages to players would be cool.")
**Clarification & Notes:**
- Nice-to-have, but **not essential** for version 1.
---
## **Key Takeaways & Next Steps**
- **Most important feature:** Initiative tracking
- **Secondary features:** Dice rolling, notifications, private messaging
- **Deadline:** 3 months
- **Risks:** Potential cheating in dice rolls
### **Next Steps:**
✅ Prioritize initiative tracking
✅ Research existing products for inspiration
✅ Explore integration options with Roll20/D&D Beyond
---
### **Signatures**
(Client) **[Clients Name]** *Reviewed & Approved*
(Developer) **[Your Name]** *Re

View file

@ -1,181 +0,0 @@
## Client Meeting Notes
**Project Name:** WIP Map Project
**Date:** 02/05/2025
**Client Name:** Spencer Powell
**Interviewed By:** Spencer Powell
---
### **Client Overview**
- **Brief description of the client and their background:**
The client is an undergraduate student at Binghamton University who regularly uses the bus system as a method of transit.
- **Clients main goal for the software:**
The tool should assist in transiting around Binghamton using the 2 bus systems via providing arrival timing information and ideal routes.
---
### **Interview Questions & Responses**
#### **1. What are the costs vs. budget?**
**Clients Response:**
I'm a college student, so ideally free.
> Comparing to whatever subscriptions you currently pay for how much would you pay for this?
A dollar maybe a few bucks.
**Clarification & Notes:**
Client is looking for something free and will be relatively price sensitive if they're paying.
---
#### **2. What is the timeframe for this project?**
**Clients Response:**
I am graduating in 3 months or so, so if you deliver after that it'll be useless to me.
> Do you have any friends that aren't graduating that may be interested?
Most of my friends are also graduating in free months but a couple of them aren't I guess.
**Clarification & Notes:**
While this particular client provides a strict deadline there are other clients in similar circumstances who have a looser deadline.
As such if we want to meet their needs we'll need to work quickly on core features but there's reason to work on extra features if we go over the deadline.
---
#### **3. What problem are you trying to solve?**
**Clients Response:**
I would like to turn my brain off when I want to go somewhere outside walking distance.
> What do you need to think about when going somewhere?
How far I need to walk to take a bus
When the bus arrives/departs
Often I need to take multiple buses.
> Could you elaborate on how multiple buses complicates things?
Well lets say I want to go to/from Walmart from my home for example.
I have two main routes, one that goes to BU then takes a 47/91 or OCCT bus to walmart and one which goes to the downtown bus station and then takes the 57 to Walmart.
Vice versa to go back home from Walmart.
I need to think about which choice will get me to/from Walmart with the least amount of time spent on buses/waiting and I don't always get it right.
Combine that with the other things I mentioned and it's a pain.
**Clarification & Notes:**
- forgot line of thought due to interview being interrupted
---
#### **4. What is the most important feature?**
**Clients Response:**
Getting a reliable preferably optimal route of buses to get from point to point
**Clarification & Notes:**
- Focus on getting routes first and be reasonably certain they'll work
---
#### **5. Is there a similar product on the market?**
**Clients Response:**
Google maps can be used to get routes which only use broome county buses but doesn't take OCCT buses into account.
Furthermore it isn't clear if google maps can/would take into account live info for it's routes.
There's also etaSpot which is only a live map of OCCT buses
**Clarification & Notes:**
- Worth investigating how google maps determines routes algorithmically
- Need to integrate OCCT buses to be useful
---
#### **6. Are there any existing products or technologies that will need to be integrated?**
**Clients Response:**
No
**Clarification & Notes:**
- The client isn't aware of Broome county providing GTFS
- Furthermore from prior question etaSpot can be used for live OCCT bus info
---
#### **7. What does success look like for this project?**
**Clients Response:**
If I can put in a place I want to go on my phone and get instructions to go there via bus that are good then I'd say it's a success.
**Clarification & Notes:**
- Again focus on reliability and speed
---
#### **8. What risks are associated with a malicious user?**
**Clients Response:**
nothing?
**Clarification & Notes:**
- There's some risk of DDOS on our endpoints and possible free riders but that isn't really a risk to other users
---
#### **9. What is the expected lifetime of this software?**
**Clients Response:**
For my remaining time in university.
**Clarification & Notes:**
- While this client will be leaving university soon there will be a continuous stream of new users so long term sustainability should be somewhat accounted for
---
#### **10. What additional features would you like?**
**Clients Response:**
Live map
**Clarification & Notes:**
- nice to have and reasonably easy to implement for OCCT, harder for broome county
---
## **Key Takeaways & Next Steps**
- **Most important feature:** Route calculation
- **Secondary features:** Live map and viewing schedules
- **Deadline:** 3 months
- **Risks:** DDOS and freeloading users
### **Next Steps:**
- [ ] Collect data from GTFS and etaSpot api
- [ ] Use data to calculate crude route from point to point
- [ ] Improve route making
- [ ] Add live map
---
### **Signatures**
(Client) **Spencer Powell** *Reviewed & Approved*
(Developer) **Spencer Powell** *Re

View file

@ -1,130 +0,0 @@
## Client Meeting Notes
**Project Name:** [Your Project Name]
**Date:** [MM/DD/YYYY]
**Client Name:** [Clients Name]
**Interviewed By:** [Your Name(s)]
---
### **Client Overview**
- **Brief description of the client and their background:**
(Example: Kevin is a Dungeon Master who wants a tool to help manage his D&D campaigns.)
- **Clients main goal for the software:**
(Example: The tool should help track player turns, roll dice automatically, and send notifications.)
---
### **Interview Questions & Responses**
#### **1. What are the costs vs. budget?**
**Clients Response:**
(Example: "I dont want to spend much, ideally free, but Id consider paying a one-time fee.")
**Clarification & Notes:**
(Example: The client is looking for a free tool but may accept optional paid features.)
---
#### **2. What is the timeframe for this project?**
**Clients Response:**
(Example: "I need this ready before my next campaign starts in three months.")
**Clarification & Notes:**
(Example: A strict deadline, must focus on core features first.)
---
#### **3. What problem are you trying to solve?**
**Clients Response:**
(Example: "Managing initiative order and tracking actions is too slow.")
**Clarification & Notes:**
- The real problem is **speed and efficiency** during gameplay, not just "initiative tracking."
- Consider features like **automation** and **UI improvements** to streamline this.
---
#### **4. What is the most important feature?**
**Clients Response:**
(Example: "Initiative tracking needs to be easy and automatic.")
**Clarification & Notes:**
- This feature should be the **core focus** of the software.
- Other features (e.g., dice rolling) are secondary.
---
#### **5. Is there a similar product on the market?**
**Clients Response:**
(Example: "Yes, but they are either too complex or too expensive.")
**Clarification & Notes:**
- Competitor analysis needed.
- Find a way to differentiate: **Simpler UI? Free version? Customizable?**
---
#### **6. Are there any existing products or technologies that will need to be integrated?**
**Clients Response:**
(Example: "It would be nice if it worked with Roll20 or D&D Beyond.")
**Clarification & Notes:**
- Check for **APIs** for possible integrations.
- Consider a **manual import/export** feature if full integration isnt feasible.
---
#### **7. What does success look like for this project?**
**Clients Response:**
(Example: "If my players can see whose turn it is without me saying anything, thats a win.")
**Clarification & Notes:**
- Focus on **visual clarity and notifications**.
---
#### **8. What risks are associated with a malicious user?**
**Clients Response:**
(Example: "Not much, unless players try to cheat dice rolls.")
**Clarification & Notes:**
- Consider **server-side dice validation** to prevent cheating.
---
#### **9. What is the expected lifetime of this software?**
**Clients Response:**
(Example: "I want to use it for many campaigns, so it should last for years.")
**Clarification & Notes:**
- Future **maintenance and updates** should be considered.
---
#### **10. What additional features would you like?**
**Clients Response:**
(Example: "A way to send private messages to players would be cool.")
**Clarification & Notes:**
- Nice-to-have, but **not essential** for version 1.
---
## **Key Takeaways & Next Steps**
- **Most important feature:** Initiative tracking
- **Secondary features:** Dice rolling, notifications, private messaging
- **Deadline:** 3 months
- **Risks:** Potential cheating in dice rolls
### **Next Steps:**
✅ Prioritize initiative tracking
✅ Research existing products for inspiration
✅ Explore integration options with Roll20/D&D Beyond
---
### **Signatures**
(Client) **[Clients Name]** *Reviewed & Approved*
(Developer) **[Your Name]** *Re

View file

@ -1,17 +0,0 @@
# Topic: Flutter
## Outline
- What Flutter is
- What Flutter is not
- Who is Dart?
- Asynchronous code
- Building app with flutters
## Questions
- Compared to other frameworks
## Review
Good Job!

View file

@ -1,16 +0,0 @@
# Topic: Deno
## Outline
- WHat is deno
- Deno imporvements over node
- typescript
- security
## Questions
- migration to deno
## Review
Good Job

View file

@ -1,57 +0,0 @@
## How will you use this application?
Open app/web app on phone and use it to plan out trip.
## Where would the user be located physically when using this application?
Somewhere in or around Binghamton.
## Where would the results be visible?
On a website.
## When will this application be used?
When transiting via bus.
## When can the application fail?
Multiple failure cases.
## Who is my user?
Binghamton student who use the bus systems to get around.
## Who will deliver the inputs for the application?
User/ OCCT eta spot api
## Who will receive the outputs of the application?
User
## What do I know about this application?
_
## What does this application need to do?
Gather data on available buses and plan a route based on that information
## What are the components of this application?
Backend to gather bus info and calculate routes
Frontend to receive route requests and display the calculated route
## What needs to happen next?
API between frontend and backend needs to be decided and tasks divided up/planned out
## What must happen before the next step?
_
## What needs to be tracked?
_

View file

@ -1,19 +0,0 @@
## **Backend**
- **NodeJS** → [node](https://hub.docker.com/_/node)
## **Frontend**
- **Flutter** → [the thing](https://flutter.dev/)
## **Web Server**
- **Caddy** → [caddy:latest](https://hub.docker.com/_/caddy)
## **Database**
- **Sqlite** → [file format lmao](https://www.sqlite.org/index.html)
## **Additional Dependencies**
-

View file

@ -1,22 +0,0 @@
|Technology | Spencer | Levi |
|---------------------|-------------|-------------|
|Flutter | None | Profiecient |
|Unity | Novice | Novice |
|Docker | Profiecient | Novice |
|Git | Profiecient | Profiecient |
|Rust | Profiecient | Competent |
|Javascript/Typescript| Profiecient | Profiecient |
|Python | Competent | Profiecient |
|Haskell | Competent | Novice |
|Node | Competent | Competent |
|Nginx | Competent | None |
|Dart | None | Profiecient |
|Caddy | Novice | None |
|Shell | Competent | Competent |
|React | Novice | Novice |
|Svelte | Novice | Novice |
|HTML | Competent | Competent |
|CSS | Profiecient | Compentent |
|Linux | Profiecient | Profiecient |
|Kubernetes | None | None |
|Firebase | None | Profiecient |

View file

@ -1,31 +0,0 @@
# [Feature Name]
## **Persona**
- **Name:** Sam
- **Relevant Characteristics:** Freshman binghamton university student who just downloaded the app
- **Behaviors & Attitudes:** Reasonably comfortable with a phone/google maps and have little to no experience/understanding of the bus systems in Binghamton
- **Goal:** Go from home to somewhere else in binghamton and back again
---
## **User Story**
As a **university student**
I want **a way to figure out good routes with buses without memorizing every bus route/stop**
so that **I can buy groceries, go to downtown and easily get to/from the university**
---
## **Acceptance Criteria**
- A route to get from point A to point B which isn't obnoxiously slow is generated
- said route is reliable
- route is generated within seconds (10 seconds max if possible)
---
## **Notes**
- route should be reasonably fast
- route should have reasonably low amounts of walking
- OCCT can sometimes drop the ball on live map info leading to suboptimal routing
- furthermore it's possible OCCT or broome county service is temporarily canceled which can cause a route to be wrong

View file

@ -1,25 +0,0 @@
# [Feature Name]
## **Persona**
- **Name:** [Fictional User Name]
- **Relevant Characteristics:** [Background, role, experience level]
- **Behaviors & Attitudes:** [How they use technology, frustrations, goals]
- **Goal:** [What they want to achieve]
---
## **User Story**
[Write the user story using one of the approved formats]
---
## **Acceptance Criteria**
- [Define the success conditions for this feature]
- [How will we know when this is done?]
- [What must be functional before release?]
---
## **Notes**
- [Any additional considerations or insights]
- [Potential risks or challenges]

View file

@ -1,34 +0,0 @@
# Client Meeting Notes
## Sprint Notes
## SRS Updated
N/A
## Requirements Attempted in this Sprint
N/A
## Requirements complete
N/A
## Requirements incomplete
N/A
## Requirement Flex Remaining
<< 5 / 5 >>
## Requirements Attempted in next Sprint
Access transit to information
# Milestone Status
Pass
## Requested Changes

View file

@ -1,71 +0,0 @@
# **Project Overview**
## **Application Vision/Goal:**
The two main public transit providers for Binghamton University students are OCCT and BC buses. Each provider has their own routes, schedules, and quirks, making the simple question “how do I get there” surprisingly difficult to answer, especially in a timely manner. This project will pull data from both services to get people to their destinations.
Users will be able to input a start and end location and the app will respond with a route using either, both or neither bus providers to get there.
## **Scope:**
Allow viewing bus routes and pick start and end point and get bus route between them.
## **Deliverables:**
Google maps which you can click on 2 points to get route between them and get a visual of the bus routes for current time.
## **Success Criteria:**
Making something which is more functional than [Spencer's HackBU 2024 project](https://git.pagwin.xyz/Pagwin/BinghamonBetterBus), ([Video demo](https://youtu.be/5WcKPhMqveY)) hackBU project doesn't handle Broome county buses due to a bug and suggests comically bad bus routes.
## **Assumptions:**
- etaSPOT data is reasonably accurate
- Broome county GTFS data is reasonably accurate
- Google maps will continue to exist and allow for drawing of path on map
## **Risks:**
- if we want/need static OCCT data that will involve manual work
- potential combinatorial explosion causing calculation to take too long
## **Design / Architectural Review:**
Client server split maybe database
## **Test Environment:**
Unit tests for the backend.
---
# **Team Setup**
## **Team Members:**
- Spencer Powell
- Levi Lesches
## **Team Roles:**
- Spencer: server
- Levi: client
## **Team Norms:**
- handle CI for their side of the app
## **Application Stack:**
- Caddy for reverse proxy
- Flutter for client
- Google Maps API for visual
- Nodejs + Typescript for backend
- sqlite for db
### **Libraries/Frameworks:**
- JSON for api
- etaSpot for dynamic OCCT info
- maybe scraping for static OCCT info
- GTFS data for static BC info

View file

@ -1,36 +0,0 @@
# Client Meeting Notes
## Sprint Notes
## SRS Updated
Requirements section - complete
## Requirements Attempted in this Sprint
Access transit to information
## Requirements complete
Access transit to information
## Requirements incomplete
N/A
## Requirement Flex Remaining
<< 5 / 5 >>
## Requirements Attempted in next Sprint
Communication between front and backend
# Milestone Status
Pass
## Requested Changes
Break requirements section of the SRS into function and non-function, and connect each requirement to one or more user story

View file

@ -1,74 +0,0 @@
# Sprint Meeting Notes
**Attended**: Spencer Powell, Levi Lesches
**DATE**: 03-03-2025
***
## Sprint 1 Review
### SRS Sections Updated
<< List any SRS sections that were updated in the last sprint >>
Functional and non functional requirements
### User Story
<< Corresponding User Stories completed in this sprint >>
No User stories
### Sprint Requirements Attempted
Convert data into desired format from data available via APIs
### Completed Requirements
TBD
### Incomplete Requirements
TBD
### The summary of the entire project
<< A general overview of the entire project >>
***
## Sprint << num >> Planning
## Requirements Flex
<< # >>/5 requirement flexes remaining
## Technical Debt
<< Any requirements from the previous sprint that are using a technical flex >>
### Requirement Target
<< The corresponding SRS requirement that our team will be complete in next sprint >>
### User Stories
<< Corresponding User Stories >>
### Planning
<< Our team's detailed plan to complete the sprint >>
### Action Items
<< A list of things that need to happen in our for our team to complete the sprint >>
### Issues and Risks
<< A list of potential obstacles that could keep us from completing the sprint and what's being done about them >>
### Team Work Assignments
<< A list of each team member and their works assignments for this sprint >>

View file

@ -1,39 +0,0 @@
# Client Meeting Notes
## Sprint Notes
## SRS Updated
Archetecture Diagram
## Requirements Attempted in this Sprint
Communication between front and backend
- CLient/Server integration
## Requirements complete\
Communication between front and backend
- CLient/Server integration
## Requirements incomplete
## Requirement Flex Remaining
<< 3 / 5 >>
- Missed sprint goal
## Requirements Attempted in next Sprint
- Convert data into desired format from data available via APIs
- Client and server should integrate without issue
# Milestone Status
Pass
## Requested Changes

View file

@ -1,77 +0,0 @@
# Sprint Meeting Notes
**Attended**: Spencer Powell
**DATE**: 3/21/2025
***
## Sprint 2 Review
### SRS Sections Updated
None
### User Story
None
### Sprint Requirements Attempted
None
### Completed Requirements
None
### Incomplete Requirements
- Timing information for OCCT is a work in progress
- work on the client hasn't started
- api endpoint that makes a route isn't mocked or working
- while the data for the trips/stops is gathered the setup for the server to provide that data to the client isn't made yet
### The summary of the entire project
Still very much in an early messy state.
***
## Sprint 3 Planning
## Requirements Flex
4/5 requirement flexes remaining
## Technical Debt
Connecting up the client and the server/putting data in desired format
### Requirement Target
- Convert data into desired format from data available via APIs
- Client and server should integrate without issue
### User Stories
Initial user story
### Planning
Levi will finish up handling getting static timing for OCCT and build the client to display some data and Spencer will setup the server to show the generated files and for the api endpoint which gives a route have it emit a static hand-written route
### Action Items
Static timing info needs to be brought into the OCCT data from the OCCT site, client needs to be built and server needs to be built
### Issues and Risks
Other school work could take too much time and prevent progress from being made.
### Team Work Assignments
- Levi
- OCCT static timing info
- Client
- Spencer
- Server

View file

@ -1,37 +0,0 @@
# Client Meeting Notes
## Sprint Notes
## SRS Updated
UI
## Requirements Attempted in this Sprint
- Convert data into desired format from data available via APIs
- Client and server should integrate without issue
## Requirements complete\
- Convert data into desired format from data available via APIs
- Client and server should integrate without issue
## Requirements incomplete
## Requirement Flex Remaining
<< 3 / 5 >>
## Requirements Attempted in next Sprint
- Merge client sevrer to one branch, and integration
- Docker Services working
# Milestone Status
Pass
## Requested Changes

View file

@ -1,64 +0,0 @@
# Sprint Meeting Notes
*note: replace anything surrounded by << >> and **remove** the << >>*
**Attended**: Spencer Powell
**DATE**: << meeting date >>
***
## Sprint 3 Review
### SRS Sections Updated
requirements
### User Story
arguably the initial story
### Sprint Requirements Attempted
- Archetecture Diagram completion
### Completed Requirements
- Client and server now share a language although the plumbing between them isn't setup yet
- server now determine's stops for a "route" although those stops aren't related to the response given by `/path` due to them not considering bus directionality
### Incomplete Requirements
Depends on how you interpret the requirements and completing them, ultimately it was determined that picking a somewhat useful route was a bigger requirement than expected however the naive algorithm which ignores bus directionality was implemented but not connected to the actual API due to being fundamentally unfit for purpose
### The summary of the entire project
Things are almost coming toegether into a state where we can show something but getting useful functionality will probably be pushed back a few weeks
***
## Sprint 4 Planning
## Requirements Flex
3/5 requirement flexes remaining
## Technical Debt
### Requirement Target
- Display routes or display routes (changed from text based simply giving back info retrieved from api to actually using google maps)
- decide on representation of graph and graph representation of the bus systems and create nodes in the representation of the graph for the graph representation of the system
### User Stories
Initial user story
### Issues and Risks
- Graph representation will probably need to be complicated
### Team Work Assignments
- Levi will setup the client to use google maps instead of just displaying text
- Spencer will figure out the representation of the graph and graph representation of the system

View file

@ -1,32 +0,0 @@
# Client Meeting Notes
## Sprint Notes
## Requirements Attempted in this Sprint
- Merge client sevrer to one branch, and integration
- Docker Services working
## Requirements complete
- Merge client sevrer to one branch, and integration
- Docker Services working
## Requirements incomplete
## Requirement Flex Remaining
<< 3 / 5 >>
## Requirements Attempted in next Sprint
- Routing is based on start and stop location
# Milestone Status
Pass
## Requested Changes

View file

@ -1,51 +0,0 @@
# Sprint Meeting Notes
*note: replace anything surrounded by << >> and **remove** the << >>*
**Attended**: Spencer Powell
**DATE**: 2025-04-02
***
## Sprint 4 Review
### SRS Sections Updated
None
### User Story
None
### Sprint Requirements Attempted
Frontend visualization and graph node creation
### Completed Requirements
Frontend visualization and graph node creation
### The summary of the entire project
Very close to being usable and giving answers.
***
## Sprint 5 Planning
## Requirements Flex
3/5 requirement flexes remaining
### Requirement Target
Client and server will be hooked up to make client use information from server and things being brought up via docker-compose
### Planning
We are going to meet out of class in person to get this done
### Issues and Risks
Unintended/undocumented desync from the api specification

View file

@ -1,25 +0,0 @@
# Client Meeting Notes
## Sprint Notes
## Requirements Attempted in this Sprint
- Routing is based on start and stop location
## Requirements complete
- Routing is based on start and stop location
## Requirements incomplete
## Requirement Flex Remaining
<< 3 / 5 >>
# Milestone Status
Pass
## Requested Changes

View file

@ -1,57 +0,0 @@
# Sprint Meeting Notes
*note: replace anything surrounded by << >> and **remove** the << >>*
**Attended**: Spencer Powell
**DATE**: 04-16-2025
***
## Sprint 5 Review
### SRS Sections Updated
### Sprint Requirements Attempted
Integrate client and server
### Completed Requirements
Integrate client and server
### The summary of the entire project
The project is nearing completion, all that needs to be done is the addition of graph edges to the graph database
***
## Sprint 6 Planning
## Requirements Flex
3/5 requirement flexes remaining
## Technical Debt
Need to fix a bug in the client that causes it to not display info fetched from server correctly
### Requirement Target
Setup edges in graph
### User Stories
Baseline user story
### Planning
We'll probably use a technical flex but otherwise the plan would be for us to both work on setups which add data into the graph database
### Action Items
Edges need to be added to graph and graph db needs to be used to find a valid graph path for a given query
### Issues and Risks
This will probably be the most obnoxious part of the project for various reasons

View file

@ -1,71 +0,0 @@
# Sprint Meeting Notes
*note: replace anything surrounded by << >> and **remove** the << >>*
**Attended**: << record the team members in attendance (virtual counts as long as they are participating) >>
**DATE**: << meeting date >>
***
## Sprint << num >> Review
### SRS Sections Updated
<< List any SRS sections that were updated in the last sprint >>
### User Story
<< Corresponding User Stories completed in this sprint >>
### Sprint Requirements Attempted
<< The corresponding SRS requirement that the team completed in the last sprint >>
### Completed Requirements
<< The work that's been completed in this sprint >>
### Incomplete Requirements
<< The work that has not been completed in this sprint. Be VERY detailed and specific regarding what isn't working and what needs to be completed >>
### The summary of the entire project
<< A general overview of the entire project >>
***
## Sprint << num >> Planning
## Requirements Flex
<< # >>/5 requirement flexes remaining
## Technical Debt
<< Any requirements from the previous sprint that are using a technical flex >>
### Requirement Target
<< The corresponding SRS requirement that our team will be complete in next sprint >>
### User Stories
<< Corresponding User Stories >>
### Planning
<< Our team's detailed plan to complete the sprint >>
### Action Items
<< A list of things that need to happen in our for our team to complete the sprint >>
### Issues and Risks
<< A list of potential obstacles that could keep us from completing the sprint and what's being done about them >>
### Team Work Assignments
<< A list of each team member and their works assignments for this sprint >>

3
src/.gitignore vendored
View file

@ -1,3 +0,0 @@
*.env
pubspec.lock
.dart_tool

View file

@ -1,13 +0,0 @@
setup:
# this is a hack that is done due to a lack of communication
# towards the end of this project
# a slightly better hack could be done which avoids this but I
# don't wanna write things to work that way
cp -r client shared/client-dir
cp -r shared client/shared-dir
cp -r server/data shared/server-data
cp pubspec.yaml client/root-pubspec.yaml
cp pubspec.yaml shared/root-pubspec.yaml
clean:
rm -rf client/root-pubspec.yaml shared/root-pubspec.yaml shared/client-dir client/shared-dir shared/server-data

View file

@ -1,52 +0,0 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints. See the following for docs:
# https://dart.dev/guides/language/analysis-options
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
include: package:very_good_analysis/analysis_options.yaml # has more lints
analyzer:
language:
# Strict casts isn't helpful with null safety. It only notifies you on `dynamic`,
# which happens all the time in JSON.
#
# See https://github.com/dart-lang/language/blob/main/resources/type-system/strict-casts.md
strict-casts: false
# Don't let any types be inferred as `dynamic`.
#
# See https://github.com/dart-lang/language/blob/main/resources/type-system/strict-inference.md
strict-inference: true
# Don't let Dart infer the wrong type on the left side of an assignment.
#
# See https://github.com/dart-lang/language/blob/main/resources/type-system/strict-raw-types.md
strict-raw-types: true
exclude:
- lib/generated/**.dart
- test/**.dart
- example/**.dart
linter:
rules:
# Rules NOT in package:very_good_analysis
prefer_double_quotes: true
prefer_expression_function_bodies: true
# Rules to be disabled from package:very_good_analysis
prefer_single_quotes: false # prefer_double_quotes
lines_longer_than_80_chars: false # lines should be at most 100 chars
sort_pub_dependencies: false # Sort dependencies by function
use_key_in_widget_constructors: false # not in Flutter apps
directives_ordering: false # sort dart, then flutter, then package imports
always_use_package_imports: false # not when importing sibling files
sort_constructors_first: false # final properties, then constructor
avoid_dynamic_calls: false # this lint takes over errors in the IDE
one_member_abstracts: false # abstract classes are good for interfaces
cascade_invocations: false # cascades are often harder to read
# Temporarily disabled until we are ready to document
public_member_api_docs: false

View file

@ -1,101 +0,0 @@
# API spec for communicating between the frontend and the backend
## List of routes
- `/trips/`
- `/trips/{id}`
- `/stops/{id}`
- `/path?start_lat={latitude}&start_lon={longitude}&end_lat={latitude}&end_lon={longitude}&time={unix_epoch}`
## `/trips`
being a json list of urls to each of all the trips over the course of the week, probably won't be used for core functionality but it'll be trivial to implement so might as well
## `/trips/{id}`
json object describing a route, I'm planning on this having an infinite cache time, if a route changes a new id will be made for the route after the change
```yaml
{
"_links":{
"self":{
"href":"https://$base_domain/trips/{id}"
},
# stops in order of when the bus reaches them
"stops": [{
"href":"https://$base_domain/stops/{id}",
"latitude":{latitude},
"longitude":{longitude},
"times":[
{"dotw": "Mo, Tu, We, Th, Fr, Sa or Su", "time": "HH:MM"},
]
},
...
]
},
"name": "name of the route",
"abbr": "shortened name of the route or null",
"polyline": "whatever the route's polyline is",
}
```
## `GET /stops`
```yaml
[
{
"name": {name},
"longitude": {longitude},
"latitude": {latitude},
# "provider": {occt|bc},
"routeID":
}
]
## `/stops/{id}`
```yaml
{
"_links":{
"self":{
"href":"https://$base_domain/stops/{id}"
},
# trips that make use of the stop in arbitrary order
"trips": [
{
"href":"https://$base_domain/trips/{id}",
}
]
}
"latitude": {latitude},
"longitude": {longitude}
}
```
`/path?start_lat={latitude}&start_lon={longitude}&end_lat={latitude}&end_lon={longitude}&time={unix_epoch}`
```yaml
{
"_links":{
"self":{
"href": "https://$base_domain/path?start_lat={latitude}&start_lon={longitude}&end_lat={latitude}&end_lon={longitude}&time={unix_epoch}"
}
"path":[
{
"trip":{
"href":"https://$base_domain/trips/{id}",
"polyline": "{polyline}"
},
"enter_bus":{
"href":"https://$base_domain/stops/{id}",
"latitude": # {latitude},
"longitude": # {longitude},
"time": "same format as above",
},
"exit_bus":{
"href":"https://$base_domain/stops/{id}",
"latitude": # {latitude},
"longitude": # {longitude},
"time": "same format as above",
},
}
]
},
}
```

View file

@ -1 +0,0 @@
GOOGLE_MAPS_API_KEY=

View file

@ -1,2 +0,0 @@
Dockerfile
.dockerignore

View file

@ -1 +0,0 @@
GOOGLE_MAPS_API_KEY=

50
src/client/.gitignore vendored
View file

@ -1,50 +0,0 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
pubspec.lock
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
.env
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
shared-dir
root-pubspec.yaml

View file

@ -1,33 +0,0 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "3e493a3e4d0a5c99fa7da51faae354e95a9a1abe"
channel: "beta"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 3e493a3e4d0a5c99fa7da51faae354e95a9a1abe
base_revision: 3e493a3e4d0a5c99fa7da51faae354e95a9a1abe
- platform: web
create_revision: 3e493a3e4d0a5c99fa7da51faae354e95a9a1abe
base_revision: 3e493a3e4d0a5c99fa7da51faae354e95a9a1abe
- platform: windows
create_revision: 3e493a3e4d0a5c99fa7da51faae354e95a9a1abe
base_revision: 3e493a3e4d0a5c99fa7da51faae354e95a9a1abe
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View file

@ -1,35 +0,0 @@
FROM alpine:3.21
RUN mkdir /client
RUN apk add bash curl file git unzip which zip gcompat wget tar xz
#ENTRYPOINT bash
WORKDIR /client
RUN wget -O flutter.tar.xz https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.29.0-stable.tar.xz
RUN tar xf flutter.tar.xz
ENV PATH="/client/flutter/bin/:${PATH}"
RUN git config --global --add safe.directory /client/flutter
RUN flutter upgrade
RUN flutter doctor
COPY root-pubspec.yaml /client/pubspec.yaml
COPY ./shared-dir /client/shared
COPY ./ /client/client
WORKDIR /client/client
RUN flutter build web --dart-define-from-file=.env
WORKDIR /client/client/build/web
EXPOSE 80/tcp
CMD ["dart", "run", "dhttpd", "--host", "0.0.0.0", "--port", "80"]

View file

@ -1,16 +0,0 @@
# client
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View file

@ -1,132 +0,0 @@
// Entrypoints can print
// ignore_for_file: avoid_print
import "dart:convert";
import "dart:io";
import "package:http/http.dart";
import "package:html/parser.dart";
typedef Json = Map<String, dynamic>;
extension <E> on List<E> {
Iterable<(int, E)> enumerate() sync* {
for (var i = 0; i < length; i++) {
yield (i, this[i]);
}
}
}
final spotClient = Client();
class OcctStop {
static final Map<int, OcctStop> stopsById = {};
static final Map<String, OcctStop> stopsByName = {};
static Future<void> readStops() async {
final file = File("data/stops.json");
final json = jsonDecode(await file.readAsString()) as List;
for (final stopJson in json.cast<Json>()) {
final name = stopJson["name"] as String;
final id = stopJson["id"] as int;
final routeId = stopJson["rid"] as int;
var stop = stopsById[id];
if (stop == null) {
stop = OcctStop(id, name);
stopsById[id] = stop;
stopsByName[name] = stop;
}
stop.routes.add(routeId);
}
}
final String name;
final int id;
final List<int> routes = [];
OcctStop(this.id, this.name);
}
class OcctRoute {
final int id;
final String name;
final String abbr;
final List<OcctRouteStop> stops = [];
final String encodedLine;
final String hexColor;
OcctRoute.fromJson(Json json) :
id = json["id"],
name = json["name"],
abbr = json["abbr"],
encodedLine = json["encLine"],
hexColor = json["color"];
}
class OcctRouteStop {
final int id;
final String name;
final List<String> times = [];
OcctRouteStop(this.id, this.name);
}
/*
{
"route_id": "int",
"stops": [
{
"stop_id": "int",
"stop_name": "string",
"times": [ "HH:MM PM" ]
}
]
}
*/
String getStopName(String text) {
final regex = RegExp("(?:Returns to|Arrives at|Leaves)? ?(?:the )?(.+)");
const stopNameSubstitutions = {
"Binghamton University": "University Union",
"Innovative Tech Complex": "Innovative Technology Center",
"UClub / University Plaza": "UCLUB",
};
print("Trying to find stop name in [$text]");
final match = regex.firstMatch(text)!;
final name = match.group(1)!;
return stopNameSubstitutions[name] ?? name;
}
void main() async {
await OcctStop.readStops();
final uri = Uri.parse("https://occtransport.org/pages/routes/iu.html");
final response = await get(uri);
if (response.statusCode != 200) {
print("Got non-200 status: ");
print(response.body);
return;
}
final html = parse(response.body);
final table = html.getElementsByTagName("tbody").first;
print(table.children.first.innerHtml);
final numStops = table.children.first.children.length;
print("There are $numStops stops");
final stopsHtml = html.getElementsByClassName("sch-notes").first;
final routeStops = <OcctRouteStop>[];
for (final stopHtml in stopsHtml.children) {
final sourceName = stopHtml.text.substring(2); // eg, 1 Leaves Mohawk
final name = getStopName(sourceName);
final id = OcctStop.stopsByName[name]?.id;
if (id == null) {
print("Could not find a stop with the name: $name");
return;
}
routeStops.add(OcctRouteStop(id, name));
}
for (final row in table.children) {
for (final (index, timeHtml) in row.children.enumerate()) {
final time = timeHtml.innerHtml;
routeStops[index].times.add(time);
}
}
print(routeStops);
}

View file

@ -1,442 +0,0 @@
[
{
"id": 1,
"name": "Westside Outbound",
"abbr": "WS O",
"stops": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
132,
13,
14,
15,
16,
17,
18,
19,
20,
21
],
"vType": "Bus",
"encLine": "wb{_GhitnMBAFh@LZ@nCO~CIfA??IpAUjBM|@AVSbA_@`B??e@|Ao@rA}@lAm@f@??o@PSCy@?aAAy@O??eAYi@W??WWkA|Be@v@??eBjBy@l@s@b@??YNuBv@gB`@??aCd@SRUb@iAO??s@?}DNBwA??D{CTcAPk@V_@??`CeDRi@Fc@?YA_@EUM]W_@OKe@M??cIS??sHU??mCOsC]??mD}@iA_@??yCkAyB_A??iH{C??mGoCUY??oAm@g@[g@k@u@iA??e@q@Q]GYCWAe@B]NWBg@CUWMMaA??i@iGI]E{@??m@wK??s@kL??u@kL??u@mL??QoCo@qG??]gD[_E??s@qJ??YkDUqA??Qw@cB{F??w@eB]a@kAmA??_CiB~@iC??rA_EdAgC??`E}I??`EcJ??`DgH^u@??fAwBdCuF??nD_I??rCmG??nBkE??nBiE??pBmE??pBkE??rBkE??xByE??nAuC`@sA??tAqG??^aBTuAXyB??dAeJ??`A_J??n@cGDwB??FwK??@aAlCf@PFTA??XGPIV[Re@F[L{A~B^????|Dv@",
"color": "#191970",
"type": "INBOUND",
"order": 1,
"showDirection": false,
"showPlatform": false,
"showScheduleNumber": 1,
"useCustomShape": 0,
"showVehicleCapacity": true
},
{
"id": 2,
"name": "Westside Inbound",
"abbr": "WS I",
"stops": [
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
52,
53,
54,
55,
56,
57,
58,
59,
60,
32,
118,
1,
61
],
"vType": "Bus",
"encLine": "qv|_G`_jnMA?iDq@_@S}B_@??y@OSFM`BSn@Y^OJ[J??]LmCg@AxA??IrMCb@]xC??UdCiBhP??a@nDg@~C_BjH??k@~Bs@fB{@jB??wE`K??uE~J??uEbK??uEdK??uEbK??w@fBgBlD{@nB??wEfK??wEhK??gB|D_C|G????i@zA??A@zBdBh@h@bAhAVf@??f@lAzAjF\\zA??RlBfAxN@?TbDnA`M??pA|S??pA~S??~@bP?r@??DxAR|C@|@OHCHCLA\\D\\PJFR??Fp@HzAD`@Lp@Zn@X`@XRl@T~Ab@??j@RRBhGhC??pInD??vFfC|Aj@??d@PfBVfBRrBN??~CLzA?`BH@?|IV??d@BTJPNLRL`@Db@?RGf@Sd@ST??QHa@FYCWOSWOc@CU?a@???]EUNcJ??J{K??BcAPo@DIRMXGjAH??dAH~ATpBd@??p@R|@f@jBxA??NTxAjEFh@??^|Cf@|Bb@`B??BXHTp@xAv@|@~@l@??r@Xh@JfAF|@A??XANERKn@Q^WRU??v@gAXg@^_A^uA??h@_CDY@WTaB??LgARyCJaCB]ZOHIBa@?{@???qAGeB????g@DC@Ij@Cd@",
"color": "#191970",
"type": "INBOUND",
"order": 2,
"showDirection": false,
"showPlatform": false,
"showScheduleNumber": 1,
"useCustomShape": 0,
"showVehicleCapacity": true
},
{
"id": 3,
"name": "Downtown Center Leroy Outbound",
"abbr": "DCL O",
"stops": [
1,
2,
3,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42
],
"vType": "Bus",
"encLine": "sb{_GlitnM??Fb@LZ@nCO~CMhB??[zCM|@AVq@|C??]pA_@~@Yf@_ApA??k@b@o@PSCy@?aAAo@K??oA]i@Y??WUkA|Bg@z@??cBfBy@l@mAr@a@P??sAd@mAXuCj@??YTUb@o@Ka@Ek@@{BH??aADHsFTcAJ[??DOT]bCgDL[FUDk@??Ai@Kc@O[OSOKSIQC{BE??gJY??eFQ{@G}AS??q@ImAYcBc@{Ag@??gCcA{DaB??{IwD??yCqASWqAo@q@e@??i@q@oAkBWk@EYA[Bq@NWXKr@Mb@OTM??ROd@e@nBsCL]h@q@??lFqG??vCsDlAeB??Zo@hCsDr@kA??vEwH??l@eARe@H[L}@ZiD??z@sL??~@eM??LmAVcBtAgG??d@mBVw@pCmG??fFmL??r@gBTo@tAkG?AlAoFfAkE??dD}K@?pBuIVkA??fAqFd@oD??^kDsA?kBL??{EX??iDRKcE??SuJ??SuJ??SwJ??SmK???cA^{I??`@eL????L{C~A`@??lJ|B??nAZc@uI??e@qJ??k@qJ??e@yH??E]@YM@u@IiFcA??GAIIi@K",
"color": "#D2042D",
"type": "INBOUND",
"order": 3,
"showDirection": false,
"showPlatform": false,
"showScheduleNumber": 1,
"useCustomShape": 0,
"showVehicleCapacity": true
},
{
"id": 4,
"name": "Downtown Center Leroy Inbound",
"abbr": "DCL I",
"stops": [
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
118,
61,
1
],
"vType": "Bus",
"encLine": "iy|_Gz~inM?A??xB`@H@JClCf@??bB\\t@HJVTpC??`AxP??RrDOFIBS?{EkA??gCo@QxA??_AtIG|@??i@pN??a@bK?z@??TxL??VhM??TnK??PfI??D`B|CQ??~DU??rCSj@???f@?c@`E]hC??Y|AsAlG??qArFk@zB??qCdJ??gAjEcApE??_BjHUn@a@bA??{D~I??_E|I??g@xAoBnI??YxAS|Am@hI??eAzN??g@|FGd@Kh@KXq@pA??uErH??}@xAqBtC??W\\a@x@_CzC??oEtF??gD`Ec@`@??yBfC]Z]R??c@RQ@UCYI??[QY?WLGV?j@BNPJDNJ`A??FnAL`AXt@`@j@??XTXJbD`A??NBjEhB??rElB??~EtB??dF|B??pBr@|B\\@?nALjCRX@??nFJ??tGT??`BBhAFJDZTLRL`@D^??Cj@Kf@U^[Pa@FYCa@YIMKY??G_@?_AEUNaJ??L{M??Ru@DIRMXG|CT??rAR|@RdBd@b@T??p@`@rAhANTf@tA??`@rAN`@V`CL|@??h@dCb@`BBXb@hA??Vd@dAhAj@^n@V`@J??xAJl@?\\A??ZGRKn@Q^Wn@u@??Zg@n@sATq@f@oB??ZaB@WL}@TkBNwB??RaETK??NMBa@?eD??IoA??@Bk@DIj@CZ",
"color": "#D2042D",
"type": "INBOUND",
"order": 4,
"showDirection": false,
"showPlatform": false,
"showScheduleNumber": 1,
"useCustomShape": 0,
"showVehicleCapacity": true
},
{
"id": 8,
"name": "Campus Shuttle",
"abbr": "CS",
"stops": [
61,
117,
62,
63,
64,
65,
110,
111,
112,
113,
114,
115,
116,
1
],
"vType": "Bus",
"encLine": "{a{_GdptnM?u@??A_A??AgA??CiA?AAe@Ee@??GkA??KgA??MaAQ{@??g@kBy@uB??aAqBm@y@??MKg@Ye@IS?A?UBe@Pg@ZGL??[f@Sj@M`A??IhB?jB??LtD??@LgAl@WNY\\??_@j@s@vA??e@v@{@x@??GFI@g@Tg@L??a@JWLWDMLEJEZ??B^LP`AtC??Vt@????V`CMj@??I`C??IdCEn@??WxB?jA??J`Av@tB??FRbBgB??f@{@t@{A?ADEx@j@n@V??^JtAJ??p@???RpHA?nAE??nCO??dCU??pAGr@M??bC]??bC]??bC]H?AhASjB??WfAYv@o@pA??s@~@cAdA??cBdB??q@r@]j@??q@CG@]zB?h@`@?@I??TqAV{@??b@w@`AcA??|B_C??|@eAj@cA??Rc@Xw@XoA?AJaAFw@AqAEg@??Gm@Mq@Qk@g@oAKShB{C??|BgE??v@eBp@mB??HW}@o@?A?@YWCAe@zAABG^BfB??Dl@DL??HN}@`B??wAhC??sAxBg@n@??}@|@{@n@??gCnA??u@\\[Jo@L??m@D_AE??a@Gk@SQOO[??e@cBl@g@j@s@??j@aATk@f@aB??j@gCDY@WNeA??RcBTeD??LiC",
"color": "#228B22",
"type": "Linear",
"order": 5,
"showDirection": false,
"showPlatform": false,
"showScheduleNumber": 0,
"useCustomShape": true,
"showVehicleCapacity": true
},
{
"id": 16,
"name": "ITC - UCLUB",
"abbr": "IU",
"stops": [
61,
63,
64,
65,
109,
66,
67,
68,
134,
69
],
"vType": "Bus",
"encLine": "ub{_GfbtnMYkBQaAe@gBe@qA??a@aAs@sAs@_Ag@[??]KOAU?UBe@Pg@Zc@t@Qd@??OdAIjB?lBJrC??Bl@gAl@c@\\m@z@e@|@??m@jAWXq@l@I@g@Te@L??c@JWLWDy@OkA]??uAe@uBi@A?{B_@wBK??OAe@_@M[Kq@J}F??P{L??FqE|CM??hBKh@Gb@Q^U??`A{@??e@qAUa@o@m@??QWEQ?a@n@yARoA??\\cCJBD_@??f@uD??BMa@D??[Be@C?B???Cc@ESBMH??ITSpA??CV@VFLLH??bAZ\\HRAGb@In@?AWfBK^??k@pA?N???PDPHNRR??b@`@Zl@??^dAu@n@??k@`@c@PYD??wBL??sAF??kADBqB??HqF??F}ANkC??PiBTaB??t@qE??v@mE??x@oE??x@mE??t@cE??t@eE??`@aCn@R??jA\\T@??`AZ??O~@??e@nC??g@xC??e@lC??QhATJ?AbC|@??lBr@??fA`@d@H??lC`@??lC\\`AX??zCjAXXBN??RbAFbAEx@o@bI??CVxE[??tDWdAOj@O??d@KzCcAn@O??h@Gv@@zBH??lBDr@AF|A??NfDmCR??[JKJUd@G\\?nADjCAt@??IjB]zBa@tAi@nA??e@x@]b@wAtAs@h@??y@f@y@\\o@R_@H??WBy@BaAQIB[T??UPPxA@H?DHpAH|@??????DdB??@bB??@rA@FXMHIBa@?}@???oAGcB??c@B??G@I}@Cw@??AW",
"color": "#CF9FFF",
"type": "INBOUND",
"order": 6,
"showDirection": false,
"showPlatform": false,
"showScheduleNumber": 1,
"useCustomShape": 0,
"showVehicleCapacity": true
},
{
"id": 5,
"name": "UCLUB",
"abbr": "UC",
"stops": [
61,
63,
64,
65,
66,
67,
68,
69,
134,
1
],
"vType": "Bus",
"encLine": "yb{_GxatnMS_BWkA??a@{Ae@oA??a@cAcAgB??c@k@GEg@Yi@I??a@@UFUJg@Z??c@t@Ut@MdAGrB???tAJlC??Br@{@b@c@XUVY`@??}AxC?AWZq@l@I@]N??}Ad@??MFWDg@Iy@S??mAc@??kBi@??cAWu@K??y@MiAG??}@E_@YIKGO??Mw@FaD??HyF??HwF??HyF??HwF??DoCJkB??NsBViC??T_Bl@gD??fAcG??bAqF??bAsF??`AsF?@b@kCn@R??jA\\N?~@X??FB_@|BC???B?m@jD??m@nD??YfBTH??lE`B??jBp@rAT??hDd@??bALXFpAd@??pBv@XXJ`@A?Lp@FbAQ`C??a@nF??Eb@~DY??rEY??`AOpA[??tCaA??~@S^Ev@@h@B??~DJr@AHlB??LvC}BNWDSHKJ??Ud@G\\?nADjCAjAItA[pB??c@~Ae@hAi@~@gAnAm@h@w@l@??qAr@mA`@g@Ji@D_@?a@G??[GI?u@f@RzAH`BH|@M`A",
"color": "#BF40BF",
"type": "INBOUND",
"order": 7,
"showDirection": false,
"showPlatform": false,
"showScheduleNumber": 1,
"useCustomShape": 0,
"showVehicleCapacity": true
},
{
"id": 12,
"name": "University Downtown Center Outbound",
"abbr": "UDC O",
"stops": [
1,
21,
42
],
"vType": "Bus",
"encLine": "sb{_GnitnMF`@LZ@nCO~CSxCUjBM|@AVIb@??y@lDUp@o@rA}@lAWVa@Tc@J??SCy@?aAAc@G{Aa@g@WYWi@m@e@o@e@aAQMg@mB??c@qBc@mDCY@e@G_BS{AD]G[??MSOIWDy@Ok@OuBs@{Aa@kAU??kAQgCMe@_@M[Kq@HcE??ZgU??NiKPaD`@_E??TgBdCeN??zCoPtAeI??xBkMn@oEX{A??dAsFh@cCd@kBzAcF??l@aBdEuK??bGsOn@yB??fAkEl@mDT}ANuA\\iE??|A{Z??d@iJ@oAC}AC_AU_De@wC??o@eCkHaT??w@{Bq@yBk@aCc@qC]qCk@{D??mDuV??e@wCWoBSo@Wa@GG_@Qa@EY@kFp@??qDb@cCVI@eAISIQO[e@Oq@??Cm@Hw@Vs@d@a@F@b@E`@D\\P^l@??FPHf@t@pL??RfEG~@ERQb@y@d@w@O@?cCc@IIs@M",
"color": "#FF7F50",
"type": "INBOUND",
"order": 8,
"showDirection": false,
"showPlatform": false,
"showScheduleNumber": 1,
"useCustomShape": 0,
"showVehicleCapacity": true
},
{
"id": 13,
"name": "University Downtown Center Inbound",
"abbr": "UDC I",
"stops": [
21,
32,
118,
1,
61
],
"vType": "Bus",
"encLine": "qx|_G~~inMD@??zAX??AyE??E}D@u@???AfAe@n@Ov@MN@~AS??xGw@??~Ca@\\BVJPNLN??N^Jl@bAlH??xA`K??hB`M@?x@bGh@tCNl@??XdA`DfJ??dEzL??l@jB^`Bb@bDRxC??B|BCvA_@hI??{@nP??c@vIa@~E??_@vCe@nCa@fBy@|C??w@dCgEzK??kHdRoAhE??q@hC{@~DcAtFcA|G??UzAsBrK_B~I??eF~Y??a@dCQvA]|DMpCSlI??Y|X??G|El@AlBLpAJl@H??nB^dBd@|@f@`At@??h@b@NTxAjEV`CRrA??b@nBb@`BFb@^`AVb@v@|@`An@??f@R^JzAJl@?h@CNERKb@KJE`@Y??hA{AXg@^_Ah@sBXoAJk@@WD[??\\mCRyCNyC?EZOHIBa@AgA??@eAG_B?Ck@D??Ij@Cd@",
"color": "#FF7F50",
"type": "INBOUND",
"order": 9,
"showDirection": false,
"showPlatform": false,
"showScheduleNumber": 1,
"useCustomShape": 0,
"showVehicleCapacity": true
},
{
"id": 14,
"name": "Main Street Outbound",
"abbr": "MS O",
"stops": [
1,
2,
3,
70,
71,
72,
73,
74,
75,
76,
77,
155,
78,
12,
132,
13,
14,
15,
16,
17,
18,
19,
21
],
"vType": "Bus",
"encLine": "sb{_GfitnMFh@LZ@nCO~C[`E??[`CAVEX}@vDk@|Ak@`A??y@bA_@VUHYFSCy@?}@A??cAQs@Qa@Q[S??OOkA|Bg@z@WV??kAnAy@l@mAr@cA^??q@VmAXuCj@YTMV??GJo@Ka@Ek@@sBH??iADHsFRaA??\\}@~AwB??n@}@Ri@Fc@?YCg@??I[O[OS[QYGoBE??mGQ??uHW??qAIoC]_B_@??qA]aA[aE_B??eJ{D??kH_DSWo@YAJeAg@q@e@{@w@gAkAeA_A??m@]o@YeAW{@IeA?_@D??{@Ny@Vm@VsDjB??mBbAkB|@o@RyAX??yADqD?KK[GUKSQ??SYKUMk@UuBK]Wg@[Ya@S[Ei@F??}ARIi@Ay@EmH??GaP??EgFBY?e@OmJ??KaFtGYlHa@OmCIaCIsD?kBCkCcEsCr@oCNSb@yA??|@eDd@{AiD@gCJoFR}DT}C`@s@mBa@gAi@uBOmAAuB?y@f@WXFxEvCz@b@a@|BMz@Ct@FnABXpBKbCIxCMdHQh@AFSjBkG??lCsJ`@}AAMd@}A??x@qCrCiIt@iB??lI_R??fFgLlByD??rImR??zIsR??zIqR?@pCeGr@gBV}@hAoF??h@}BTuA\\mC~AuNJB??KCrA_MDy@@sE????FcIjCd@??RHL?`@IVMLOJUJWHc@??JsAvATxB`@??|AZ",
"color": "#6495ED",
"type": "INBOUND",
"order": 10,
"showDirection": false,
"showPlatform": false,
"showScheduleNumber": 1,
"useCustomShape": 0,
"showVehicleCapacity": true
},
{
"id": 15,
"name": "Main Street Inbound",
"abbr": "MS I",
"stops": [
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
79,
155,
80,
81,
82,
83,
84,
85,
32,
61,
1
],
"vType": "Bus",
"encLine": "wv|_G~~inMyCk@??KC_@SwB_@A?}@OSFKtAI`@Uf@UV??IDg@NQHmB]??_@IItL???`BEx@c@vDM~A??sAhLGAF@y@vH]zB?A}BpK??W~@Wr@eBvD??yCtG??{CtG??yCtG??mAjC??uAxC?@gDnH??gDpH??gDpH??uAnCuA|C??gDpH??gDrH??gDnH??]|@_C~G??{@jCw@lCGDW~@??wCjK??oBjH{IRuH^iD`@y@Hg@qAk@{Ac@}AQ{@IcAAmA@kA\\WNGd@RtD`Cd@Vh@TShA]dBAn@BjAHp@zEYtGU`FIs@vB??sA|EGZs@jC?AAHfEjC?T??@pCDhD??RzH??@t@}DP??yFX??}BJIF?fA??DjFJ~F??@x@FXFtH??DzL??@dGDxBKl@?H??BzCTlG??DlA@`GdA@??lBAf@QLIVYP]Po@VsA??TcAZq@RSRMTGZC??XCNGpCE@?t@Ij@KbA]\\Q??`EuB??hAk@lCqA??`@Op@Ql@IfAA|@F??t@LRFhAj@f@\\t@t@??rBrB\\V|Av@??r@\\NLfFvB??dHvC??rErB??pB|@lBr@??THfBVxAP??xCTjAB??vCD~AH??pFN??tCJTJPNLRJX??BTBh@Gf@GRU^MHYJUBOA??SGWSS_@Ig@AiA??CKPwK??JaL??@IPo@DIRMXGjCR??dBT|@RdBf@??|@d@nA`A??ZVNT~@nC??Xz@V`CXhB??`AzDFb@^|@??Vf@v@|@x@j@~@\\??b@HfAFl@?h@CNERK??n@Q^Wx@cAh@}@??Vo@Tq@\\uA^aBDY@WL}@??TkBRyCLqC??@M????`@UDM@U???oCCeAC]_@BK@M~@?Z",
"color": "#6495ED",
"type": "INBOUND",
"order": 11,
"showDirection": false,
"showPlatform": false,
"showScheduleNumber": 1,
"useCustomShape": 0,
"showVehicleCapacity": true
},
{
"id": 9,
"name": "Riviera Ridge - Town Square Mall",
"abbr": "RRT",
"stops": [
118,
2,
3,
119,
120,
121,
122,
123,
124,
125,
126,
127,
128,
32,
61,
1
],
"vType": "Bus",
"encLine": "me{_GbysnMX|@d@vBRzAFxA??JdAFvC@nCC^??K~BSxCUjBM|@AV??e@zBi@rB_@~@Yf@m@x@??]b@_@Vo@PSCy@?aAAq@M??aAUa@Q_@UKMeBbD??MTcBfBy@l@mAr@??uBv@mAXsB`@??g@JSRUb@o@Ka@Ek@@iBF??sAF_@?MjM??EzBe@rL??q@nM??q@vM??KfDGvE??CnI??CfI??EhN?AFvFLrF??D`AXD~BInGa@??rJg@??rCOBbB??VzHPjI??NbJRhD??ZrEyEZ??UBoAZiF~B??}GzC??yC|AwA~@??{AlANbC??bA`P??H~AD^f@fA??F\\m@\\SN??CBFj@Bf@Mf@Kf@EXJX??FZ\\|@??t@fB??RXFDf@GNCIsA??OaC??OcC??MsB??C_@bAO??~AK??|AO??dAKLAMuB??OaC??QgC??OgC??QeC??OgC??OeC?AQcC??OoB??OuB??UkC??U{C??W{C??U{C??U{C??W{C?@U{C??W{C??U{C??Q}C?@MyC??GqC??AmC??CmC??@mC??@oC??BmC???mC??@kC???_B??@qB??@oB??@}B??DaC?@F}B@?A?J_C??JaC??L_C??L_C??J_C??L_C??LaC??L_C??J_C??H_C??J}B??F_C??F}B??D_C??BaC??BmB`AE??jBIA?|@Ah@F??f@HTc@XU??fB]??bB_@??~Ag@??l@Wh@[??b@Wl@c@??jAmA??b@c@j@aA??v@}Ax@j@z@Z??f@Jj@D??h@Br@C??TANERK^I@?LG^Wn@u@??Zg@l@oA??Vu@\\sA?A^aBDg@??VmB??RyB??NwB??JsB??@Y`@UDM@U???cC??EyAAU[BM@A?????Ij@Cf@",
"color": "#E37383",
"type": "INBOUND",
"order": 12,
"showDirection": false,
"showPlatform": false,
"showScheduleNumber": 1,
"useCustomShape": 0,
"showVehicleCapacity": true
},
{
"id": 10,
"name": "Oakdale Commons",
"abbr": "OC",
"stops": [
118,
2,
3,
129,
130,
32,
61,
1
],
"vType": "Bus",
"encLine": "me{_GdysnMBFf@jBR~@NhAHz@Bv@H|@FpD??@tBO~CSxCUjB??M|@AVEX}@vDk@|AYf@k@x@??_@b@_@Vo@PSCy@?aAAgAS??mAa@_@UKMkA|Bc@r@??gBnBy@l@mAr@QH??cBl@mAXoB^??k@LSRUb@o@Ka@E{BF??mBHDuB??B}BTcAZ{@rBqC??Zc@L[FUDu@Ek@Og@W_@OKSIQCe@A??{FO??kGQ??{CM{@GsAQ??{@KmAYcBc@yAg@??iCcA{DaB??gIkD??mD}AqCqA??k@a@mDkDm@c@??kAk@eAW{@IeA???aANs@NmAd@qAp@??sGdD??g@To@Ru@Pw@HsAB??uG@??}DGyA???_CAyCDg@BA?iBHsARqAXo@R??q@TyB|@eClA??cHnD??kD`BuBz@??uExB]JYDG^EpA??EtB@tBA`E???hAaBB??Y@Ut@Uf@]n@??UZWXu@f@e@Py@NeER?O???qCEc@??ASEEe@???iBBED??CNCbA??B~B???DfDCz@E??hCMx@OJE??XKt@g@VYT[??\\o@Tg@Ts@??zBGNFv@@??N?r@RRBlAE??n@EE}B??M{G??G_CSa@WZ??y@dAKTEVB~@??J~F??DvBSCs@S??WOWUISKu@@mA??FkF??D}ATi@??d@w@xAo@??pCyA??vCwA??vCuA??vCuA??tCwA??lCoA??~@a@zAg@??x@Q~AS??pAKdCC??|DA??fE???dE???dE???z@Al@EfAU??|@YxBgA??nDkB??|BiAt@Y??l@Ol@IfAAn@D??bANZJ`Af@t@h@??zC|Cj@^??dBx@\\RNLnAh@??jDvA??xD`B??xD`B??bEhB??p@ZpBr@TD??pAPvBTA?|BPdBD??|BBvAH??dFN??~ABhAFTJRP??Tf@DXBh@Gf@Sd@WVYJ_@@??OAWOSWIQIg@?_AEU??HeF??HeF??DgF????DoCPo@DIRMXG\\B??rBNjAP??|Bf@`AZ??t@`@bBpA??V\\\\~@j@hBN`@BX??b@lDd@tB??d@hBBXHTb@bAV`@?Al@r@x@j@x@ZTFn@D??j@Dl@?h@Cl@U??d@MTOVW|@kA??n@uAf@cB??j@eCDY@WF]??ZkCLiB??ToE??ZOHIBa@?a@???kBCw@?@Cm@k@D????Ij@Ch@",
"color": "#FFC000",
"type": "INBOUND",
"order": 13,
"showDirection": false,
"showPlatform": false,
"showScheduleNumber": 1,
"useCustomShape": 0,
"showVehicleCapacity": true
},
{
"id": 17,
"name": "Downtown Southside Outbound",
"abbr": "DSO",
"stops": [
136,
137,
1,
138,
139,
140,
141,
142,
143,
144,
145,
146,
147,
148,
154,
150,
151,
153,
21
],
"vType": "Bus",
"encLine": "ub{_GbitnM@?@VDTLZ?X@tBQtD??QbCUjBM|@AVEX}@vDk@|AYj@A?{@hAWVa@Tc@JSCy@?aAA_AQ_AWA?e@WYW??i@m@e@o@e@aAQMc@aBg@}BYyB??MmA@e@G_B??QuA@M@UAQMWKI??IEWDg@I??}@U}@[??_Cs@{@SoBYgCMe@_@M[??Kq@JmF??ToP??R{M??DsATgDZuC^gC??xBuL??fAeG??pAeHfC{N?AfAsGNgA??^gCz@sE??`@yB@A\\[^gA??n@kB??r@wBVo@j@iA??|BmE??`BkE??|BmG??fCaH??t@sBbB{F??xBsH??V}@\\gBLmAJoB?@TuD`@cE??|A_Q?A`@mEj@gF??V}C?@HsBMyEKaAUgAiAkD??mB{Go@qB??mE{M??gBsF?@}@qC??EOwCK??aDEuAG??[Gi@Sg@_@Y[Sc@Qc@gAsE??gCyL??i@uCk@eF??a@qEUoG??]mNK_C??mAuJqAkG??UmB_AaN??WiDuAcL??_AqH??Ky@mBp@??uFrB??uFrB??yGbC??eBr@s@`@}BzA??uEbDOH[F??e@LsEx@?rC@hBh@lFd@xEb@hCn@`D`@bCf@tCj@pCThAz@bF??TbB??BPEf@PpAjAnG??R`AJHNt@??ZfBb@jG??Dn@vAX??nB^??lB\\@?n@DzE???z@ATNO|B??IrA??CXR@jBZ??hDn@@?j@L",
"color": "#CECECE",
"type": "INBOUND",
"order": 15,
"showDirection": false,
"showPlatform": false,
"showScheduleNumber": 1,
"useCustomShape": 0,
"showVehicleCapacity": true
}
]

File diff suppressed because it is too large Load diff

View file

@ -1,6 +0,0 @@
export "src/data/utils.dart";
export "src/data/trip.dart";
export "src/data/path.dart";
export "src/data/polyline.dart";
export "package:shared/data.dart";

View file

@ -1,27 +0,0 @@
import "package:flutter/material.dart";
import "package:client/models.dart";
import "package:client/pages.dart";
import "package:client/services.dart";
Future<void> main() async {
await services.init();
await models.init();
await models.initFromOthers();
runApp(const ClientApp());
}
/// The main app widget.
class ClientApp extends StatelessWidget {
/// A const constructor.
const ClientApp();
@override
Widget build(BuildContext context) => MaterialApp.router(
title: "Flutter Demo",
theme: ThemeData(
useMaterial3: true,
),
routerConfig: router,
);
}

View file

@ -1,31 +0,0 @@
import "src/models/model.dart";
export "src/models/model.dart";
/// A [DataModel] to manage all other data models.
class Models extends DataModel {
/// Prevents other instances of this class from being created.
Models._();
// List your models here
/// A list of all models to manage.
List<DataModel> get models => [];
@override
Future<void> init() async {
for (final model in models) {
await model.init();
}
}
@override
Future<void> initFromOthers() async {
for (final model in models) {
await model.initFromOthers();
}
}
}
/// The global data model singleton.
final models = Models._();

View file

@ -1,22 +0,0 @@
import "package:go_router/go_router.dart";
export "package:go_router/go_router.dart";
import "src/pages/home.dart";
/// Contains all the routes for this app.
class Routes {
/// The route for the home page.
static const home = "/";
}
/// The router for the app.
final router = GoRouter(
initialLocation: Routes.home,
routes: [
GoRoute(
path: Routes.home,
name: Routes.home,
builder: (_, __) => HomePage(),
),
],
);

View file

@ -1,31 +0,0 @@
/// Defines and manages the different services used by the app.
library;
import "src/services/api.dart";
import "src/services/service.dart";
import "src/services/maps.dart";
/// A [Service] that manages all other services used by the app.
class Services extends Service {
/// Prevents other instances of this class from being created.
Services._();
// Define your services here
final api = ApiService();
final maps = GoogleMapsService();
/// The different services to initialize, in this order.
List<Service> get services => [api, maps];
@override
Future<void> init() async {
for (final service in services) {
await service.init();
}
}
}
/// The global services object.
final services = Services._();

View file

@ -1,15 +0,0 @@
import "trip.dart";
import "package:shared/data.dart";
class PathStep {
final PathTrip trip;
final TripStop enter;
final TripStop exit;
PathStep.fromJson(Json json) :
trip = PathTrip.fromJson(json["trip"]),
enter = TripStop.fromJson(json["enter_bus"]),
exit = TripStop.fromJson(json["exit_bus"]);
}
typedef Path = List<PathStep>;

View file

@ -1,45 +0,0 @@
import "package:fixnum/fixnum.dart";
import "package:google_maps_flutter/google_maps_flutter.dart";
List<LatLng> decodePolyline(String polyline) {
final points = <LatLng>[]; // List to hold the decoded coordinates
var index = 0;
final len = polyline.length;
var lat = Int32();
var lng = Int32();
// While there are still characters to process in the polyline
while (index < len) {
int b;
var shift = 0;
var result = Int32();
final oneF = Int32(0x1F);
// Decode latitude
do {
b = polyline.codeUnitAt(index++) - 63; // Get the next character
result |= (Int32(b) & oneF) << shift; // Update the result
shift += 5; // Increase the shift
} while (b >= 0x20); // Continue until a character less than 0x20 is found
final dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1)); // Decode the latitude delta
lat = (lat + dlat) as Int32; // Update the latitude
shift = 0; // Reset shift for longitude decoding
result = Int32(); // Reset result for longitude decoding
// Decode longitude
do {
b = polyline.codeUnitAt(index++) - 63; // Get the next character
result |= (Int32(b) & oneF) << shift; // Update the result
shift += 5; // Increase the shift
} while (b >= 0x20); // Continue until a character less than 0x20 is found
final dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1)); // Decode the longitude delta
lng = (lng + dlng) as Int32; // Update the longitude
// Add the new LatLng poInt32 to the list of points
points.add(LatLng(lat.toInt() / 1E5, lng.toInt() / 1E5));
}
return points;
}

View file

@ -1,80 +0,0 @@
import "package:shared/data.dart";
import "package:flutter/material.dart" show TimeOfDay;
extension type TripID(String id) { }
class Trip {
final List<TripStop> stops;
final String path;
final String name;
final String abbrev;
final String polyline;
const Trip({
required this.stops,
required this.path,
required this.name,
required this.abbrev,
required this.polyline,
});
Trip.fromJson(Json json) :
stops = [
for (final stop in json["stops"])
TripStop.fromJson(stop),
],
path = json["path"],
name = json["name"],
abbrev = json["abbrev"],
polyline = json["polyline"];
}
class TripStop {
final double lat;
final double long;
// final List<ScheduledTime> times;
// For testing, use this:
final String time;
const TripStop({
required this.lat,
required this.long,
// required this.times,
required this.time,
});
TripStop.fromJson(Json json) :
lat = json["latitude"],
long = json["longitude"],
time = json["time"];
// times = [
// for (final time in json["times"])
// ScheduledTime.fromJson(time),
// ];
}
class ScheduledTime {
static const weekdays = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
final String weekday;
final TimeOfDay timeOfDay;
const ScheduledTime({
required this.weekday,
required this.timeOfDay,
});
factory ScheduledTime.fromJson(Json json) {
final weekday = json["dotw"];
final [hour, minute] = (json["time"] as String).split(":");
final timeOfDay = TimeOfDay(hour: int.parse(hour), minute: int.parse(minute));
return ScheduledTime(weekday: weekday, timeOfDay: timeOfDay);
}
}
class PathTrip {
final String polyline;
PathTrip.fromJson(Json json) :
polyline = json["polyline"];
}

View file

@ -1,7 +0,0 @@
import "package:google_maps_flutter/google_maps_flutter.dart";
import "package:shared/data.dart";
extension CoordinatesUtils on Coordinates {
LatLng toLatLng() => LatLng(lat, long);
}

View file

@ -1,17 +0,0 @@
import "package:flutter/foundation.dart";
/// A model containing data needed throughout the app.
///
/// This model may need to be initialized, so [init] should be called before using it. This model
/// should also be held as a singleton in some global scope.
abstract class DataModel with ChangeNotifier {
/// Loads any data needed by the model.
///
/// This function must not depend on any other model.
Future<void> init();
/// Loads any data from other models.
///
/// At this point, all models have run [init], so it is safe to use other models.
Future<void> initFromOthers() async { }
}

View file

@ -1,92 +0,0 @@
import "package:client/data.dart";
import "package:flutter/material.dart";
import "package:google_maps_flutter/google_maps_flutter.dart";
import "package:client/view_models.dart";
import "package:client/widgets.dart";
/// The home page.
class HomePage extends ReactiveWidget<HomeModel> {
@override
HomeModel createModel() => HomeModel();
@override
Widget build(BuildContext context, HomeModel model) => Scaffold(
appBar: AppBar(title: const Text("Counter")),
body: Row(
children: [
SizedBox(
width: 325,
child: Sidebar(model),
),
Expanded(
child: Card(
child: Column(
children: [
SwitchListTile(
value: model.markerState == MarkerState.override,
onChanged: model.overrideMarkers,
title: const Text("Show stops list"),
subtitle: const Text("To select a start or end stop, use the buttons below"),
),
LatLongEditor(
latitudeController: model.startLatitudeController,
longitudeController: model.startLongitudeController,
label: "Start at",
isShowingMarkers: model.markerState == MarkerState.start,
showMarkers: () => model.showMarkers(MarkerState.start),
hideMarkers: model.hideMarkers,
),
const SizedBox(height: 12),
LatLongEditor(
latitudeController: model.endLatitudeController,
longitudeController: model.endLongitudeController,
label: "End at",
isShowingMarkers: model.markerState == MarkerState.end,
showMarkers: () => model.showMarkers(MarkerState.end),
hideMarkers: model.hideMarkers,
),
const SizedBox(height: 24),
FilledButton(onPressed: model.search, child: const Text("Search for a path")),
const SizedBox(height: 8),
const Divider(),
const SizedBox(height: 8),
if (model.isLoading)
const LinearProgressIndicator()
else if (model.isSearching && model.path == null)
const Text("Could not connect to API")
else
for (final step in model.path ?? <PathStep>[]) Text(
"Get on at ${step.enter.lat}, ${step.enter.long}\n"
"Get off at ${step.exit.lat}, ${step.exit.long}",
),
Expanded(
child: model.isGoogleReady
? GoogleMap(
onMapCreated: (controller) => model.mapController = controller,
initialCameraPosition: const CameraPosition(
target: LatLng(42.10125081757972, -75.94181323552698),
zoom: 14,
),
markers: model.markers,
onTap: model.onMapTapped,
polylines: {
for (final (index, route) in model.paths.indexed) Polyline(
polylineId: PolylineId(index.toString()),
color: routeColors[index],
points: route,
),
},
)
: const Center(child: CircularProgressIndicator()),
),
],
),
),
),
],
),
);
}
const routeColors = [Colors.blue, Colors.red, Colors.green, Colors.yellow, Colors.orange];

View file

@ -1,53 +0,0 @@
import "package:flutter/foundation.dart" show debugPrint, kDebugMode;
import "package:client/data.dart";
import "package:http/http.dart" as http;
import "api_client.dart";
import "service.dart";
class ApiService extends Service {
static const usingDocker = !kDebugMode;
final _client = ApiClient();
@override
Future<void> init() async {
debugPrint("Running with Docker? $usingDocker");
}
Uri get _base => usingDocker
? Uri(path: "api/")
: Uri(scheme: "http", host: "localhost", port: 8001);
Future<String?> getPath({
required Coordinates start,
required Coordinates end,
}) async {
final uri = Uri.parse("/tmp-api/path").replace(
queryParameters: {
"start_lat": start.lat.toString(),
"start_lon": start.long.toString(),
"end_lat": end.lat.toString(),
"end_lon": end.long.toString(),
},
);
try {
final response = await http.get(uri);
return response.body;
// Any error should show null
// ignore: avoid_catches_without_on_clauses
} catch (error) {
return null;
}
}
Future<List<Route>?> getRoutes() => _client.getJsonList(
_base.resolve("/routes"),
Route.fromJson,
);
Future<List<Stop>?> getStops() => _client.getJsonList(
_base.resolve("/stops"),
Stop.fromJson,
);
}

View file

@ -1,30 +0,0 @@
import "dart:convert";
import "package:client/data.dart";
import "package:flutter/foundation.dart";
import "package:http/http.dart" as http;
class ApiClient {
final client = http.Client();
Future<List<T>?> getJsonList<T>(Uri uri, FromJson<T> fromJson, {String? key}) async {
try {
final response = await client.get(uri);
if (response.statusCode != 200) return null;
// No key: Assume the entire body is a list
// With key: Assume the body is a map with a list at the key
final listOfJson = key == null
? (jsonDecode(response.body) as List).cast<Json>()
: ((jsonDecode(response.body) as Json)[key] as List).cast<Json>();
return [
for (final json in listOfJson)
fromJson(json),
];
// Want to catch all errors for the UI
// ignore: avoid_catches_without_on_clauses
} catch (error, stackTrace) {
debugPrint("Error: $error\n$stackTrace");
return null;
}
}
}

View file

@ -1,12 +0,0 @@
import "package:web/web.dart";
import "service.dart";
class GoogleMapsService extends Service {
@override
Future<void> init() async {
const key = String.fromEnvironment("GOOGLE_MAPS_API_KEY");
final script = document.createElement("script") as HTMLScriptElement;
script.src = "https://maps.googleapis.com/maps/api/js?key=$key";
document.head?.appendChild(script);
}
}

View file

@ -1,7 +0,0 @@
/// Defines a service: a singleton object to manage a resource.
abstract class Service {
/// Initializes any resources needed by the service.
///
/// This is guaranteed to be called before any other methods.
Future<void> init();
}

View file

@ -1,83 +0,0 @@
import "dart:async";
import "package:client/data.dart";
import "package:client/services.dart";
import "package:flutter/widgets.dart" hide Path;
import "package:google_maps_flutter/google_maps_flutter.dart";
import "home_markers.dart";
import "view_model.dart";
/// The view model for the home page.
class HomeModel extends ViewModel with HomeMarkers {
final startLatitudeController = TextEditingController();
final startLongitudeController = TextEditingController();
final endLatitudeController = TextEditingController();
final endLongitudeController = TextEditingController();
Path? path;
GoogleMapController? mapController;
double? get startLatitude => double.tryParse(startLatitudeController.text);
double? get startLongitude => double.tryParse(startLongitudeController.text);
double? get endLatitude => double.tryParse(endLatitudeController.text);
double? get endLongitude => double.tryParse(endLongitudeController.text);
bool isSearching = false;
bool isGoogleReady = false;
List<List<LatLng>> paths = [];
@override
Future<void> init() async {
await Future<void>.delayed(const Duration(seconds: 2));
isGoogleReady = true;
notifyListeners();
await updateMarkers();
}
void onMapTapped(LatLng coordinates) {
if (!shouldShowMarkers) return;
final marker = Marker(
markerId: markerId ?? const MarkerId("Tapped"),
position: coordinates,
icon: markerIcon ?? BitmapDescriptor.defaultMarker,
infoWindow: InfoWindow(title: "$startOrEnd here"),
);
switch (markerState!) {
case MarkerState.start: updateStart(coordinates, marker);
case MarkerState.end: updateEnd(coordinates, marker);
case MarkerState.override: return;
}
}
@override
void updateStart(LatLng coordinates, Marker marker) {
startLatitudeController.text = coordinates.latitude.toString();
startLongitudeController.text = coordinates.longitude.toString();
super.updateStart(coordinates, marker);
}
@override
void updateEnd(LatLng coordinates, Marker marker) {
endLatitudeController.text = coordinates.latitude.toString();
endLongitudeController.text = coordinates.longitude.toString();
super.updateEnd(coordinates, marker);
}
String? pathText;
Future<void> search() async {
final start = (lat: startLatitude, long: startLongitude);
final end = (lat: endLatitude, long: endLongitude);
if (start.lat == null || start.long == null) return;
if (end.lat == null || end.long == null) return;
isSearching = true;
isLoading = true;
final result = await services.api.getPath(start: start as Coordinates, end: end as Coordinates);
pathText = result ?? "An error occurred";
isLoading = false;
if (result == null) return;
isSearching = false;
notifyListeners();
}
}

View file

@ -1,166 +0,0 @@
import "package:client/data.dart";
import "package:client/services.dart";
import "package:flutter/foundation.dart";
import "package:google_maps_flutter/google_maps_flutter.dart";
enum MarkerState {
start,
end,
override;
}
mixin HomeMarkers on ChangeNotifier {
MarkerState? markerState;
bool get shouldShowMarkers => markerState != null;
// List<Stop>? stops;
Map<StopID, Stop> stops = {};
Map<RouteID, Route> routes = {};
Marker? _startMarker;
Marker? _endMarker;
Iterable<Stop> getStopsForRoute(RouteID routeID) => routes[routeID]!.stops
.map((stopID) => stops[stopID]!);
Set<Marker> get _filteredMarkers => {
// for (final stop in stops ?? <Stop>[])
for (final routeID in routesToShow)
for (final stop in getStopsForRoute(routeID))
Marker(
markerId: MarkerId(stop.name),
position: stop.coordinates.toLatLng(),
onTap: () => onMarkerTapped(stop),
consumeTapEvents: true,
infoWindow: InfoWindow(
title: stop.name,
snippet: stop.summary,
),
),
};
Set<Marker> get markers => shouldShowMarkers ? _filteredMarkers : {
if (_startMarker != null) _startMarker!,
if (_endMarker != null) _endMarker!,
};
List<Route> bcRouteNames = [];
List<Route> occtRouteNames = [];
Set<RouteID> routesToShow = {};
Iterable<(String, List<Route>)> get providers => [
("OCCT", occtRouteNames),
("BC Transit", bcRouteNames),
];
int _parseBcNumber(String routeName) {
// eg, "53)" --> 53
final first = routeName.split(" ").first;
final withoutParen = first.substring(0, first.length - 1);
return int.parse(withoutParen);
}
int compareBcRoutes(Route a, Route b) =>
_parseBcNumber(a.shortName).compareTo(_parseBcNumber(b.shortName));
int compareOcctRoutes(Route a, Route b) =>
a.shortName.compareTo(b.shortName);
MarkerId? get markerId => switch (markerState!) {
MarkerState.start => const MarkerId("start"),
MarkerState.end => const MarkerId("end"),
_ => null,
};
BitmapDescriptor? get markerIcon => switch (markerState!) {
MarkerState.start => BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueCyan),
MarkerState.end => BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen),
_ => null,
};
String? get startOrEnd => switch (markerState!) {
MarkerState.start => "Start",
MarkerState.end => "End",
_ => null,
};
Future<void> updateMarkers() async {
final stopsResponse = await services.api.getStops();
if (stopsResponse == null) return;
stops = {
for (final stop in stopsResponse)
stop.id: stop,
};
final routesResponse = await services.api.getRoutes();
if (routesResponse == null) return;
routes = {
for (final route in routesResponse)
route.id: route,
};
for (final route in routesResponse) {
final namesList = switch (route.provider) {
Provider.occt => occtRouteNames,
Provider.bc => bcRouteNames,
};
namesList.add(route);
}
occtRouteNames.sort(compareOcctRoutes);
bcRouteNames.sort(compareBcRoutes);
}
void showMarkers(MarkerState newState) {
markerState = newState;
notifyListeners();
}
void hideMarkers() {
markerState = null;
notifyListeners();
}
void onMarkerTapped(Stop stop) {
final coordinates = stop.coordinates.toLatLng();
final marker = Marker(
markerId: markerId ?? MarkerId(stop.name),
position: coordinates,
infoWindow: InfoWindow(
title: "$startOrEnd at ${stop.name}",
snippet: stop.description,
),
icon: markerIcon ?? BitmapDescriptor.defaultMarker,
);
switch (markerState!) {
case MarkerState.start: updateStart(coordinates, marker);
case MarkerState.end: updateEnd(coordinates, marker);
case MarkerState.override: return;
}
}
// Flutter widget
// ignore: avoid_positional_boolean_parameters
void overrideMarkers(bool value) {
markerState = value ? MarkerState.override : null;
notifyListeners();
}
@mustCallSuper
void updateStart(LatLng coordinates, Marker marker) {
_startMarker = marker;
markerState = null;
notifyListeners();
}
@mustCallSuper
void updateEnd(LatLng coordinates, Marker marker) {
_endMarker = marker;
markerState = null;
notifyListeners();
}
void showRoute(Route route, {required bool shouldShow}) {
if (shouldShow) {
routesToShow.add(route.id);
} else {
routesToShow.remove(route.id);
}
notifyListeners();
}
}

View file

@ -1,54 +0,0 @@
import "package:flutter/foundation.dart";
/// A model to load and manage state needed by any piece of UI.
///
/// [init] is called right away it is *not* awaited. Use [isLoading] and
/// [errorText] to convey progress to the user. This allows the UI to load immediately.
abstract class ViewModel with ChangeNotifier {
/// Calls [init] right away but does not await it.
ViewModel() {
init();
}
/// Override this method to initializes any data needed by the model.
Future<void> init() async {}
/// Whether this model is currently loading data. Setting this updates the UI.
bool _isLoading = false;
bool get isLoading => _isLoading;
set isLoading(bool value) {
_isLoading = value;
notifyListeners();
}
/// Whether this model has encountered an error. Setting this updates the UI.
String? _errorText;
String? get errorText => _errorText;
set errorText(String? value) {
_errorText = value;
notifyListeners();
}
/// Wether this model is still attached to a widget.
bool _isMounted = true;
@override
void notifyListeners() {
if (_isMounted) super.notifyListeners();
}
@override
void dispose() {
_isMounted = false;
super.dispose();
}
}
/// A model to build a value from the UI.
abstract class BuilderModel<T> extends ViewModel {
/// The value being edited.
T get value;
/// Whether the [value] is ready to be accessed.
bool get isReady;
}

View file

@ -1,60 +0,0 @@
import "package:client/widgets.dart";
import "package:flutter/material.dart";
class LatLongEditor extends StatelessWidget {
final TextEditingController latitudeController;
final TextEditingController longitudeController;
final String label;
final VoidCallback showMarkers;
final VoidCallback hideMarkers;
final bool isShowingMarkers;
const LatLongEditor({
required this.latitudeController,
required this.longitudeController,
required this.label,
required this.isShowingMarkers,
required this.showMarkers,
required this.hideMarkers,
super.key,
});
@override
Widget build(BuildContext context) => SizedBox(
width: 400,
child: Row(
children: [
Text(label, style: context.textTheme.titleSmall),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: latitudeController,
decoration: const InputDecoration(
hintText: "Latitude",
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: longitudeController,
decoration: const InputDecoration(
hintText: "Longitude",
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 8),
if (isShowingMarkers) TextButton(
onPressed: hideMarkers,
child: const Text("Cancel"),
) else TextButton.icon(
icon: const Icon(Icons.location_on),
onPressed: showMarkers,
label: const Text("Pick on map"),
),
],
),
);
}

View file

@ -1,80 +0,0 @@
import "package:flutter/material.dart";
abstract class ReactiveWidgetInterface<T extends ChangeNotifier> extends StatefulWidget {
const ReactiveWidgetInterface();
/// A function to create or find the model. This function will only be called once.
T createModel();
/// Whether the view model should be disposed or not.
bool get shouldDispose;
/// Builds the UI according to the state in [model].
Widget build(BuildContext context, T model);
@override
ReactiveWidgetState createState() => ReactiveWidgetState<T>();
/// This function gives you an opportunity to update the view model when the widget updates.
///
/// For more details, see [State.didUpdateWidget].
void didUpdateWidget(covariant ReactiveWidgetInterface<T> oldWidget, T model) { }
}
/// A widget that creates, subscribes to, and disposes of a [ChangeNotifier].
abstract class ReactiveWidget<T extends ChangeNotifier> extends ReactiveWidgetInterface<T> {
/// A const constructor.
const ReactiveWidget();
@override
T createModel();
@override
bool get shouldDispose => true;
}
/// A [ReactiveWidget] that works with a reusable [ChangeNotifier].
abstract class ReusableReactiveWidget<T extends ChangeNotifier> extends ReactiveWidgetInterface<T> {
/// The model to listen to.
final T model;
/// Creates a widget that listens to a view model.
const ReusableReactiveWidget(this.model);
@override
T createModel() => model;
@override
bool get shouldDispose => false;
}
/// A state for [ReactiveWidget] that manages the [model].
class ReactiveWidgetState<T extends ChangeNotifier> extends State<ReactiveWidgetInterface<T>>{
/// The model to listen to.
late final T model;
@override
void initState() {
super.initState();
model = widget.createModel();
model.addListener(listener);
}
@override
void dispose() {
model.removeListener(listener);
if (widget.shouldDispose) model.dispose();
super.dispose();
}
@override
void didUpdateWidget(covariant ReactiveWidgetInterface<T> oldWidget) {
widget.didUpdateWidget(oldWidget, model);
super.didUpdateWidget(oldWidget);
}
/// Updates the UI when [model] updates.
void listener() => setState(() {});
@override
Widget build(BuildContext context) => widget.build(context, model);
}

View file

@ -1,65 +0,0 @@
import "package:client/view_models.dart";
import "package:client/widgets.dart";
import "package:flutter/material.dart";
class Sidebar extends ReusableReactiveWidget<HomeModel> {
const Sidebar(super.model);
@override
Widget build(BuildContext context, HomeModel model) => DefaultTabController(
length: 2,
child: Card(
clipBehavior: Clip.hardEdge,
child: Column(
children: [
const SizedBox(height: 16),
if (model.markerState != null && model.markerState != MarkerState.override) ...[
Text(
"Select routes",
maxLines: 1,
style: context.textTheme.titleLarge,
textAlign: TextAlign.center,
),
Text(
"Or click anywhere on the map",
maxLines: 1,
style: context.textTheme.bodyMedium,
textAlign: TextAlign.center,
),
],
const SizedBox(height: 8),
if (model.shouldShowMarkers) TabBar(
tabs: [
for (final (provider, _) in model.providers)
Tab(text: provider),
],
),
if (!model.shouldShowMarkers && model.pathText != null)
SingleChildScrollView(child: Center(child: Text(model.pathText!, style: context.textTheme.bodySmall))),
if (!model.shouldShowMarkers && model.pathText == null)
const Center(child: Text("Choose start or end location")),
if (model.shouldShowMarkers) Expanded(
child: TabBarView(
children: [
for (final (_, routesList) in model.providers)
ListView(
children: [
for (final route in routesList) CheckboxListTile(
title: Text(route.fullName, maxLines: 1),
subtitle: Text(
"${route.stops.length} stops",
maxLines: 1,
),
value: model.routesToShow.contains(route.id),
onChanged: (value) => model.showRoute(route, shouldShow: value!),
),
],
),
],
),
),
],
),
),
);
}

View file

@ -1,4 +0,0 @@
export "src/view_models/view_model.dart";
export "src/view_models/home_markers.dart";
export "src/view_models/home.dart";

View file

@ -1,22 +0,0 @@
import "package:flutter/material.dart";
export "package:go_router/go_router.dart";
export "src/widgets/lat_long_editor.dart";
export "src/widgets/reactive_widget.dart";
export "src/widgets/sidebar.dart";
/// Helpful methods on [BuildContext].
extension ContextUtils on BuildContext {
/// Gets the app's color scheme.
ColorScheme get colorScheme => Theme.of(this).colorScheme;
/// Gets the app's text theme.
TextTheme get textTheme => Theme.of(this).textTheme;
/// Formats a date according to the user's locale.
String formatDate(DateTime date) => MaterialLocalizations.of(this).formatCompactDate(date);
/// Formats a time according to the user's locale.
String formatTime(DateTime time) => MaterialLocalizations.of(this).formatTimeOfDay(TimeOfDay.fromDateTime(time));
}

View file

@ -1,30 +0,0 @@
name: client
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 0.1.0+1
environment:
sdk: ^3.5.0
resolution: workspace
dependencies:
fixnum: ^1.1.1
csv: ^6.0.0
flutter:
sdk: flutter
go_router: ^14.2.7
google_maps_flutter: ^2.12.0
html: ^0.15.5
http: ^1.3.0
polyline_tools: ^0.0.2
web: ^1.1.1
shared:
path: ../shared
dev_dependencies:
dhttpd: ^4.1.0
flutter_test:
sdk: flutter
flutter:
uses-material-design: true

Binary file not shown.

Before

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View file

@ -1,38 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="client">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>client</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>

View file

@ -1,35 +0,0 @@
{
"name": "client",
"short_name": "client",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "icons/Icon-maskable-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "icons/Icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}

View file

@ -1,17 +0,0 @@
flutter/ephemeral/
# Visual Studio user-specific files.
*.suo
*.user
*.userosscache
*.sln.docstates
# Visual Studio build-related files.
x64/
x86/
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/

View file

@ -1,108 +0,0 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.14)
project(client LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "client")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(VERSION 3.14...3.25)
# Define build configuration option.
get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if(IS_MULTICONFIG)
set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
CACHE STRING "" FORCE)
else()
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
endif()
# Define settings for the Profile build mode.
set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
# Use Unicode for all projects.
add_definitions(-DUNICODE -D_UNICODE)
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_17)
target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
target_compile_options(${TARGET} PRIVATE /EHsc)
target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# Support files are copied into place next to the executable, so that it can
# run in place. This is done instead of making a separate bundle (as on Linux)
# so that building and running from within Visual Studio will work.
set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
# Make the "install" step default, as it's required to run.
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
if(PLUGIN_BUNDLED_LIBRARIES)
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
CONFIGURATIONS Profile;Release
COMPONENT Runtime)

View file

@ -1,109 +0,0 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.14)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
# Set fallback configurations for older versions of the flutter tool.
if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
set(FLUTTER_TARGET_PLATFORM "windows-x64")
endif()
# === Flutter Library ===
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"flutter_export.h"
"flutter_windows.h"
"flutter_messenger.h"
"flutter_plugin_registrar.h"
"flutter_texture_registrar.h"
)
list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
add_dependencies(flutter flutter_assemble)
# === Wrapper ===
list(APPEND CPP_WRAPPER_SOURCES_CORE
"core_implementations.cc"
"standard_codec.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
"plugin_registrar.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_APP
"flutter_engine.cc"
"flutter_view_controller.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
# Wrapper sources needed for a plugin.
add_library(flutter_wrapper_plugin STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
)
apply_standard_settings(flutter_wrapper_plugin)
set_target_properties(flutter_wrapper_plugin PROPERTIES
POSITION_INDEPENDENT_CODE ON)
set_target_properties(flutter_wrapper_plugin PROPERTIES
CXX_VISIBILITY_PRESET hidden)
target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
target_include_directories(flutter_wrapper_plugin PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_plugin flutter_assemble)
# Wrapper sources needed for the runner.
add_library(flutter_wrapper_app STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_APP}
)
apply_standard_settings(flutter_wrapper_app)
target_link_libraries(flutter_wrapper_app PUBLIC flutter)
target_include_directories(flutter_wrapper_app PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_app flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
${PHONY_OUTPUT}
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
${FLUTTER_TARGET_PLATFORM} $<CONFIG>
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
)

View file

@ -1,11 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
void RegisterPlugins(flutter::PluginRegistry* registry) {
}

View file

@ -1,15 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter/plugin_registry.h>
// Registers Flutter plugins.
void RegisterPlugins(flutter::PluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View file

@ -1,23 +0,0 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View file

@ -1,40 +0,0 @@
cmake_minimum_required(VERSION 3.14)
project(runner LANGUAGES CXX)
# Define the application target. To change its name, change BINARY_NAME in the
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
# work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME} WIN32
"flutter_window.cpp"
"main.cpp"
"utils.cpp"
"win32_window.cpp"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
"Runner.rc"
"runner.exe.manifest"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add preprocessor definitions for the build version.
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
# Disable Windows macros that collide with C++ standard library functions.
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
# Add dependency libraries and include directories. Add any application-specific
# dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)

View file

@ -1,121 +0,0 @@
// Microsoft Visual C++ generated resource script.
//
#pragma code_page(65001)
#include "resource.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "winres.h"
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
// English (United States) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE
BEGIN
"#include ""winres.h""\r\n"
"\0"
END
3 TEXTINCLUDE
BEGIN
"\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Icon
//
// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_APP_ICON ICON "resources\\app_icon.ico"
/////////////////////////////////////////////////////////////////////////////
//
// Version
//
#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
#else
#define VERSION_AS_NUMBER 1,0,0,0
#endif
#if defined(FLUTTER_VERSION)
#define VERSION_AS_STRING FLUTTER_VERSION
#else
#define VERSION_AS_STRING "1.0.0"
#endif
VS_VERSION_INFO VERSIONINFO
FILEVERSION VERSION_AS_NUMBER
PRODUCTVERSION VERSION_AS_NUMBER
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS__WINDOWS32
FILETYPE VFT_APP
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904e4"
BEGIN
VALUE "CompanyName", "com.example" "\0"
VALUE "FileDescription", "client" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "client" "\0"
VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0"
VALUE "OriginalFilename", "client.exe" "\0"
VALUE "ProductName", "client" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1252
END
END
#endif // English (United States) resources
/////////////////////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED

View file

@ -1,71 +0,0 @@
#include "flutter_window.h"
#include <optional>
#include "flutter/generated_plugin_registrant.h"
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
: project_(project) {}
FlutterWindow::~FlutterWindow() {}
bool FlutterWindow::OnCreate() {
if (!Win32Window::OnCreate()) {
return false;
}
RECT frame = GetClientArea();
// The size here must match the window dimensions to avoid unnecessary surface
// creation / destruction in the startup path.
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
frame.right - frame.left, frame.bottom - frame.top, project_);
// Ensure that basic setup of the controller was successful.
if (!flutter_controller_->engine() || !flutter_controller_->view()) {
return false;
}
RegisterPlugins(flutter_controller_->engine());
SetChildContent(flutter_controller_->view()->GetNativeWindow());
flutter_controller_->engine()->SetNextFrameCallback([&]() {
this->Show();
});
// Flutter can complete the first frame before the "show window" callback is
// registered. The following call ensures a frame is pending to ensure the
// window is shown. It is a no-op if the first frame hasn't completed yet.
flutter_controller_->ForceRedraw();
return true;
}
void FlutterWindow::OnDestroy() {
if (flutter_controller_) {
flutter_controller_ = nullptr;
}
Win32Window::OnDestroy();
}
LRESULT
FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
// Give Flutter, including plugins, an opportunity to handle window messages.
if (flutter_controller_) {
std::optional<LRESULT> result =
flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
lparam);
if (result) {
return *result;
}
}
switch (message) {
case WM_FONTCHANGE:
flutter_controller_->engine()->ReloadSystemFonts();
break;
}
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
}

View file

@ -1,33 +0,0 @@
#ifndef RUNNER_FLUTTER_WINDOW_H_
#define RUNNER_FLUTTER_WINDOW_H_
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <memory>
#include "win32_window.h"
// A window that does nothing but host a Flutter view.
class FlutterWindow : public Win32Window {
public:
// Creates a new FlutterWindow hosting a Flutter view running |project|.
explicit FlutterWindow(const flutter::DartProject& project);
virtual ~FlutterWindow();
protected:
// Win32Window:
bool OnCreate() override;
void OnDestroy() override;
LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
LPARAM const lparam) noexcept override;
private:
// The project to run.
flutter::DartProject project_;
// The Flutter instance hosted by this window.
std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
};
#endif // RUNNER_FLUTTER_WINDOW_H_

View file

@ -1,43 +0,0 @@
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <windows.h>
#include "flutter_window.h"
#include "utils.h"
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
_In_ wchar_t *command_line, _In_ int show_command) {
// Attach to console when present (e.g., 'flutter run') or create a
// new console when running with a debugger.
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
CreateAndAttachConsole();
}
// Initialize COM, so that it is available for use in the library and/or
// plugins.
::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
flutter::DartProject project(L"data");
std::vector<std::string> command_line_arguments =
GetCommandLineArguments();
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
FlutterWindow window(project);
Win32Window::Point origin(10, 10);
Win32Window::Size size(1280, 720);
if (!window.Create(L"client", origin, size)) {
return EXIT_FAILURE;
}
window.SetQuitOnClose(true);
::MSG msg;
while (::GetMessage(&msg, nullptr, 0, 0)) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
::CoUninitialize();
return EXIT_SUCCESS;
}

View file

@ -1,16 +0,0 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by Runner.rc
//
#define IDI_APP_ICON 101
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

View file

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 and Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>

View file

@ -1,65 +0,0 @@
#include "utils.h"
#include <flutter_windows.h>
#include <io.h>
#include <stdio.h>
#include <windows.h>
#include <iostream>
void CreateAndAttachConsole() {
if (::AllocConsole()) {
FILE *unused;
if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
_dup2(_fileno(stdout), 1);
}
if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
_dup2(_fileno(stdout), 2);
}
std::ios::sync_with_stdio();
FlutterDesktopResyncOutputStreams();
}
}
std::vector<std::string> GetCommandLineArguments() {
// Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
int argc;
wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
if (argv == nullptr) {
return std::vector<std::string>();
}
std::vector<std::string> command_line_arguments;
// Skip the first argument as it's the binary name.
for (int i = 1; i < argc; i++) {
command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
}
::LocalFree(argv);
return command_line_arguments;
}
std::string Utf8FromUtf16(const wchar_t* utf16_string) {
if (utf16_string == nullptr) {
return std::string();
}
unsigned int target_length = ::WideCharToMultiByte(
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
-1, nullptr, 0, nullptr, nullptr)
-1; // remove the trailing null character
int input_length = (int)wcslen(utf16_string);
std::string utf8_string;
if (target_length == 0 || target_length > utf8_string.max_size()) {
return utf8_string;
}
utf8_string.resize(target_length);
int converted_length = ::WideCharToMultiByte(
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
input_length, utf8_string.data(), target_length, nullptr, nullptr);
if (converted_length == 0) {
return std::string();
}
return utf8_string;
}

View file

@ -1,19 +0,0 @@
#ifndef RUNNER_UTILS_H_
#define RUNNER_UTILS_H_
#include <string>
#include <vector>
// Creates a console for the process, and redirects stdout and stderr to
// it for both the runner and the Flutter library.
void CreateAndAttachConsole();
// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
// encoded in UTF-8. Returns an empty std::string on failure.
std::string Utf8FromUtf16(const wchar_t* utf16_string);
// Gets the command line arguments passed in as a std::vector<std::string>,
// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
std::vector<std::string> GetCommandLineArguments();
#endif // RUNNER_UTILS_H_

View file

@ -1,288 +0,0 @@
#include "win32_window.h"
#include <dwmapi.h>
#include <flutter_windows.h>
#include "resource.h"
namespace {
/// Window attribute that enables dark mode window decorations.
///
/// Redefined in case the developer's machine has a Windows SDK older than
/// version 10.0.22000.0.
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
#endif
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
/// Registry key for app theme preference.
///
/// A value of 0 indicates apps should use dark mode. A non-zero or missing
/// value indicates apps should use light mode.
constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
// The number of Win32Window objects that currently exist.
static int g_active_window_count = 0;
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
// Scale helper to convert logical scaler values to physical using passed in
// scale factor
int Scale(int source, double scale_factor) {
return static_cast<int>(source * scale_factor);
}
// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
// This API is only needed for PerMonitor V1 awareness mode.
void EnableFullDpiSupportIfAvailable(HWND hwnd) {
HMODULE user32_module = LoadLibraryA("User32.dll");
if (!user32_module) {
return;
}
auto enable_non_client_dpi_scaling =
reinterpret_cast<EnableNonClientDpiScaling*>(
GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
if (enable_non_client_dpi_scaling != nullptr) {
enable_non_client_dpi_scaling(hwnd);
}
FreeLibrary(user32_module);
}
} // namespace
// Manages the Win32Window's window class registration.
class WindowClassRegistrar {
public:
~WindowClassRegistrar() = default;
// Returns the singleton registrar instance.
static WindowClassRegistrar* GetInstance() {
if (!instance_) {
instance_ = new WindowClassRegistrar();
}
return instance_;
}
// Returns the name of the window class, registering the class if it hasn't
// previously been registered.
const wchar_t* GetWindowClass();
// Unregisters the window class. Should only be called if there are no
// instances of the window.
void UnregisterWindowClass();
private:
WindowClassRegistrar() = default;
static WindowClassRegistrar* instance_;
bool class_registered_ = false;
};
WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
const wchar_t* WindowClassRegistrar::GetWindowClass() {
if (!class_registered_) {
WNDCLASS window_class{};
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
window_class.lpszClassName = kWindowClassName;
window_class.style = CS_HREDRAW | CS_VREDRAW;
window_class.cbClsExtra = 0;
window_class.cbWndExtra = 0;
window_class.hInstance = GetModuleHandle(nullptr);
window_class.hIcon =
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
window_class.hbrBackground = 0;
window_class.lpszMenuName = nullptr;
window_class.lpfnWndProc = Win32Window::WndProc;
RegisterClass(&window_class);
class_registered_ = true;
}
return kWindowClassName;
}
void WindowClassRegistrar::UnregisterWindowClass() {
UnregisterClass(kWindowClassName, nullptr);
class_registered_ = false;
}
Win32Window::Win32Window() {
++g_active_window_count;
}
Win32Window::~Win32Window() {
--g_active_window_count;
Destroy();
}
bool Win32Window::Create(const std::wstring& title,
const Point& origin,
const Size& size) {
Destroy();
const wchar_t* window_class =
WindowClassRegistrar::GetInstance()->GetWindowClass();
const POINT target_point = {static_cast<LONG>(origin.x),
static_cast<LONG>(origin.y)};
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
double scale_factor = dpi / 96.0;
HWND window = CreateWindow(
window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
Scale(size.width, scale_factor), Scale(size.height, scale_factor),
nullptr, nullptr, GetModuleHandle(nullptr), this);
if (!window) {
return false;
}
UpdateTheme(window);
return OnCreate();
}
bool Win32Window::Show() {
return ShowWindow(window_handle_, SW_SHOWNORMAL);
}
// static
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
if (message == WM_NCCREATE) {
auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
SetWindowLongPtr(window, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
EnableFullDpiSupportIfAvailable(window);
that->window_handle_ = window;
} else if (Win32Window* that = GetThisFromHandle(window)) {
return that->MessageHandler(window, message, wparam, lparam);
}
return DefWindowProc(window, message, wparam, lparam);
}
LRESULT
Win32Window::MessageHandler(HWND hwnd,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
switch (message) {
case WM_DESTROY:
window_handle_ = nullptr;
Destroy();
if (quit_on_close_) {
PostQuitMessage(0);
}
return 0;
case WM_DPICHANGED: {
auto newRectSize = reinterpret_cast<RECT*>(lparam);
LONG newWidth = newRectSize->right - newRectSize->left;
LONG newHeight = newRectSize->bottom - newRectSize->top;
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
return 0;
}
case WM_SIZE: {
RECT rect = GetClientArea();
if (child_content_ != nullptr) {
// Size and position the child window.
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
rect.bottom - rect.top, TRUE);
}
return 0;
}
case WM_ACTIVATE:
if (child_content_ != nullptr) {
SetFocus(child_content_);
}
return 0;
case WM_DWMCOLORIZATIONCOLORCHANGED:
UpdateTheme(hwnd);
return 0;
}
return DefWindowProc(window_handle_, message, wparam, lparam);
}
void Win32Window::Destroy() {
OnDestroy();
if (window_handle_) {
DestroyWindow(window_handle_);
window_handle_ = nullptr;
}
if (g_active_window_count == 0) {
WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
}
}
Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
return reinterpret_cast<Win32Window*>(
GetWindowLongPtr(window, GWLP_USERDATA));
}
void Win32Window::SetChildContent(HWND content) {
child_content_ = content;
SetParent(content, window_handle_);
RECT frame = GetClientArea();
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
frame.bottom - frame.top, true);
SetFocus(child_content_);
}
RECT Win32Window::GetClientArea() {
RECT frame;
GetClientRect(window_handle_, &frame);
return frame;
}
HWND Win32Window::GetHandle() {
return window_handle_;
}
void Win32Window::SetQuitOnClose(bool quit_on_close) {
quit_on_close_ = quit_on_close;
}
bool Win32Window::OnCreate() {
// No-op; provided for subclasses.
return true;
}
void Win32Window::OnDestroy() {
// No-op; provided for subclasses.
}
void Win32Window::UpdateTheme(HWND const window) {
DWORD light_mode;
DWORD light_mode_size = sizeof(light_mode);
LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
kGetPreferredBrightnessRegValue,
RRF_RT_REG_DWORD, nullptr, &light_mode,
&light_mode_size);
if (result == ERROR_SUCCESS) {
BOOL enable_dark_mode = light_mode == 0;
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
&enable_dark_mode, sizeof(enable_dark_mode));
}
}

View file

@ -1,102 +0,0 @@
#ifndef RUNNER_WIN32_WINDOW_H_
#define RUNNER_WIN32_WINDOW_H_
#include <windows.h>
#include <functional>
#include <memory>
#include <string>
// A class abstraction for a high DPI-aware Win32 Window. Intended to be
// inherited from by classes that wish to specialize with custom
// rendering and input handling
class Win32Window {
public:
struct Point {
unsigned int x;
unsigned int y;
Point(unsigned int x, unsigned int y) : x(x), y(y) {}
};
struct Size {
unsigned int width;
unsigned int height;
Size(unsigned int width, unsigned int height)
: width(width), height(height) {}
};
Win32Window();
virtual ~Win32Window();
// Creates a win32 window with |title| that is positioned and sized using
// |origin| and |size|. New windows are created on the default monitor. Window
// sizes are specified to the OS in physical pixels, hence to ensure a
// consistent size this function will scale the inputted width and height as
// as appropriate for the default monitor. The window is invisible until
// |Show| is called. Returns true if the window was created successfully.
bool Create(const std::wstring& title, const Point& origin, const Size& size);
// Show the current window. Returns true if the window was successfully shown.
bool Show();
// Release OS resources associated with window.
void Destroy();
// Inserts |content| into the window tree.
void SetChildContent(HWND content);
// Returns the backing Window handle to enable clients to set icon and other
// window properties. Returns nullptr if the window has been destroyed.
HWND GetHandle();
// If true, closing this window will quit the application.
void SetQuitOnClose(bool quit_on_close);
// Return a RECT representing the bounds of the current client area.
RECT GetClientArea();
protected:
// Processes and route salient window messages for mouse handling,
// size change and DPI. Delegates handling of these to member overloads that
// inheriting classes can handle.
virtual LRESULT MessageHandler(HWND window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept;
// Called when CreateAndShow is called, allowing subclass window-related
// setup. Subclasses should return false if setup fails.
virtual bool OnCreate();
// Called when Destroy is called.
virtual void OnDestroy();
private:
friend class WindowClassRegistrar;
// OS callback called by message pump. Handles the WM_NCCREATE message which
// is passed when the non-client area is being created and enables automatic
// non-client DPI scaling so that the non-client area automatically
// responds to changes in DPI. All other messages are handled by
// MessageHandler.
static LRESULT CALLBACK WndProc(HWND const window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept;
// Retrieves a class instance pointer for |window|
static Win32Window* GetThisFromHandle(HWND const window) noexcept;
// Update the window frame's theme to match the system theme.
static void UpdateTheme(HWND const window);
bool quit_on_close_ = false;
// window handle for top level window.
HWND window_handle_ = nullptr;
// window handle for hosted content.
HWND child_content_ = nullptr;
};
#endif // RUNNER_WIN32_WINDOW_H_

View file

@ -1,6 +0,0 @@
# Created by venv; see https://docs.python.org/3/library/venv.html
bin
include
lib
lib64
local.settings.json

View file

@ -1,5 +0,0 @@
{
"recommendations": [
"ms-azuretools.vscode-azurefunctions"
]
}

View file

@ -1,2 +0,0 @@
#!/bin/sh
func azure functionapp publish cron-map-scrape

View file

@ -1,41 +0,0 @@
import azure.functions as func
import datetime
import json
import urllib.request
import logging
app = func.FunctionApp()
# https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-storage-table?tabs=isolated-process%2Ctable-api%2Cextensionv3&pivots=programming-language-python#output
@app.timer_trigger(
schedule="*/5 * * * * *",
arg_name="myTimer",
run_on_startup=False,
use_monitor=False,
)
def Timer(myTimer: func.TimerRequest) -> None:
if myTimer.past_due:
logging.info("The timer is past due!")
logging.info("Python timer trigger function executed.")
endpoint = "https://binghamtonupublic.etaspot.net/service.php?service=get_vehicles&token=TESTING"
contents = urllib.request.urlopen(endpoint).read()
try:
vehicles = json.loads(contents)["get_vehicles"]
time = datetime.datetime.now()
seconds = int(
(time - datetime.datetime(1970, 1, 1)).total_seconds() % (3600 * 24)
)
weekday = time.weekday()
for vehicle in vehicles:
route = vehicle["routeID"]
equipment = vehicle["equipmentID"]
lattitude = vehicle["lat"]
longitude = vehicle["lng"]
logging.info(
f"{route},{equipment},{lattitude},{longitude},{seconds},{weekday}"
)
except Exception as e:
logging.error(e)
raise e

View file

@ -1,15 +0,0 @@
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
}
}
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[4.*, 5.0.0)"
}
}

View file

@ -1,5 +0,0 @@
home = /usr/sbin
include-system-site-packages = false
version = 3.13.1
executable = /usr/bin/python3.13
command = /usr/sbin/python -m venv /home/pagwin/projects/school/spring2025/cs445_soft_eng/portfolio-team4/src/data/azure_function_timer

View file

@ -1,5 +0,0 @@
# Do not include azure-functions-worker in this file
# The Python Worker is managed by the Azure Functions platform
# Manually managing azure-functions-worker may cause unexpected issues
azure-functions

Some files were not shown because too many files have changed in this diff Show more