From aad60fe6b24c0805b4ec3f44f1290d16cad4685c Mon Sep 17 00:00:00 2001 From: Sidney Kochman Date: Tue, 11 Jul 2017 23:36:28 -0400 Subject: [PATCH] Restructure (#55) Restructure the whole repo into smaller, testable sub packages. --- .codecov.yaml | 5 + .gitignore | 8 +- .travis.yml | 15 + Dockerfile | 16 +- README.md | 54 ++- api/api.go | 176 ++++++++++ {tracking => api}/legacy.go | 156 +++------ {tracking => api}/predict.go | 9 +- {tracking => api}/routes.go | 335 +++++++------------ api/vehicles.go | 180 ++++++++++ api/vehicles_test.go | 25 ++ cmd/runner.go | 41 +++ cmd/shuttletracker.go | 54 +++ conf.json.sample | 23 +- config/config.go | 40 +++ database/database.go | 52 +++ goget | 5 - gopath.sh | 2 - log/log.go | 119 +++++++ main.go | 119 +------ migration/database.go | 62 ---- migration/schema.go | 67 ---- model/model.go | 177 ++++++++++ prediction/predictor/statisticalpredictor.go | 25 -- prediction/predictor/tablepredictor.go | 22 -- prediction/schedule.go | 30 -- seed/vehicle_seed.json | 20 -- test.sh | 12 + tracking/app.go | 171 ---------- tracking/vehicles.go | 321 ------------------ updater/updater.go | 155 +++++++++ vendor/vendor.json | 205 ++++++++++++ 32 files changed, 1497 insertions(+), 1204 deletions(-) create mode 100644 .codecov.yaml create mode 100644 .travis.yml create mode 100644 api/api.go rename {tracking => api}/legacy.go (54%) rename {tracking => api}/predict.go (91%) rename {tracking => api}/routes.go (53%) create mode 100644 api/vehicles.go create mode 100644 api/vehicles_test.go create mode 100644 cmd/runner.go create mode 100644 cmd/shuttletracker.go create mode 100644 config/config.go create mode 100644 database/database.go delete mode 100755 goget delete mode 100644 gopath.sh create mode 100644 log/log.go delete mode 100644 migration/database.go delete mode 100644 migration/schema.go create mode 100644 model/model.go delete mode 100644 prediction/predictor/statisticalpredictor.go delete mode 100644 prediction/predictor/tablepredictor.go delete mode 100644 prediction/schedule.go delete mode 100644 seed/vehicle_seed.json create mode 100755 test.sh delete mode 100644 tracking/app.go delete mode 100644 tracking/vehicles.go create mode 100644 updater/updater.go create mode 100644 vendor/vendor.json diff --git a/.codecov.yaml b/.codecov.yaml new file mode 100644 index 000000000..52bc77a98 --- /dev/null +++ b/.codecov.yaml @@ -0,0 +1,5 @@ +comment: false +coverage: + status: + patch: false + project: false diff --git a/.gitignore b/.gitignore index 4b65db877..ab2e140ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,5 @@ -# See https://help.github.com/articles/ignoring-files for more about ignoring files. -# -# If you find yourself ignoring temporary files generated by your text editor -# or operating system, you probably want to add a global ignore instead: -# git config --global core.excludesfile '~/.gitignore_global' - bower_components conf.json *.swp +/vendor/*/ +/coverage.txt diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..684cc410a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +language: go + +go: + - 1.8.x + - tip + +before_install: + - go get -u github.com/kardianos/govendor + - govendor sync + +script: + - ./test.sh + +after_success: + - bash <(curl -s https://codecov.io/bash) diff --git a/Dockerfile b/Dockerfile index c73f92966..79fc95586 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,19 +13,19 @@ RUN ln -s /usr/bin/nodejs /usr/bin/node ENV GOPATH /go ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH -RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH" -WORKDIR $GOPATH/src/shuttle_tracking_2 +RUN mkdir -p "$GOPATH/src/github.com/wtg/shuttletracker" "$GOPATH/bin" && chmod -R 777 "$GOPATH" +WORKDIR $GOPATH/src/github.com/wtg/shuttletracker +RUN go get -u github.com/kardianos/govendor # ADD ./package.json /app RUN npm install -g bower - -COPY ./bower.json $GOPATH/src/shuttle_tracking_2 +COPY ./bower.json . RUN bower install --allow-root -COPY . $GOPATH/src/shuttle_tracking_2 +COPY . . -RUN go get -RUN go build +RUN govendor sync +RUN go build -o shuttletracker -CMD ["shuttle_tracking_2"] +CMD ["./shuttletracker"] diff --git a/README.md b/README.md index 8d28f4991..311a2b83d 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,28 @@ -Shuttle Tracking v2 -=================== +Shuttle Tracker [![Build Status](https://travis-ci.org/wtg/shuttletracker.svg?branch=master)](https://travis-ci.org/wtg/shuttletracker) [![codecov](https://codecov.io/gh/wtg/shuttletracker/branch/master/graph/badge.svg)](https://codecov.io/gh/wtg/shuttletracker) [![GoDoc](https://godoc.org/github.com/wtg/shuttletracker?status.svg)](https://godoc.org/github.com/wtg/shuttletracker) [![Go Report Card](https://goreportcard.com/badge/github.com/wtg/shuttletracker)](https://goreportcard.com/report/github.com/wtg/shuttletracker) +=============== -Remaking the original [Shuttle Tracker](https://github.com/wtg/shuttle_tracking) using [Go](https://golang.org/), [Polymer Web Components](https://www.polymer-project.org/), and [MongoDB](https://www.mongodb.org/). +Tracking and mapping RPI's shuttles with [Go](https://golang.org/), [Polymer Web Components](https://www.polymer-project.org/), and [MongoDB](https://www.mongodb.org/). -Setting Up ------------------ -1. Clone this repository using `git clone https://github.com/wtg/shuttle_tracking_2` - * Make sure the repository is cloned to a parent directory named `src` -2. Make sure you have npm, bower, golang and mongodb installed - * On Debian-based linux, run `sudo apt-get install nodejs npm golang mongodb` to install npm and go language packages - * On CentOs run `sudo yum install nodejs npm golang mongodb` instead - * Run `sudo npm install -g bower` to install bower -3. Run `bower install` inside shuttle tracking directory to install dependencies listed in bower.json -4. Rename conf.json.sample to conf.json -5. Edit conf.json with the following: - * Data Feed: API with tracking information, this is a unique API info url that we can get data from it. Since it is private, we will only put this on our private group for now (Slacks). - * UpdateInterval: Number of seconds between each request to the data feed - * MongoUrl: Url where MongoDB is located - * MongoPort: Port where MongoDB is bound (default is 27017) -3. Change your gopath to the parent directory or src directory listed on step 1 using `export GOPATH="path-to-directory"` -4. Run `bower install` inside shuttle tracking directory to install dependencies listed in bower.json -5. Run `./goget` (script provided) to install additional dependencies -6. Rename conf.json.sample to conf.json and edit with the following: - * Data Feed: API with tracking information (iTrak in our case), if using the dummy server, http://localhost:8081 - * UpdateInterval: Number of seconds between each request to the data feed - * MongoUrl: Url where MongoDB is located - * MongoPort: Port where MongoDB is bound (default is 27017) -7. Run the app using `go run main.go` in the project root directory -8. Visit http://localhost:8080/ to view the tracking application and http://localhost:8080/admin to view the admin panel +Check it out in action at [shuttles.rpi.edu](https://shuttles.rpi.edu). -More Information +Setting Up ----------------- -For more information please visit the [Wiki page](https://github.com/KeyboardNerd/shuttle_tracking_2/wiki). +1. Install Go +2. `go get github.com/wtg/shuttletracker` +3. `govendor sync` +4. Make sure you have npm, bower, golang and mongodb installed +5. Run `bower install` inside shuttle tracking directory to install dependencies listed in bower.json +6. Rename conf.json.sample to conf.json +7. Edit conf.json with the following: + * Data Feed: API with tracking information, this is a unique API info url that we can get data from it. Since it is private, we will only put this on our private group for now (Slacks). + * UpdateInterval: Number of seconds between each request to the data feed + * MongoUrl: Url where MongoDB is located + * MongoPort: Port where MongoDB is bound (default is 27017) +9. Run `bower install` inside shuttle tracking directory to install dependencies listed in bower.json +10. Rename conf.json.sample to conf.json and edit with the following: + * Data Feed: API with tracking information (iTrak in our case), if using the dummy server, http://localhost:8081 + * UpdateInterval: Number of seconds between each request to the data feed + * MongoUrl: Url where MongoDB is located + * MongoPort: Port where MongoDB is bound (default is 27017) +11. Run the app using `go run main.go` in the project root directory +12. Visit http://localhost:8080/ to view the tracking application and http://localhost:8080/admin to view the admin panel diff --git a/api/api.go b/api/api.go new file mode 100644 index 000000000..efdbf81e5 --- /dev/null +++ b/api/api.go @@ -0,0 +1,176 @@ +package api + +import ( + "encoding/json" + "net/http" + "net/url" + + "github.com/wtg/shuttletracker/database" + "github.com/wtg/shuttletracker/log" + + "fmt" + "github.com/gorilla/mux" + "gopkg.in/cas.v1" + "gopkg.in/mgo.v2/bson" + "strings" +) + +// Configuration holds the settings for connecting to outside resources. +type Config struct { + GoogleMapAPIKey string + GoogleMapMinDistance int + CasURL string `env:"CAS_URL"` + Authenticate bool `env:"AUTHENTICATE" envDefault:"true"` + ListenURL string +} + +// App holds references to Mongo resources. +type API struct { + cfg Config + CasAUTH *cas.Client + CasMEM *cas.MemoryStore + db database.Database + handler http.Handler +} + +// InitApp initializes the application given a config and connects to backends. +// It also seeds any needed information to the database. +func New(cfg Config, db database.Database) (*API, error) { + // Set up CAS authentication + url, err := url.Parse(cfg.CasURL) + if err != nil { + return nil, err + } + var tickets *cas.MemoryStore + + client := cas.NewClient(&cas.Options{ + URL: url, + Store: nil, + }) + + // Create API instance to store database session and collections + api := API{ + cfg: cfg, + CasAUTH: client, + CasMEM: tickets, + db: db, + } + + r := mux.NewRouter() + + // Public + r.HandleFunc("/vehicles", api.VehiclesHandler).Methods("GET") + r.HandleFunc("/updates", api.UpdatesHandler).Methods("GET") + r.HandleFunc("/updates/message", api.UpdateMessageHandler).Methods("GET") + r.HandleFunc("/routes", api.RoutesHandler).Methods("GET") + r.HandleFunc("/stops", api.StopsHandler).Methods("GET") + + // Admin + r.Handle("/admin/", api.CasAUTH.HandleFunc(api.AdminHandler)).Methods("GET") + r.Handle("/admin", api.CasAUTH.HandleFunc(api.AdminHandler)).Methods("GET") + r.Handle("/admin/success/", api.CasAUTH.HandleFunc(api.AdminPageServer)).Methods("GET") + r.Handle("/admin/success", api.CasAUTH.HandleFunc(api.AdminPageServer)).Methods("GET") + r.Handle("/admin/logout/", api.CasAUTH.HandleFunc(api.AdminLogout)).Methods("GET") + r.Handle("/admin/logout", api.CasAUTH.HandleFunc(api.AdminLogout)).Methods("GET") + r.Handle("/vehicles/create", api.CasAUTH.HandleFunc(api.VehiclesCreateHandler)).Methods("POST") + r.Handle("/vehicles/edit", api.CasAUTH.HandleFunc(api.VehiclesEditHandler)).Methods("POST") + r.Handle("/vehicles/{id:[0-9]+}", api.CasAUTH.HandleFunc(api.VehiclesDeleteHandler)).Methods("DELETE") + r.Handle("/routes/create", api.CasAUTH.HandleFunc(api.RoutesCreateHandler)).Methods("POST") + r.Handle("/routes/{id:.+}", api.CasAUTH.HandleFunc(api.RoutesDeleteHandler)).Methods("DELETE") + r.Handle("/stops/create", api.CasAUTH.HandleFunc(api.StopsCreateHandler)).Methods("POST") + r.Handle("/stops/{id:.+}", api.CasAUTH.HandleFunc(api.StopsDeleteHandler)).Methods("DELETE") + //r.HandleFunc("/import", api.ImportHandler).Methods("GET") + + // Legacy routes to support the ancient iOS app + r.HandleFunc("/vehicles/current.js", api.LegacyVehiclesHandler).Methods("GET") + r.HandleFunc("/displays/netlink.js", api.LegacyRoutesHandler).Methods("GET") + + // Static files + r.HandleFunc("/", IndexHandler).Methods("GET") + r.PathPrefix("/bower_components/").Handler(http.StripPrefix("/bower_components/", http.FileServer(http.Dir("bower_components/")))) + r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("static/")))) + + // Serve requests + hand := api.CasAUTH.Handle(r) + api.handler = hand + + return &api, nil +} + +func NewConfig() *Config { + return &Config{ListenURL: "localhost:8080"} +} + +func (api *API) Run() { + if err := http.ListenAndServe(api.cfg.ListenURL, api.handler); err != nil { + log.WithError(err).Error("Unable to serve.") + } +} + +// IndexHandler serves the index page. +func IndexHandler(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "index.html") +} + +type User struct { + Name string +} + +// AdminHandler serves the admin page. +func (api *API) AdminHandler(w http.ResponseWriter, r *http.Request) { + fmt.Printf("%u", api.cfg.Authenticate) + if api.cfg.Authenticate && !cas.IsAuthenticated(r) { + cas.RedirectToLogin(w, r) + return + } else { + valid := false + var users []User + api.db.Users.Find(bson.M{}).All(&users) + for _, u := range users { + if u.Name == strings.ToLower(cas.Username(r)) { + valid = true + } + } + if api.cfg.Authenticate == false { + valid = true + fmt.Printf("not authenticating") + } + if valid { + http.Redirect(w, r, "/admin/success/", 301) + } else { + http.Redirect(w, r, "/admin/logout/", 301) + } + } + +} + +func (api *API) AdminPageServer(w http.ResponseWriter, r *http.Request) { + + if api.cfg.Authenticate && !cas.IsAuthenticated(r) { + http.Redirect(w, r, "/admin/", 301) + return + } else { + http.ServeFile(w, r, "admin.html") + } + +} + +func (api *API) AdminLogout(w http.ResponseWriter, r *http.Request) { + + if cas.IsAuthenticated(r) { + cas.RedirectToLogout(w, r) + } + +} + +// WriteJSON writes the data as JSON. +func WriteJSON(w http.ResponseWriter, data interface{}) error { + w.Header().Set("Content-Type", "application/json") + b, err := json.MarshalIndent(data, "", " ") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return err + } + w.Write(b) + return nil +} diff --git a/tracking/legacy.go b/api/legacy.go similarity index 54% rename from tracking/legacy.go rename to api/legacy.go index 4df095483..200248283 100644 --- a/tracking/legacy.go +++ b/api/legacy.go @@ -1,83 +1,32 @@ -package tracking +package api import ( - "net/http" + "github.com/wtg/shuttletracker/log" + "github.com/wtg/shuttletracker/model" "gopkg.in/mgo.v2/bson" - // log "github.com/Sirupsen/logrus" - "strconv" - "time" "math/big" + "net/http" + "strconv" ) -type LatestPosition struct { - Longitude string `json:"longitude"` - Latitude string `json:"latitude"` - Timestamp time.Time `json:"timestamp"` - Speed float64 `json:"speed"` - Heading int `json:"heading"` - Cardinal string `json:"cardinal_point"` - StatusMessage *string `json:"public_status_message"` // this is a pointer so it defaults to null -} - -type LegacyVehicle struct { - Name string `json:"name"` - ID int `json:"id"` - LatestPosition LatestPosition `json:"latest_position"` - Icon map[string]int `json:"icon"` -} - -type LegacyVehicleContainer struct { - Vehicle LegacyVehicle `json:"vehicle"` -} - -type LegacyCoordinate struct { - Latitude string `json:"latitude"` - Longitude string `json:"longitude"` -} - -type LegacyRoute struct { - Name string `json:"name"` - Width int `json:"width"` - ID big.Int `json:"id"` - Color string `json:"color"` - Coordinates []LegacyCoordinate `json:"coords"` -} - -type LegacyStopRoute struct { - Name string `json:"name"` - ID big.Int `json:"id"` -} - -type LegacyStop struct { - Name string `json:"name"` - Longitude string `json:"longitude"` - Latitude string `json:"latitude"` - ShortName string `json:"short_name"` - Routes []LegacyStopRoute `json:"routes"` -} - -type LegacyRoutesAndStopsContainer struct { - Routes []LegacyRoute `json:"routes"` - Stops []LegacyStop `json:"stops"` -} - -func (App *App) LegacyVehiclesHandler(w http.ResponseWriter, r *http.Request) { +func (App *API) LegacyVehiclesHandler(w http.ResponseWriter, r *http.Request) { // Query all Vehicles - var vehicles []Vehicle - err := App.Vehicles.Find(bson.M{}).All(&vehicles) + var vehicles []model.Vehicle + err := App.db.Vehicles.Find(bson.M{}).All(&vehicles) // Handle errors if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } // Find recent updates for each vehicle - var legacy_vehicles []LegacyVehicleContainer + var legacy_vehicles []model.LegacyVehicleContainer for _, vehicle := range vehicles { - var update VehicleUpdate + var update model.VehicleUpdate // here, huge waste of computational power, you record every shit inside the Updates table and using sort, I don't know what the hell is going on - err := App.Updates.Find(bson.M{"vehicleID": vehicle.VehicleID}).Sort("-created").Limit(1).One(&update) + err := App.db.Updates.Find(bson.M{"vehicleID": vehicle.VehicleID}).Sort("-created").Limit(1).One(&update) if err != nil { + log.WithError(err).Error("Could not fetch vehicle updates.") http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -97,7 +46,7 @@ func (App *App) LegacyVehiclesHandler(w http.ResponseWriter, r *http.Request) { return } - // calculate cardinal direction + // determine cardinal direction cardinal := CardinalDirection(&update.Heading) // legacy app expects vehicle ID to be a number... @@ -107,35 +56,32 @@ func (App *App) LegacyVehiclesHandler(w http.ResponseWriter, r *http.Request) { return } - - latestPosition := LatestPosition{ + latestPosition := model.LatestPosition{ Longitude: update.Lng, - Latitude: update.Lat, - Heading: int(heading), - Cardinal: cardinal, - Speed: speed, + Latitude: update.Lat, + Heading: int(heading), + Cardinal: cardinal, + Speed: speed, Timestamp: update.Created, } - legacy_vehicle := LegacyVehicle{ - Name: vehicle.VehicleName, - ID: vehicleID, + legacy_vehicle := model.LegacyVehicle{ + Name: vehicle.VehicleName, + ID: vehicleID, LatestPosition: latestPosition, - Icon: map[string]int{"id": 1}, + Icon: map[string]int{"id": 1}, } - - - legacy_vehicles = append(legacy_vehicles, LegacyVehicleContainer{Vehicle: legacy_vehicle}) + legacy_vehicles = append(legacy_vehicles, model.LegacyVehicleContainer{Vehicle: legacy_vehicle}) } // Convert updates to JSON WriteJSON(w, legacy_vehicles) } -func (App *App) LegacyRoutesHandler(w http.ResponseWriter, r *http.Request) { +func (App *API) LegacyRoutesHandler(w http.ResponseWriter, r *http.Request) { // Find all routes in database - var routes []Route - err := App.Routes.Find(bson.M{}).All(&routes) + var routes []model.Route + err := App.db.Routes.Find(bson.M{}).All(&routes) // Handle query errors if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -143,32 +89,32 @@ func (App *App) LegacyRoutesHandler(w http.ResponseWriter, r *http.Request) { } // Convert routes to legacy routes - var legacyRoutes []LegacyRoute + var legacyRoutes []model.LegacyRoute for _, route := range routes { // legacy app expects route ID to be a number, so we convert Mongo's base 16 ID to base 10 int var routeID big.Int routeID.SetString(route.ID, 16) // convert coordinates to legacy coordinates - var coordinates []LegacyCoordinate + var coordinates []model.LegacyCoordinate for _, coordinate := range route.Coords { // convert from float to string latitude := strconv.FormatFloat(coordinate.Lat, 'f', 5, 64) longitude := strconv.FormatFloat(coordinate.Lng, 'f', 5, 64) - legacyCoordinate := LegacyCoordinate{ - Latitude: latitude, + legacyCoordinate := model.LegacyCoordinate{ + Latitude: latitude, Longitude: longitude, } coordinates = append(coordinates, legacyCoordinate) } - legacyRoute := LegacyRoute{ - Name: route.Name, - Width: route.Width, - Color: route.Color, - ID: routeID, + legacyRoute := model.LegacyRoute{ + Name: route.Name, + Width: route.Width, + Color: route.Color, + ID: routeID, Coordinates: coordinates, } @@ -176,8 +122,8 @@ func (App *App) LegacyRoutesHandler(w http.ResponseWriter, r *http.Request) { } // Find all stops in databases - var stops []Stop - err = App.Stops.Find(bson.M{}).All(&stops) + var stops []model.Stop + err = App.db.Stops.Find(bson.M{}).All(&stops) // Handle query errors if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -185,7 +131,7 @@ func (App *App) LegacyRoutesHandler(w http.ResponseWriter, r *http.Request) { } // convert stops to legacy stops - var legacyStops []LegacyStop + var legacyStops []model.LegacyStop for _, stop := range stops { // see if this stop has already been created. this should probably use a map for faster lookup, but the data is small. found := false @@ -195,8 +141,8 @@ func (App *App) LegacyRoutesHandler(w http.ResponseWriter, r *http.Request) { // already created, so just append this route to the stop's routes instead of creating a duplicate // get route name - var route Route - err := App.Routes.Find(bson.M{"id": stop.RouteID}).One(&route) + var route model.Route + err := App.db.Routes.Find(bson.M{"id": stop.RouteID}).One(&route) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -206,7 +152,7 @@ func (App *App) LegacyRoutesHandler(w http.ResponseWriter, r *http.Request) { var routeID big.Int routeID.SetString(route.ID, 16) - legacyStopRoute := LegacyStopRoute{Name: route.Name, ID: routeID} + legacyStopRoute := model.LegacyStopRoute{Name: route.Name, ID: routeID} ls.Routes = append(ls.Routes, legacyStopRoute) found = true @@ -222,8 +168,8 @@ func (App *App) LegacyRoutesHandler(w http.ResponseWriter, r *http.Request) { longitude := strconv.FormatFloat(stop.Lng, 'f', 5, 64) // get route name - var route Route - err := App.Routes.Find(bson.M{"id": stop.RouteID}).One(&route) + var route model.Route + err := App.db.Routes.Find(bson.M{"id": stop.RouteID}).One(&route) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -233,24 +179,24 @@ func (App *App) LegacyRoutesHandler(w http.ResponseWriter, r *http.Request) { var routeID big.Int routeID.SetString(route.ID, 16) - legacyStopRoute := LegacyStopRoute{Name: route.Name, ID: routeID} - legacyStopRoutes := []LegacyStopRoute{legacyStopRoute} + legacyStopRoute := model.LegacyStopRoute{Name: route.Name, ID: routeID} + legacyStopRoutes := []model.LegacyStopRoute{legacyStopRoute} - legacyStop := LegacyStop{ - Name: stop.Name, + legacyStop := model.LegacyStop{ + Name: stop.Name, Longitude: longitude, - Latitude: latitude, + Latitude: latitude, ShortName: stop.Name, - Routes: legacyStopRoutes, + Routes: legacyStopRoutes, } legacyStops = append(legacyStops, legacyStop) } // Send to client as JSON - routesAndStops := LegacyRoutesAndStopsContainer{ + routesAndStops := model.LegacyRoutesAndStopsContainer{ Routes: legacyRoutes, - Stops: legacyStops, + Stops: legacyStops, } WriteJSON(w, routesAndStops) } diff --git a/tracking/predict.go b/api/predict.go similarity index 91% rename from tracking/predict.go rename to api/predict.go index 982249f84..38d83c92c 100644 --- a/tracking/predict.go +++ b/api/predict.go @@ -1,4 +1,4 @@ -package tracking +package api // Arrival Time serving what's the time to next N stops for one shuttle import ( @@ -9,14 +9,15 @@ import ( "bytes" + "github.com/wtg/shuttletracker/model" mgo "gopkg.in/mgo.v2" "gopkg.in/mgo.v2/bson" ) // GetArrivalTime is experimental -func GetArrivalTime(update *VehicleUpdate, routes *mgo.Collection, stops *mgo.Collection) string { +func GetArrivalTime(update *model.VehicleUpdate, routes *mgo.Collection, stops *mgo.Collection) string { if i, err := strconv.ParseFloat(update.Speed, 64); i > 5.0 && err == nil { - route := Route{} + route := model.Route{} routes.Find(bson.M{"id": "582f2794e05a0b9c1f2948fa"}).One(&route) // get closest segment x0, err := strconv.ParseFloat(update.Lat, 64) @@ -45,7 +46,7 @@ func GetArrivalTime(update *VehicleUpdate, routes *mgo.Collection, stops *mgo.Co if ShuttleSegment >= 0 && ShuttleSegment < len(route.Duration) { fmt.Printf("ID = %s, Segment = %d (%f, %f), duration = %f\n", update.VehicleID, ShuttleSegment, route.Duration[ShuttleSegment].Start.Latitude, route.Duration[ShuttleSegment].Start.Longitude, route.Duration[ShuttleSegment].Duration) } - allstops := []Stop{} + allstops := []model.Stop{} stops.Find(bson.M{"routeId": route.ID}).All(&allstops) var buffer bytes.Buffer for _, i := range allstops { diff --git a/tracking/routes.go b/api/routes.go similarity index 53% rename from tracking/routes.go rename to api/routes.go index 2947d6c80..fc2054691 100644 --- a/tracking/routes.go +++ b/api/routes.go @@ -1,116 +1,31 @@ -package tracking +package api import ( "bytes" + "database/sql" "encoding/json" + "fmt" + _ "github.com/go-sql-driver/mysql" + "gopkg.in/cas.v1" "math" "net/http" "strconv" "time" - "gopkg.in/cas.v1" - "database/sql" - "fmt" - _ "github.com/go-sql-driver/mysql" log "github.com/Sirupsen/logrus" "github.com/gorilla/mux" "io/ioutil" + "github.com/wtg/shuttletracker/model" "gopkg.in/mgo.v2/bson" ) -// Coord represents a single lat/lng point used to draw routes -type Coord struct { - Lat float64 `json:"lat" bson:"lat"` - Lng float64 `json:"lng" bson:"lng"` -} - -// Route represents a set of coordinates to draw a path on our tracking map -type Route struct { - ID string `json:"id" bson:"id"` - Name string `json:"name" bson:"name"` - Description string `json:"description" bson:"description"` - StartTime string `json:"startTime" bson:"startTime"` - EndTime string `json:"endTime" bson:"endTime"` - Enabled bool `json:"enabled,string" bson:"enabled"` - Color string `json:"color" bson:"color"` - Width int `json:"width,string" bson:"width"` - Coords []Coord `json:"coords" bson:"coords"` - Duration []Segment `json:"duration" bson:"duration"` - StopsID []string `json:"stopsid" bson:"stopsid"` - AvailableRoute int `json:"availableroute" bson:"availableroute"` - Created time.Time `json:"created" bson:"created"` - Updated time.Time `json:"updated" bson:"updated"` -} - -// Stop indicates where a tracked object is scheduled to arrive -type Stop struct { - ID string `json:"id" bson:"id"` - Name string `json:"name" bson:"name"` - Description string `json:"description" bson:"description"` - Lat float64 `json:"lat,string" bson:"lat"` - Lng float64 `json:"lng,string" bson:"lng"` - Address string `json:"address" bson:"address"` - StartTime string `json:"startTime" bson:"startTime"` - EndTime string `json:"endTime" bson:"endTime"` - Enabled bool `json:"enabled,string" bson:"enabled"` - RouteID string `json:"routeId" bson:"routeId"` - SegmentIndex int `json:"segmentindex" bson:"segmentindex"` -} - -type MapPoint struct { - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` -} -type MapResponsePoint struct { - Location MapPoint `json:"location"` - OriginalIndex int `json:"originalIndex,omitempty"` - PlaceID string `json:"placeId"` -} -type MapResponse struct { - SnappedPoints []MapResponsePoint -} - -type MapDistanceMatrixDuration struct { - Value int `json:"value"` - Text string `json:"text"` -} - -type MapDistanceMatrixDistance struct { - Value int `json:"value"` - Text string `json:"text"` -} - -type MapDistanceMatrixElement struct { - Status string `json:"status"` - Duration MapDistanceMatrixDuration `json:"duration"` - Distance MapDistanceMatrixDistance `json:"distance"` -} - -type MapDistanceMatrixElements struct { - Elements []MapDistanceMatrixElement `json:"elements"` -} -type MapDistanceMatrixResponse struct { - Status string `json:"status"` - OriginAddresses []string `json:"origin_addresses"` - DestinationAddresses []string `json:"destination_addresses"` - Rows []MapDistanceMatrixElements `json:"rows"` -} - -type Segment struct { - ID string `json:"id"` - Start MapPoint `json:"origin"` - End MapPoint `json:"destination"` - Distance float64 `json:"distance"` - Duration float64 `json:"duration"` -} - // RoutesHandler finds all of the routes in the database -func (App *App) RoutesHandler(w http.ResponseWriter, r *http.Request) { +func (App *API) RoutesHandler(w http.ResponseWriter, r *http.Request) { // Find all routes in database - var routes []Route - err := App.Routes.Find(bson.M{}).All(&routes) + var routes []model.Route + err := App.db.Routes.Find(bson.M{}).All(&routes) // Handle query errors if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -120,10 +35,10 @@ func (App *App) RoutesHandler(w http.ResponseWriter, r *http.Request) { } // StopsHandler finds all of the route stops in the database -func (App *App) StopsHandler(w http.ResponseWriter, r *http.Request) { +func (App *API) StopsHandler(w http.ResponseWriter, r *http.Request) { // Find all stops in databases - var stops []Stop - err := App.Stops.Find(bson.M{}).All(&stops) + var stops []model.Stop + err := App.db.Stops.Find(bson.M{}).All(&stops) // Handle query errors if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -133,7 +48,7 @@ func (App *App) StopsHandler(w http.ResponseWriter, r *http.Request) { } // Interpolate do interpolation using user input coordinates -func Interpolate(coords []Coord, key string) []Coord { +func Interpolate(coords []model.Coord, key string) []model.Coord { // make request prefix := "https://roads.googleapis.com/v1/snapToRoads?" var buffer bytes.Buffer @@ -162,12 +77,12 @@ func Interpolate(coords []Coord, key string) []Coord { } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) - mapResponse := MapResponse{} + mapResponse := model.MapResponse{} json.Unmarshal(body, &mapResponse) // read response - result := []Coord{} + result := []model.Coord{} for _, location := range mapResponse.SnappedPoints { - currentLocation := Coord{ + currentLocation := model.Coord{ Lat: float64(location.Location.Latitude), Lng: float64(location.Location.Longitude), } @@ -176,7 +91,7 @@ func Interpolate(coords []Coord, key string) []Coord { return result } -func GoogleSegmentCompute(from Coord, to Coord, key string) Segment { +func GoogleSegmentCompute(from model.Coord, to model.Coord, key string) model.Segment { prefix := "https://maps.googleapis.com/maps/api/distancematrix/json?units=imperial&" var buffer bytes.Buffer buffer.WriteString(prefix) @@ -187,19 +102,19 @@ func GoogleSegmentCompute(from Coord, to Coord, key string) Segment { resp, err := http.Get(buffer.String()) if err != nil { fmt.Errorf("Error Not valid response from Google API") - return Segment{} + return model.Segment{} } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) - mapResponse := MapDistanceMatrixResponse{} + mapResponse := model.MapDistanceMatrixResponse{} json.Unmarshal(body, &mapResponse) fmt.Println(mapResponse) - result := Segment{ - Start: MapPoint{ + result := model.Segment{ + Start: model.MapPoint{ Latitude: float64(from.Lat), Longitude: float64(from.Lng), }, - End: MapPoint{ + End: model.MapPoint{ Latitude: float64(to.Lat), Longitude: float64(to.Lng), }, @@ -211,17 +126,17 @@ func GoogleSegmentCompute(from Coord, to Coord, key string) Segment { } // compute distance between two coordinates and return a value -func ComputeDistance(c1 Coord, c2 Coord) float64 { +func ComputeDistance(c1 model.Coord, c2 model.Coord) float64 { return float64(math.Sqrt(math.Pow(c1.Lat-c2.Lat, 2) + math.Pow(c1.Lng-c2.Lng, 2))) } -func ComputeDistanceMapPoint(c1 MapPoint, c2 MapPoint) float64 { +func ComputeDistanceMapPoint(c1 model.MapPoint, c2 model.MapPoint) float64 { return float64(math.Sqrt(math.Pow(c1.Latitude-c2.Latitude, 2) + math.Pow(c1.Longitude-c2.Longitude, 2))) } // Compute the Segment for each segment of the coordinates -func ComputeSegments(coords []Coord, key string, threshold int) []Segment { - result := []Segment{} +func ComputeSegments(coords []model.Coord, key string, threshold int) []model.Segment { + result := []model.Segment{} // only compute the distance greater than some theshold distance and assume all in between has the same Segment prev := 0 index := 1 @@ -230,11 +145,11 @@ func ComputeSegments(coords []Coord, key string, threshold int) []Segment { if index%threshold == 0 { v := GoogleSegmentCompute(coords[prev], coords[index], key) for inner := prev + 1; inner <= index; inner++ { - result = append(result, Segment{ + result = append(result, model.Segment{ Distance: v.Distance / float64(index-prev), Duration: v.Duration / float64(index-prev), - Start: MapPoint{Latitude: float64(coords[inner-1].Lat), Longitude: float64(coords[inner-1].Lng)}, - End: MapPoint{Latitude: float64(coords[inner].Lat), Longitude: float64(coords[inner].Lng)}, + Start: model.MapPoint{Latitude: float64(coords[inner-1].Lat), Longitude: float64(coords[inner-1].Lng)}, + End: model.MapPoint{Latitude: float64(coords[inner].Lat), Longitude: float64(coords[inner].Lng)}, }) } prev = index @@ -244,59 +159,59 @@ func ComputeSegments(coords []Coord, key string, threshold int) []Segment { } //This is really a temporary funcion to import the old database, only supports adding two routes -func (App *App) ImportHandler(w http.ResponseWriter, r *http.Request){ - var count int; - count = 0; - db,err := sql.Open("mysql","root:pass@/shuttle_tracking"); +func (App *API) ImportHandler(w http.ResponseWriter, r *http.Request) { + var count int + count = 0 + db, err := sql.Open("mysql", "root:pass@/shuttle_tracking") //Begin connecting to database - if err != nil{ - log.Fatalf("Couldnt connect to mysql"); + if err != nil { + log.Fatalf("Couldnt connect to mysql") } if err := db.Ping(); err != nil { - log.Fatal(err) + log.Fatal(err) } - if db == nil{ - log.Fatalf("db empty"); + if db == nil { + log.Fatalf("db empty") } //Begin grabbing information we need - rows,err := db.Query("SELECT * FROM coords"); - if(err != nil){ - log.Fatalf("bad query"); + rows, err := db.Query("SELECT * FROM coords") + if err != nil { + log.Fatalf("bad query") } //iterate through rows - coords := []Coord{} - var oldId int; - oldId = 1; + coords := []model.Coord{} + var oldId int + oldId = 1 for rows.Next() { - var id int; - var lat float64; - var long float64; - var position int; - var route_id int; - var created_at string; - var updated_at string; - - if err := rows.Scan(&id,&lat,&long,&position,&route_id,&created_at,&updated_at); err != nil { + var id int + var lat float64 + var long float64 + var position int + var route_id int + var created_at string + var updated_at string + + if err := rows.Scan(&id, &lat, &long, &position, &route_id, &created_at, &updated_at); err != nil { log.Fatal(err) } //We're done with the first route, update it and put it in the database. - if(oldId == 1 && route_id == 2){ - - coords = Interpolate(coords, App.Config.GoogleMapAPIKey) - segments := ComputeSegments(coords, App.Config.GoogleMapAPIKey, App.Config.GoogleMapMinDistance) - - route := db.QueryRow("SELECT name,description,start_time,end_time,color FROM routes where id = 1"); - var name string; - var desc string; - var color string; - var start_time string; - var end_time string; - err := route.Scan(&name,&desc,&start_time,&end_time,&color); - if(err != nil){ - log.Fatal(err); + if oldId == 1 && route_id == 2 { + + coords = Interpolate(coords, App.cfg.GoogleMapAPIKey) + segments := ComputeSegments(coords, App.cfg.GoogleMapAPIKey, App.cfg.GoogleMapMinDistance) + + route := db.QueryRow("SELECT name,description,start_time,end_time,color FROM routes where id = 1") + var name string + var desc string + var color string + var start_time string + var end_time string + err := route.Scan(&name, &desc, &start_time, &end_time, &color) + if err != nil { + log.Fatal(err) } - newRoute := Route{ + newRoute := model.Route{ ID: bson.NewObjectId().Hex(), Name: name, Description: desc, @@ -309,39 +224,39 @@ func (App *App) ImportHandler(w http.ResponseWriter, r *http.Request){ Duration: segments, Created: time.Now(), Updated: time.Now()} - _ = newRoute - fmt.Printf(name) - coords = nil; - err = App.Routes.Insert(&newRoute) - } + _ = newRoute + fmt.Printf(name) + coords = nil + err = App.db.Routes.Insert(&newRoute) + } oldId = route_id - myCoord := Coord{lat, long} - if(route_id == 1){ - coords = append(coords,myCoord); - }else{ - count += 1; - if(count % 10 == 0){ - coords = append(coords,myCoord); + myCoord := model.Coord{lat, long} + if route_id == 1 { + coords = append(coords, myCoord) + } else { + count += 1 + if count%10 == 0 { + coords = append(coords, myCoord) } } } - coords = Interpolate(coords, App.Config.GoogleMapAPIKey) - segments := ComputeSegments(coords, App.Config.GoogleMapAPIKey, App.Config.GoogleMapMinDistance) - - route := db.QueryRow("SELECT name,description,start_time,end_time,color FROM routes where id = 2"); - var name string; - var desc string; - var color string; - var start_time string; - var end_time string; - err = route.Scan(&name,&desc,&start_time,&end_time,&color); - if(err != nil){ - log.Fatal(err); + coords = Interpolate(coords, App.cfg.GoogleMapAPIKey) + segments := ComputeSegments(coords, App.cfg.GoogleMapAPIKey, App.cfg.GoogleMapMinDistance) + + route := db.QueryRow("SELECT name,description,start_time,end_time,color FROM routes where id = 2") + var name string + var desc string + var color string + var start_time string + var end_time string + err = route.Scan(&name, &desc, &start_time, &end_time, &color) + if err != nil { + log.Fatal(err) } - newRoute := Route{ + newRoute := model.Route{ ID: bson.NewObjectId().Hex(), Name: name, Description: desc, @@ -354,17 +269,19 @@ func (App *App) ImportHandler(w http.ResponseWriter, r *http.Request){ Duration: segments, Created: time.Now(), Updated: time.Now()} - _ = newRoute - fmt.Printf(name) - coords = []Coord{}; - err = App.Routes.Insert(&newRoute) + _ = newRoute + fmt.Printf(name) + coords = []model.Coord{} + err = App.db.Routes.Insert(&newRoute) } // RoutesCreateHandler adds a new route to the database -func (App *App) RoutesCreateHandler(w http.ResponseWriter, r *http.Request) { +func (App *API) RoutesCreateHandler(w http.ResponseWriter, r *http.Request) { // Create a new route object using request fields - if(App.Config.Authenticate && !cas.IsAuthenticated(r)){return;} + if App.cfg.Authenticate && !cas.IsAuthenticated(r) { + return + } var routeData map[string]string var coordsData []map[string]float64 // Decode route details @@ -380,14 +297,14 @@ func (App *App) RoutesCreateHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) } // Create a Coord from each set of input coordinates - coords := []Coord{} + coords := []model.Coord{} for _, c := range coordsData { - coord := Coord{c["lat"], c["lng"]} + coord := model.Coord{c["lat"], c["lng"]} coords = append(coords, coord) } // Here do the interpolation - coords = Interpolate(coords, App.Config.GoogleMapAPIKey) - segments := ComputeSegments(coords, App.Config.GoogleMapAPIKey, App.Config.GoogleMapMinDistance) + coords = Interpolate(coords, App.cfg.GoogleMapAPIKey) + segments := ComputeSegments(coords, App.cfg.GoogleMapAPIKey, App.cfg.GoogleMapMinDistance) // now we get the Segment for each segment ( this should be stored in database, just store it inside route for god sake) fmt.Printf("Size of coordinates = %d", len(coords)) @@ -396,7 +313,7 @@ func (App *App) RoutesCreateHandler(w http.ResponseWriter, r *http.Request) { width, _ := strconv.Atoi(routeData["width"]) currentTime := time.Now() // Create a new route - route := Route{ + route := model.Route{ ID: bson.NewObjectId().Hex(), Name: routeData["name"], Description: routeData["description"], @@ -410,7 +327,7 @@ func (App *App) RoutesCreateHandler(w http.ResponseWriter, r *http.Request) { Created: currentTime, Updated: currentTime} // Store new route under routes collection - err = App.Routes.Insert(&route) + err = App.db.Routes.Insert(&route) // Error handling if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -419,13 +336,15 @@ func (App *App) RoutesCreateHandler(w http.ResponseWriter, r *http.Request) { } //Deletes route from database -func (App *App) RoutesDeleteHandler(w http.ResponseWriter, r *http.Request) { - if(App.Config.Authenticate && !cas.IsAuthenticated(r)){return;} +func (App *API) RoutesDeleteHandler(w http.ResponseWriter, r *http.Request) { + if App.cfg.Authenticate && !cas.IsAuthenticated(r) { + return + } vars := mux.Vars(r) - fmt.Printf(vars["id"]); + fmt.Printf(vars["id"]) log.Debugf("deleting", vars["id"]) - err := App.Routes.Remove(bson.M{"id": vars["id"]}); + err := App.db.Routes.Remove(bson.M{"id": vars["id"]}) // Error handling if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -433,7 +352,7 @@ func (App *App) RoutesDeleteHandler(w http.ResponseWriter, r *http.Request) { } // this could be improved -func GetSegment(stop Stop, startIndex int, segments []Segment) int { +func GetSegment(stop model.Stop, startIndex int, segments []model.Segment) int { // choose the segment with lowest distance fmt.Println(len(segments)) x0 := float64(stop.Lat) @@ -456,16 +375,18 @@ func GetSegment(stop Stop, startIndex int, segments []Segment) int { } // StopsCreateHandler adds a new route stop to the database -func (App *App) StopsCreateHandler(w http.ResponseWriter, r *http.Request) { - if(App.Config.Authenticate && !cas.IsAuthenticated(r)){return;} +func (App *API) StopsCreateHandler(w http.ResponseWriter, r *http.Request) { + if App.cfg.Authenticate && !cas.IsAuthenticated(r) { + return + } fmt.Print("Create Stop Handler called") // Create a new stop object using request fields - stop := Stop{} + stop := model.Stop{} err := json.NewDecoder(r.Body).Decode(&stop) stop.ID = bson.NewObjectId().Hex() - route := Route{} - err1 := App.Routes.Find(bson.M{"id": stop.RouteID}).One(&route) + route := model.Route{} + err1 := App.db.Routes.Find(bson.M{"id": stop.RouteID}).One(&route) // Error handling if err1 != nil { @@ -480,7 +401,7 @@ func (App *App) StopsCreateHandler(w http.ResponseWriter, r *http.Request) { stop.SegmentIndex = GetSegment(stop, route.AvailableRoute, route.Duration) // Store new stop under stops collection - err = App.Stops.Insert(&stop) + err = App.db.Stops.Insert(&stop) // Error handling if err != nil { fmt.Println(err.Error()) @@ -489,7 +410,7 @@ func (App *App) StopsCreateHandler(w http.ResponseWriter, r *http.Request) { query := bson.M{"id": stop.RouteID} change := bson.M{"$set": bson.M{"availableroute": stop.SegmentIndex + 1, "stopsid": route.StopsID}} - err = App.Routes.Update(query, change) + err = App.db.Routes.Update(query, change) if err != nil { fmt.Println(err.Error()) } @@ -497,13 +418,15 @@ func (App *App) StopsCreateHandler(w http.ResponseWriter, r *http.Request) { WriteJSON(w, stop) } -func (App *App) StopsDeleteHandler(w http.ResponseWriter, r *http.Request) { - if(App.Config.Authenticate && !cas.IsAuthenticated(r)){return;} +func (App *API) StopsDeleteHandler(w http.ResponseWriter, r *http.Request) { + if App.cfg.Authenticate && !cas.IsAuthenticated(r) { + return + } vars := mux.Vars(r) log.Debugf("deleting", vars["id"]) - fmt.Printf(vars["id"]); - err := App.Stops.Remove(bson.M{"id": vars["id"]}) + fmt.Printf(vars["id"]) + err := App.db.Stops.Remove(bson.M{"id": vars["id"]}) // Error handling if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/api/vehicles.go b/api/vehicles.go new file mode 100644 index 000000000..888e5e551 --- /dev/null +++ b/api/vehicles.go @@ -0,0 +1,180 @@ +package api + +import ( + "encoding/json" + "fmt" + "gopkg.in/cas.v1" + "net/http" + "strconv" + "time" + + "github.com/wtg/shuttletracker/log" + "github.com/wtg/shuttletracker/model" + + "github.com/gorilla/mux" + "gopkg.in/mgo.v2/bson" +) + +var ( + lastUpdate time.Time +) + +// VehiclesHandler finds all the vehicles in the database. +func (api *API) VehiclesHandler(w http.ResponseWriter, r *http.Request) { + + // Find all vehicles in database + var vehicles []model.Vehicle + err := api.db.Vehicles.Find(bson.M{}).All(&vehicles) + // Handle query errors + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Send each vehicle to client as JSON + WriteJSON(w, vehicles) +} + +// VehiclesCreateHandler adds a new vehicle to the database. +func (api *API) VehiclesCreateHandler(w http.ResponseWriter, r *http.Request) { + if api.cfg.Authenticate && !cas.IsAuthenticated(r) { + return + } + + // Create new vehicle object using request fields + vehicle := model.Vehicle{} + vehicle.Created = time.Now() + vehicle.Updated = vehicle.Created + vehicleData := json.NewDecoder(r.Body) + err := vehicleData.Decode(&vehicle) + // Error handling + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + // Store new vehicle under vehicles collection + err = api.db.Vehicles.Insert(&vehicle) + // Error handling + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (api *API) VehiclesEditHandler(w http.ResponseWriter, r *http.Request) { + +} + +func (api *API) VehiclesDeleteHandler(w http.ResponseWriter, r *http.Request) { + if api.cfg.Authenticate && !cas.IsAuthenticated(r) { + return + } + + // Delete vehicle from Vehicles collection + vars := mux.Vars(r) + log.Debugf("deleting", vars["id"]) + err := api.db.Vehicles.Remove(bson.M{"vehicleID": vars["id"]}) + // Error handling + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +// Here's my view, keep every name the same meaning, otherwise, choose another. +// UpdatesHandler get the most recent update for each vehicle in the vehicles collection. +func (App *API) UpdatesHandler(w http.ResponseWriter, r *http.Request) { + // Store updates for each vehicle + var vehicles []model.Vehicle + var updates []model.VehicleUpdate + var update model.VehicleUpdate + var vehicleUpdates []model.VehicleUpdate + // Query all Vehicles + err := App.db.Vehicles.Find(bson.M{}).All(&vehicles) + // Handle errors + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + // Find recent updates for each vehicle + for _, vehicle := range vehicles { + // here, huge waste of computational power, you record every shit inside the Updates table and using sort, I don't know what the hell is going on + err := App.db.Updates.Find(bson.M{"vehicleID": vehicle.VehicleID}).Sort("-created").Limit(20).All(&vehicleUpdates) + update = vehicleUpdates[0] + + if err == nil { + count := 0.0 + speed := 0.0 + for i, elem := range vehicleUpdates { + if time.Since(elem.Created).Minutes() < 5 { + val, _ := strconv.ParseFloat(vehicleUpdates[i].Speed, 64) + speed += val + count += 1 + } + } + if count > 0 && speed/count > 5 { + updates = append(updates, update) + } + } + } + + // Convert updates to JSON + WriteJSON(w, updates) // it's good to take some REST in our server :) +} + +// UpdateMessageHandler generates a message about an update for a vehicle +func (App *API) UpdateMessageHandler(w http.ResponseWriter, r *http.Request) { + // For each vehicle/update, store message as a string + var messages []string + var message string + var vehicles []model.Vehicle + var update model.VehicleUpdate + + // Query all Vehicles + err := App.db.Vehicles.Find(bson.M{}).All(&vehicles) + // Handle errors + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + // Find recent updates and generate message + for _, vehicle := range vehicles { + // find 10 most recent records + err := App.db.Updates.Find(bson.M{"vehicleID": vehicle.VehicleID}).Sort("-created").Limit(1).One(&update) + if err == nil { + // Use first 4 char substring of update.Speed + speed := update.Speed + if len(speed) > 4 { + speed = speed[0:4] + } + //nextArrival := GetArrivalTime(&update, App.Routes, App.Stops) + message = fmt.Sprintf("%s
Traveling %s at
%s mph as of %s", vehicle.VehicleName, CardinalDirection(&update.Heading), speed, lastUpdate.Format("3:04:05pm") /*, nextArrival*/) + messages = append(messages, message) + } + } + // Convert to JSON + WriteJSON(w, messages) +} + +// CardinalDirection returns the cardinal direction of a vehicle's heading. +func CardinalDirection(h *string) string { + heading, err := strconv.ParseFloat(*h, 64) + if err != nil { + fmt.Println("ERROR", err.Error()) + return "North" + } + switch { + case (heading >= 22.5 && heading < 67.5): + return "North-East" + case (heading >= 67.5 && heading < 112.5): + return "East" + case (heading >= 112.5 && heading < 157.5): + return "South-East" + case (heading >= 157.5 && heading < 202.5): + return "South" + case (heading >= 202.5 && heading < 247.5): + return "South-West" + case (heading >= 247.5 && heading < 292.5): + return "West" + case (heading >= 292.5 && heading < 337.5): + return "North-West" + default: + return "North" + } +} diff --git a/api/vehicles_test.go b/api/vehicles_test.go new file mode 100644 index 000000000..859cd2842 --- /dev/null +++ b/api/vehicles_test.go @@ -0,0 +1,25 @@ +package api + +import "testing" + +func TestCardinalDirection(t *testing.T) { + table := [][]string{ + {"0", "North"}, + {"45", "North-East"}, + {"90", "East"}, + {"135", "South-East"}, + {"180", "South"}, + {"225", "South-West"}, + {"270", "West"}, + {"315", "North-West"}, + {"this isn't a direction lol", "North"}, + } + + for _, testCase := range table { + direction := CardinalDirection(&testCase[0]) + expected := testCase[1] + if direction != expected { + t.Errorf("Got %v, expected %v.", direction, expected) + } + } +} diff --git a/cmd/runner.go b/cmd/runner.go new file mode 100644 index 000000000..3e0158221 --- /dev/null +++ b/cmd/runner.go @@ -0,0 +1,41 @@ +package cmd + +import ( + "github.com/wtg/shuttletracker/log" + "sync" +) + +type Runnable interface { + Run() +} + +type Runner struct { + runnables []Runnable +} + +func NewRunner() *Runner { + runner := &Runner{runnables: []Runnable{}} + return runner +} + +func (r *Runner) Add(runnable Runnable) { + r.runnables = append(r.runnables, runnable) +} + +// Run runs all added runnables concurrently. +func (r *Runner) Run() { + wg := sync.WaitGroup{} + for i := range r.runnables { + // We have to create a new runnable var on each iteration of the loop + // to ensure that the goroutine we spawn runs the correct runnable. + runnable := r.runnables[i] + wg.Add(1) + go func() { + runnable.Run() + wg.Done() + }() + } + log.Debug("Runnables started.") + wg.Wait() + log.Debug("Runnables exited.") +} diff --git a/cmd/shuttletracker.go b/cmd/shuttletracker.go new file mode 100644 index 000000000..fb08f51d6 --- /dev/null +++ b/cmd/shuttletracker.go @@ -0,0 +1,54 @@ +// Package cmd bundles together all of shuttletracker's subpackages +// to create, configure, and run the shuttle tracker. +package cmd + +import ( + "github.com/wtg/shuttletracker/api" + "github.com/wtg/shuttletracker/config" + "github.com/wtg/shuttletracker/database" + "github.com/wtg/shuttletracker/log" + "github.com/wtg/shuttletracker/updater" +) + +// Run starts the shuttle tracker and blocks forever. +func Run() { + log.Info("Shuttle Tracker starting...") + + // Config + cfg, err := config.New() + if err != nil { + log.WithError(err).Error("Could not create config.") + return + } + + runner := NewRunner() + + // Log + log.SetLevel(cfg.Log.Level) + + // Database + db, err := database.New(*cfg.Database) + if err != nil { + log.WithError(err).Errorf("MongoDB connection to \"%v\" failed.", cfg.Database.MongoURL) + return + } + + // Make shuttle position updater + updater, err := updater.New(*cfg.Updater, *db) + if err != nil { + log.WithError(err).Error("Could not create updater.") + return + } + runner.Add(updater) + + // Make API server + api, err := api.New(*cfg.API, *db) + if err != nil { + log.WithError(err).Error("Could not create API server.") + return + } + runner.Add(api) + + // Run all runnables + runner.Run() +} diff --git a/conf.json.sample b/conf.json.sample index 93e665da6..ea6d06bee 100644 --- a/conf.json.sample +++ b/conf.json.sample @@ -1,7 +1,20 @@ { - "DataFeed": "", - "UpdateInterval": 15, - "MongoURL": "localhost", - "MongoPort": "27017" - "Authenticate": false; + "Updater": { + "DataFeed": "", + "UpdateInterval": "3s" + }, + "API": { + "GoogleMapAPIKey": "", + "GoogleMapMinDistance": 1, + "CasURL": "https://cas-auth.rpi.edu/cas/", + "Authenticate": true + }, + "Database": { + }, + "Log": { + "Level": "debug" + }, + "Server": { + "ListenURL": "localhost:8080" + } } diff --git a/config/config.go b/config/config.go new file mode 100644 index 000000000..2c96c5f12 --- /dev/null +++ b/config/config.go @@ -0,0 +1,40 @@ +package config + +import ( + "github.com/wtg/shuttletracker/api" + "github.com/wtg/shuttletracker/database" + "github.com/wtg/shuttletracker/log" + "github.com/wtg/shuttletracker/updater" + + "github.com/spf13/viper" +) + +// Global configuration struct. +type Config struct { + Database *database.Config + Updater *updater.Config + API *api.Config + Log *log.Config +} + +// Create a new, global Config. Reads in configuration from config files. +func New() (*Config, error) { + cfg := &Config{ + Database: database.NewConfig(), + Updater: updater.NewConfig(), + API: api.NewConfig(), + Log: log.NewConfig(), + } + + viper.SetConfigName("conf") + viper.AddConfigPath(".") + if err := viper.ReadInConfig(); err != nil { + return nil, err + } + + if err := viper.Unmarshal(&cfg); err != nil { + return nil, err + } + + return cfg, nil +} diff --git a/database/database.go b/database/database.go new file mode 100644 index 000000000..e5a6ff07b --- /dev/null +++ b/database/database.go @@ -0,0 +1,52 @@ +package database + +import ( + "gopkg.in/mgo.v2" +) + +type Database struct { + session *mgo.Session + Updates *mgo.Collection + Vehicles *mgo.Collection + Routes *mgo.Collection + Stops *mgo.Collection + Users *mgo.Collection +} + +type Config struct { + MongoURL string +} + +func New(cfg Config) (*Database, error) { + db := &Database{} + + session, err := mgo.Dial(cfg.MongoURL) + if err != nil { + return nil, err + } + db.session = session + + db.Updates = db.session.DB("").C("updates") + db.Vehicles = db.session.DB("").C("vehicles") + db.Routes = db.session.DB("").C("routes") + db.Stops = db.session.DB("").C("stops") + db.Users = db.session.DB("").C("users") + + // Ensure unique vehicle identification + vehicleIndex := mgo.Index{ + Key: []string{"vehicleID"}, + Unique: true, + DropDups: true} + db.Vehicles.EnsureIndex(vehicleIndex) + + // Create index on update create time to quickly find the most recent updates + db.Updates.EnsureIndexKey("created") + + return db, nil +} + +func NewConfig() *Config { + return &Config{ + MongoURL: "localhost:27017", + } +} diff --git a/goget b/goget deleted file mode 100755 index 3d03253e3..000000000 --- a/goget +++ /dev/null @@ -1,5 +0,0 @@ -go get github.com/Sirupsen/logrus -go get github.com/gorilla/mux -go get gopkg.in/mgo.v2 -go get gopkg.in/mgo.v2/bson -go get github.com/go-sql-drive/mysql diff --git a/gopath.sh b/gopath.sh deleted file mode 100644 index 8e8fd24a4..000000000 --- a/gopath.sh +++ /dev/null @@ -1,2 +0,0 @@ -export GOPATH=/home/jlyon1/shuttle_tracking2/ - diff --git a/log/log.go b/log/log.go new file mode 100644 index 000000000..c36e48f47 --- /dev/null +++ b/log/log.go @@ -0,0 +1,119 @@ +package log + +import ( + "github.com/Sirupsen/logrus" + "path" + "runtime" + "strings" +) + +var ( + logger *logrus.Logger +) + +type Config struct { + Level string +} + +type Fields map[string]interface{} + +func init() { + logger = logrus.New() +} + +func NewConfig() *Config { + return &Config{ + Level: "info", + } +} + +func SetLevel(level string) { + parsed, err := logrus.ParseLevel(level) + if err != nil { + Error(err) + return + } + logger.Level = parsed +} + +func contextFields(lvl ...int) Fields { + level := 2 + if len(lvl) == 1 { + level = lvl[0] + } + pc, file, line, _ := runtime.Caller(level) + _, fileName := path.Split(file) + parts := strings.Split(runtime.FuncForPC(pc).Name(), ".") + pl := len(parts) + packageName := "" + + if len(parts) >= 0 && pl-2 < len(parts) { + if parts[pl-2][0] == '(' { + packageName = strings.Join(parts[0:pl-2], ".") + } else { + packageName = strings.Join(parts[0:pl-1], ".") + } + + pkgs := strings.Split(packageName, "/shuttletracker/") + if len(pkgs) > 1 { + packageName = pkgs[1] + } + } + + return Fields{ + "package": packageName, + "file": fileName, + "line": line, + } +} + +func WithField(f string, v interface{}) *logrus.Entry { + return logger.WithField(f, v) +} + +func WithFields(f ...Fields) *logrus.Entry { + if len(f) == 0 { + return logger.WithFields(logrus.Fields{}) + } + e := logrus.NewEntry(logger) + for i := 0; i < len(f); i++ { + e = e.WithFields(logrus.Fields(f[i])) + } + return e +} + +func WithError(err error) *logrus.Entry { + return WithFields(contextFields()).WithField("error", err) +} + +func Error(args ...interface{}) { + WithFields(contextFields()).Error(args...) +} + +func Errorf(format string, args ...interface{}) { + WithFields(contextFields()).Errorf(format, args...) +} + +func Warn(args ...interface{}) { + WithFields(contextFields()).Warn(args...) +} + +func Warnf(format string, args ...interface{}) { + WithFields(contextFields()).Warnf(format, args...) +} + +func Info(args ...interface{}) { + WithFields(contextFields()).Info(args...) +} + +func Infof(format string, args ...interface{}) { + WithFields(contextFields()).Infof(format, args...) +} + +func Debug(args ...interface{}) { + WithFields(contextFields()).Debug(args...) +} + +func Debugf(format string, args ...interface{}) { + WithFields(contextFields()).Debugf(format, args...) +} diff --git a/main.go b/main.go index 61207770f..2ca56c99b 100644 --- a/main.go +++ b/main.go @@ -1,122 +1,11 @@ +// Package shuttletracker displays the positions of RPI's shuttles on a map. +// See https://shuttles.rpi.edu and https://github.com/wtg/shuttletracker for more information. package main import ( - "net/http" - "shuttle_tracking_2/tracking" - - "gopkg.in/cas.v1" - "strings" - "fmt" - - log "github.com/Sirupsen/logrus" - "github.com/gorilla/mux" - "gopkg.in/mgo.v2/bson" -) -var users []User - -type User struct{ - Name string -} - -var ( - // Config holds the global app settings. - Config = tracking.InitConfig() - // App holds the application itself. - App = tracking.InitApp(Config) + "github.com/wtg/shuttletracker/cmd" ) -// IndexHandler serves the index page. -func IndexHandler(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, "index.html") -} - -// AdminHandler serves the admin page. -func AdminHandler(w http.ResponseWriter, r *http.Request) { - fmt.Printf("%u",App.Config.Authenticate); - if App.Config.Authenticate && !cas.IsAuthenticated(r) { - cas.RedirectToLogin(w, r) - return - }else{ - valid := false; - for _, u := range users{ - if(u.Name == strings.ToLower(cas.Username(r))){ - valid = true; - } - } - if(App.Config.Authenticate == false){ - valid = true; - fmt.Printf("not authenticating"); - } - if valid{ - http.Redirect(w,r,"/admin/success/",301); - }else{ - http.Redirect(w,r,"/admin/logout/",301); - } - } - -} - -func AdminPageServer(w http.ResponseWriter, r *http.Request) { - - if App.Config.Authenticate && !cas.IsAuthenticated(r) { - http.Redirect(w,r,"/admin/",301); - return - }else{ - http.ServeFile(w, r, "admin.html") - } - -} - -func AdminLogout(w http.ResponseWriter, r *http.Request) { - - if cas.IsAuthenticated(r){ - cas.RedirectToLogout(w,r); - } - -} - func main() { - // close Mongo session when server terminates - defer App.Session.Close() - - - err := App.Users.Find(bson.M{}).All(&users) - _ = err; - - // Start auto updater - go App.UpdateShuttles(Config.DataFeed, Config.UpdateInterval) - // Routing - r := mux.NewRouter() - r.HandleFunc("/", IndexHandler).Methods("GET") - r.Handle("/admin/", App.CasAUTH.HandleFunc(AdminHandler)).Methods("GET") - r.Handle("/admin", App.CasAUTH.HandleFunc(AdminHandler)).Methods("GET") - r.Handle("/admin/success/", App.CasAUTH.HandleFunc(AdminPageServer)).Methods("GET") - r.Handle("/admin/success", App.CasAUTH.HandleFunc(AdminPageServer)).Methods("GET") - r.Handle("/admin/logout/", App.CasAUTH.HandleFunc(AdminLogout)).Methods("GET") - r.Handle("/admin/logout", App.CasAUTH.HandleFunc(AdminLogout)).Methods("GET") - //Has to do with r not being a client? - r.HandleFunc("/vehicles", App.VehiclesHandler).Methods("GET") - r.Handle("/vehicles/create", App.CasAUTH.HandleFunc(App.VehiclesCreateHandler)).Methods("POST") - r.Handle("/vehicles/edit", App.CasAUTH.HandleFunc(App.VehiclesEditHandler)).Methods("POST") - r.Handle("/vehicles/{id:[0-9]+}", App.CasAUTH.HandleFunc(App.VehiclesDeleteHandler)).Methods("DELETE") - r.HandleFunc("/updates", App.UpdatesHandler).Methods("GET") - r.HandleFunc("/updates/message", App.UpdateMessageHandler).Methods("GET") - r.HandleFunc("/routes", App.RoutesHandler).Methods("GET") - r.Handle("/routes/create", App.CasAUTH.HandleFunc(App.RoutesCreateHandler)).Methods("POST") - r.Handle("/routes/{id:.+}", App.CasAUTH.HandleFunc(App.RoutesDeleteHandler)).Methods("DELETE") - r.HandleFunc("/stops", App.StopsHandler).Methods("GET") - r.Handle("/stops/create", App.CasAUTH.HandleFunc(App.StopsCreateHandler)).Methods("POST") - r.Handle("/stops/{id:.+}", App.CasAUTH.HandleFunc(App.StopsDeleteHandler)).Methods("DELETE") - //r.HandleFunc("/import", App.ImportHandler).Methods("GET") - // Legacy routes to support the ancient iOS app - r.HandleFunc("/vehicles/current.js", App.LegacyVehiclesHandler).Methods("GET") - r.HandleFunc("/displays/netlink.js", App.LegacyRoutesHandler).Methods("GET") - // Static files - r.PathPrefix("/bower_components/").Handler(http.StripPrefix("/bower_components/", http.FileServer(http.Dir("bower_components/")))) - r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("static/")))) - // Serve requests - hand := App.CasAUTH.Handle(r) - if err := http.ListenAndServe(":8080", hand); err != nil { - log.Fatalf("Unable to ListenAndServe: %v", err) - } + cmd.Run() } diff --git a/migration/database.go b/migration/database.go deleted file mode 100644 index ebe4b1260..000000000 --- a/migration/database.go +++ /dev/null @@ -1,62 +0,0 @@ -package migration - -import ( - tracking "shuttle_tracking_2/tracking" - "time" - - mgo "gopkg.in/mgo.v2" - "gopkg.in/mgo.v2/bson" -) - -// represent the current location of a shuttle -type ShuttleCoordinate struct { - ID string `json:"id"` -} - -func Snap(R *mgo.Collection, S *mgo.Collection, C *mgo.Collection) { - // conver the Route points into segment information - var routes []tracking.Route - err := R.Find(bson.M{}).All(routes) - if err != nil { - return - } - - // insert the stop location into Route - var stops []tracking.Stop - err = S.Find(bson.M{}).All(&stops) - if err != nil { - return - } - for _, stop := range stops { - route := RouteNode{ - ID: string(bson.NewObjectId()), - Coordinate: Coord{Lat: stop.Lat, Lng: stop.Lng}, - Created: time.Now(), - RouteID: stop.RouteID, - RouteOrder: 0, - StopID: stop.ID, - } - C.Insert(&route) - } - -} - -// V will use information in Coord to generate Speed using Google API -func V(Coord *mgo.Collection, Velocity *mgo.Collection) { - -} - -// calculate the distance between any forward distance between two coordinates -func Distance() { - -} - -// calculate the time distance between any two points between two coordinates -func TimeDistance() { - -} - -// given a list of coordinates and a time T and a future time T', get a set of Coordinates that the shuttle could potentially arrive at. -func TravelDistance() { - -} diff --git a/migration/schema.go b/migration/schema.go deleted file mode 100644 index aa0b140e5..000000000 --- a/migration/schema.go +++ /dev/null @@ -1,67 +0,0 @@ -package migration - -import "time" - -type Coord struct { - Lat float64 `json:"lat" bson:"lat"` - Lng float64 `json:"lng" bson:"lng"` -} - -// represent a node on the graph -type RouteNode struct { - ID string `json:"id"` - Coordinate Coord `json:"coord"` - Created time.Time `json:"created"` - RouteID string `json:"routeid"` - RouteOrder int `json:"routeorder"` // this increments with 100 for better resolution of interpolation - StopID string `json:"stopid"` -} - -// represent a edge on the graph -type RouteEdge struct { - ID string `json:"id"` - Distance float64 `json:"distance"` - Velocity float64 `json:"velocity"` - StartID string `json:"start"` - EndID string `json:"end"` - Time time.Time `json:"time"` -} - -type ShuttleNode struct { - ID string `json:"id"` - ShuttleID string `json:"shuttleid"` - Coordinate Coord `json:"coord"` -} - -// Vehicle represents an object being tracked. -type ShuttleInfo struct { - ID string `json:"id" bson:"vehicleID,omitempty"` - Name string `json:"name" bson:"vehicleName"` - Created time.Time `bson:"created"` - Updated time.Time `bson:"updated"` -} - -// Route represent the information attached to a route -type RouteInfo struct { - ID string `json:"id" bson:"id"` - Name string `json:"name" bson:"name"` - Description string `json:"description" bson:"description"` - StartTime string `json:"startTime" bson:"startTime"` - EndTime string `json:"endTime" bson:"endTime"` - Enabled bool `json:"enabled,string" bson:"enabled"` - Color string `json:"color" bson:"color"` - Width int `json:"width,string" bson:"width"` - Created time.Time `json:"created" bson:"created"` - Updated time.Time `json:"updated" bson:"updated"` -} - -// Stop indicates where a tracked object is scheduled to arrive -type StopInfo struct { - ID string `json:"id" bson:"id"` - Name string `json:"name" bson:"name"` - Description string `json:"description" bson:"description"` - Address string `json:"address" bson:"address"` - StartTime string `json:"startTime" bson:"startTime"` - EndTime string `json:"endTime" bson:"endTime"` - Enabled bool `json:"enabled,string" bson:"enabled"` -} diff --git a/model/model.go b/model/model.go new file mode 100644 index 000000000..bd13bf201 --- /dev/null +++ b/model/model.go @@ -0,0 +1,177 @@ +// Package model provides structs used by multiple packages within shuttletracker. +package model + +import ( + "math/big" + "time" +) + +// VehicleUpdate represents a single position observed for a Vehicle. +type VehicleUpdate struct { + VehicleID string `json:"vehicleID" bson:"vehicleID,omitempty"` + Lat string `json:"lat" bson:"lat"` + Lng string `json:"lng" bson:"lng"` + Heading string `json:"heading" bson:"heading"` + Speed string `json:"speed" bson:"speed"` + Lock string `json:"lock" bson:"lock"` + Time string `json:"time" bson:"time"` + Date string `json:"date" bson:"date"` + Status string `json:"status" bson:"status"` + Created time.Time `json:"created" bson:"created"` + Segment string `json:"segment" bson:"segment"` // the segment that a vehicle resides on +} + +// Vehicle represents an object being tracked. +type Vehicle struct { + VehicleID string `json:"vehicleID" bson:"vehicleID,omitempty"` + VehicleName string `json:"vehicleName" bson:"vehicleName"` + Created time.Time `bson:"created"` + Updated time.Time `bson:"updated"` + Active bool `json:"active"` +} + +// Status contains a detailed message on the tracked object's status. +type Status struct { + Public bool `bson:"public"` + Message string `json:"message" bson:"message"` + Created time.Time `bson:"created"` + Updated time.Time `bson:"updated"` +} + +type LatestPosition struct { + Longitude string `json:"longitude"` + Latitude string `json:"latitude"` + Timestamp time.Time `json:"timestamp"` + Speed float64 `json:"speed"` + Heading int `json:"heading"` + Cardinal string `json:"cardinal_point"` + StatusMessage *string `json:"public_status_message"` // this is a pointer so it defaults to null +} + +type LegacyVehicle struct { + Name string `json:"name"` + ID int `json:"id"` + LatestPosition LatestPosition `json:"latest_position"` + Icon map[string]int `json:"icon"` +} + +type LegacyVehicleContainer struct { + Vehicle LegacyVehicle `json:"vehicle"` +} + +type LegacyCoordinate struct { + Latitude string `json:"latitude"` + Longitude string `json:"longitude"` +} + +type LegacyRoute struct { + Name string `json:"name"` + Width int `json:"width"` + ID big.Int `json:"id"` + Color string `json:"color"` + Coordinates []LegacyCoordinate `json:"coords"` +} + +type LegacyStopRoute struct { + Name string `json:"name"` + ID big.Int `json:"id"` +} + +type LegacyStop struct { + Name string `json:"name"` + Longitude string `json:"longitude"` + Latitude string `json:"latitude"` + ShortName string `json:"short_name"` + Routes []LegacyStopRoute `json:"routes"` +} + +type LegacyRoutesAndStopsContainer struct { + Routes []LegacyRoute `json:"routes"` + Stops []LegacyStop `json:"stops"` +} + +// Coord represents a single lat/lng point used to draw routes +type Coord struct { + Lat float64 `json:"lat" bson:"lat"` + Lng float64 `json:"lng" bson:"lng"` +} + +// Route represents a set of coordinates to draw a path on our tracking map +type Route struct { + ID string `json:"id" bson:"id"` + Name string `json:"name" bson:"name"` + Description string `json:"description" bson:"description"` + StartTime string `json:"startTime" bson:"startTime"` + EndTime string `json:"endTime" bson:"endTime"` + Enabled bool `json:"enabled,string" bson:"enabled"` + Color string `json:"color" bson:"color"` + Width int `json:"width,string" bson:"width"` + Coords []Coord `json:"coords" bson:"coords"` + Duration []Segment `json:"duration" bson:"duration"` + StopsID []string `json:"stopsid" bson:"stopsid"` + AvailableRoute int `json:"availableroute" bson:"availableroute"` + Created time.Time `json:"created" bson:"created"` + Updated time.Time `json:"updated" bson:"updated"` +} + +// Stop indicates where a tracked object is scheduled to arrive +type Stop struct { + ID string `json:"id" bson:"id"` + Name string `json:"name" bson:"name"` + Description string `json:"description" bson:"description"` + Lat float64 `json:"lat,string" bson:"lat"` + Lng float64 `json:"lng,string" bson:"lng"` + Address string `json:"address" bson:"address"` + StartTime string `json:"startTime" bson:"startTime"` + EndTime string `json:"endTime" bson:"endTime"` + Enabled bool `json:"enabled,string" bson:"enabled"` + RouteID string `json:"routeId" bson:"routeId"` + SegmentIndex int `json:"segmentindex" bson:"segmentindex"` +} + +type MapPoint struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` +} +type MapResponsePoint struct { + Location MapPoint `json:"location"` + OriginalIndex int `json:"originalIndex,omitempty"` + PlaceID string `json:"placeId"` +} +type MapResponse struct { + SnappedPoints []MapResponsePoint +} + +type MapDistanceMatrixDuration struct { + Value int `json:"value"` + Text string `json:"text"` +} + +type MapDistanceMatrixDistance struct { + Value int `json:"value"` + Text string `json:"text"` +} + +type MapDistanceMatrixElement struct { + Status string `json:"status"` + Duration MapDistanceMatrixDuration `json:"duration"` + Distance MapDistanceMatrixDistance `json:"distance"` +} + +type MapDistanceMatrixElements struct { + Elements []MapDistanceMatrixElement `json:"elements"` +} +type MapDistanceMatrixResponse struct { + Status string `json:"status"` + OriginAddresses []string `json:"origin_addresses"` + DestinationAddresses []string `json:"destination_addresses"` + Rows []MapDistanceMatrixElements `json:"rows"` +} + +type Segment struct { + ID string `json:"id"` + Start MapPoint `json:"origin"` + End MapPoint `json:"destination"` + Distance float64 `json:"distance"` + Duration float64 `json:"duration"` +} diff --git a/prediction/predictor/statisticalpredictor.go b/prediction/predictor/statisticalpredictor.go deleted file mode 100644 index 796dca0c9..000000000 --- a/prediction/predictor/statisticalpredictor.go +++ /dev/null @@ -1,25 +0,0 @@ -package predictor -import schedule -// Stores a simple interface to access table of arrival times stored in database -type StatisticalPredictor struct{ - History *mgo.Collection // historical data provided - Velocity *mgo.Collection // velocity data for each segment of a route -} - -func (this StatisticalPredictor) getNextN(StopID []string, CurrentTime time.Time , NextN int) []schedule.ArrivalTime{ - // limit N <= 5 -} - -// Get prediction for shuttle arrive at the next N stops -func (this StatisticalPredictor) getNextN(ShuttleID []string, CurrentLocation Coord, CurrentTime time.Time, NextN int) []schedule.ShuttleArrivalTime{ - -} - -type RouteVelocity struct{ - ID string `json:"id"` - CoordID string `json:"coordID"` - Velocity Coord `json:"velocity"` - StartTime Time.time `json:"starttime"` - EndTime Time.time `json:"endtime"` - // if no time within threshold is found, then use the closest one -} diff --git a/prediction/predictor/tablepredictor.go b/prediction/predictor/tablepredictor.go deleted file mode 100644 index 72fee0e73..000000000 --- a/prediction/predictor/tablepredictor.go +++ /dev/null @@ -1,22 +0,0 @@ -package predictor -import schedule -// Stores a simple interface to access table of arrival times stored in database -type TablePredictor struct{ - Table *mgo.Collection -} - -// query timetable to get the next N arrival time by select time by stopid with end date > current date > start date, time > current time, weekday = current weekday; -// limit: NextN should be less than 5 and can go beyond only one day ( check if the result is less than the requested result length) -func (this TablePredictor) getNextN(StopID []string, CurrentTime time.Time , NextN int) []schedule.ArrivalTime{ - -} - -type TimeTable struct{ - ID string `json:"id"` - StopID string `json:"stopid"` - StartDate time.Time `json:"startdate"` - EndDate time.Time `json:"enddate"` - Time time.Time `json:"time"` - WeekDay int `json:"weekday"` -} - diff --git a/prediction/schedule.go b/prediction/schedule.go deleted file mode 100644 index a98674d55..000000000 --- a/prediction/schedule.go +++ /dev/null @@ -1,30 +0,0 @@ -package schedule - -import "time" - -// Struct for arrival time for next N shuttles -// stores data for only 1 shuttle -type ShuttleArrivalTime struct{ - ID string `json:"id"` - ShuttleID string `json:"shuttleid"` - Created time.Time `json:"created"` - Arrival []ArrivalTime `json:"arrival"` -} - -// Struct for shuttle arrival time for next stop -type ArrivalTime struct { - ID string `json:"id"` - StopID string `json:"stopid"` - Created time.Time `json:"created"` - Arrival []time.Time `json:"arrival` -} - -// Interface for generating/formatting ArrivalTime -type ArrivalPredictor interface { - getNextN(StopID []string, CurrentTime time.Time, NextN int) []ArrivalTime -} - -type ShuttleArrivalPredictor interface{ - getNextN(StopID []string, CurrentTime time.Time, NextN int) []ShuttleArrivalTime -} - diff --git a/seed/vehicle_seed.json b/seed/vehicle_seed.json deleted file mode 100644 index b2228a9c8..000000000 --- a/seed/vehicle_seed.json +++ /dev/null @@ -1,20 +0,0 @@ -{"Vehicles":[ - {"VehicleID": "1831394663", - "VehicleName": "Bus 93"}, - {"VehicleID": "1831394755", - "VehicleName": "Bus 85"}, - {"VehicleID": "1831394753", - "VehicleName": "Bus 95"}, - {"VehicleID": "1831394614", - "VehicleName": "Bus 94"}, - {"VehicleID": "1831394611", - "VehicleName": "Bus 91"}, - {"VehicleID": "1632187564", - "VehicleName": "Bus 97"}, - {"VehicleID": "1831144491", - "VehicleName": "Bus 90"}, - {"VehicleID": "1831144492", - "VehicleName": "Bus 92"}, - {"VehicleID": "1831394595", - "VehicleName": "Bus 96"} -]} diff --git a/test.sh b/test.sh new file mode 100755 index 000000000..34dbbfb31 --- /dev/null +++ b/test.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -e +echo "" > coverage.txt + +for d in $(go list ./... | grep -v vendor); do + go test -race -coverprofile=profile.out -covermode=atomic $d + if [ -f profile.out ]; then + cat profile.out >> coverage.txt + rm profile.out + fi +done diff --git a/tracking/app.go b/tracking/app.go deleted file mode 100644 index 850536dc2..000000000 --- a/tracking/app.go +++ /dev/null @@ -1,171 +0,0 @@ -package tracking - -import ( - "encoding/json" - "net/http" - "net/url" - "os" - "time" - - log "github.com/Sirupsen/logrus" - "github.com/caarlos0/env" - "gopkg.in/cas.v1" - "gopkg.in/mgo.v2" -) - -// Configuration holds the settings for connecting to outside resources. -type Configuration struct { - DataFeed string `env:"DATA_FEED"` - UpdateInterval int `env:"UPDATE_INTERVAL" envDefault:"15"` - MongoURL string `env:"MONGO_URL" envDefault:"localhost:27017"` - GoogleMapAPIKey string - GoogleMapMinDistance int - CasURL string `env:"CAS_URL"` - Authenticate bool `env:"AUTHENTICATE" envDefault:"true"` -} - -// App holds references to Mongo resources. -type App struct { - Config *Configuration - Session *mgo.Session - Updates *mgo.Collection - Vehicles *mgo.Collection - Routes *mgo.Collection - Stops *mgo.Collection - Users *mgo.Collection - CasAUTH *cas.Client - CasMEM *cas.MemoryStore -} - -// InitConfig loads and return the app config. -func InitConfig() *Configuration { - // Read app configuration file - config, err := readConfiguration("conf.json") - if os.IsNotExist(err) { - log.Debug("reading configuration from environment") - config = &Configuration{} - err := env.Parse(config) - if err != nil { - log.Fatalf("error reading configuration from environment: %v", err) - } - } else if err != nil { - log.Fatalf("error reading configuration file: %v", err) - } - - return config -} - -// InitApp initializes the application given a config and connects to backends. -// It also seeds any needed information to the database. -func InitApp(Config *Configuration) *App { - //Initialize cas connection - url, error := url.Parse(Config.CasURL) - if error != nil { - log.Fatalf("invalid url") - } - var tickets *cas.MemoryStore - - client := cas.NewClient(&cas.Options{ - URL: url, - Store: nil, - }) - - // Connect to MongoDB - session, err := mgo.Dial(Config.MongoURL) - if err != nil { - log.Fatalf("MongoDB connection to \"%v\" failed: %v", Config.MongoURL, err) - } - // Create Shuttles object to store database session and collections - app := App{ - Config, - session, - session.DB("").C("updates"), - session.DB("").C("vehicles"), - session.DB("").C("routes"), - session.DB("").C("stops"), - session.DB("").C("users"), - client, - tickets, - } - - // Ensure unique vehicle identification - vehicleIndex := mgo.Index{ - Key: []string{"vehicleID"}, - Unique: true, - DropDups: true} - app.Vehicles.EnsureIndex(vehicleIndex) - - // Create index on update created time to quickly find the most recent updates - app.Updates.EnsureIndexKey("created") - - // Read vehicle configuration file - serr := readSeedConfiguration("seed/vehicle_seed.json", &app) - if serr != nil { - log.Fatalf("error reading vehicle configuration file: %v", serr) - } - return &app -} - -func readConfiguration(fileName string) (*Configuration, error) { - // Open config file and decode JSON to Configuration struct - file, err := os.Open(fileName) - if err != nil { - return nil, err - } - decoder := json.NewDecoder(file) - config := Configuration{} - if err := decoder.Decode(&config); err != nil { - return nil, err - } - return &config, nil -} - -//readSeedConfiguration adds a new vehicle to the database from seed. -func readSeedConfiguration(fileName string, app *App) error { - // Open seed_vehicle config file and decode JSON to app struct - file, err := os.Open(fileName) - - // Error handling - if err != nil { - log.Warn(err) - } - // Create a decoder for a file - fileread := json.NewDecoder(file) - - // Create map for json data and slice for vehicles - var vehiclesMap map[string][]map[string]interface{} // map with string as key and ,list of map with string as key and anything as value, as value - Vehicles := []Vehicle{} // list of default vehicle object - - // Call decode on fileread to place items into map - if err := fileread.Decode(&vehiclesMap); err != nil { - log.Warn(err) - } - - // Initialize our vehicles - for i := range vehiclesMap["Vehicles"] { - item := vehiclesMap["Vehicles"][i] - VehicleID, _ := item["VehicleID"].(string) - VehicleName, _ := item["VehicleName"].(string) - vehicle := Vehicle{VehicleID, VehicleName, time.Now(), time.Now(),false} - Vehicles = append(Vehicles, vehicle) - } - - // Add vehicles to the database - for j := range Vehicles { - app.Vehicles.Insert(&Vehicles[j]) - } - - return nil -} - -// WriteJSON writes the data as JSON. -func WriteJSON(w http.ResponseWriter, data interface{}) error { - w.Header().Set("Content-Type", "application/json") - b, err := json.MarshalIndent(data, "", " ") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return err - } - w.Write(b) - return nil -} diff --git a/tracking/vehicles.go b/tracking/vehicles.go deleted file mode 100644 index c0bf8aa89..000000000 --- a/tracking/vehicles.go +++ /dev/null @@ -1,321 +0,0 @@ -package tracking - -import ( - "encoding/json" - "fmt" - "gopkg.in/cas.v1" - "io/ioutil" - "net/http" - "regexp" - "strconv" - "strings" - "time" - - log "github.com/Sirupsen/logrus" - "github.com/gorilla/mux" - "gopkg.in/mgo.v2/bson" -) - -// represents a single position observed for a Vehicle from the data feed. -type VehicleUpdate struct { - VehicleID string `json:"vehicleID" bson:"vehicleID,omitempty"` - Lat string `json:"lat" bson:"lat"` - Lng string `json:"lng" bson:"lng"` - Heading string `json:"heading" bson:"heading"` - Speed string `json:"speed" bson:"speed"` - Lock string `json:"lock" bson:"lock"` - Time string `json:"time" bson:"time"` - Date string `json:"date" bson:"date"` - Status string `json:"status" bson:"status"` - Created time.Time `json:"created" bson:"created"` - Segment string `json:"segment" bson:"segment"` // the segment that a vehicle resides on -} - -// Vehicle represents an object being tracked. -type Vehicle struct { - VehicleID string `json:"vehicleID" bson:"vehicleID,omitempty"` - VehicleName string `json:"vehicleName" bson:"vehicleName"` - Created time.Time `bson:"created"` - Updated time.Time `bson:"updated"` - Active bool `json:"active"` -} - -// Status contains a detailed message on the tracked object's status -type Status struct { - Public bool `bson:"public"` - Message string `json:"message" bson:"message"` - Created time.Time `bson:"created"` - Updated time.Time `bson:"updated"` -} - -var ( - // Match each API field with any number (+) - // of the previous expressions (\d digit, \. escaped period, - negative number) - // Specify named capturing groups to store each field from data feed - dataRe = regexp.MustCompile(`(?PVehicle ID:([\d\.]+)) (?Plat:([\d\.-]+)) (?Plon:([\d\.-]+)) (?Pdir:([\d\.-]+)) (?Pspd:([\d\.-]+)) (?Plck:([\d\.-]+)) (?P