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