diff --git a/config.go b/config.go index 23d0695..e22ca68 100644 --- a/config.go +++ b/config.go @@ -9,24 +9,64 @@ var serverPort = 8080 var screenDir = "w" -// which routes do you want to show on the screen? add as many or as few as you'd like. -var routes = []Route{ - Route{ - Id: 15280, - Name: "46 WB", - Dir: "n", +// which routes do you want to show on the screen? check out readme.md for an explanation +var query = []Group { + Group { + Name: "34th & 45th Street", + Stations: []Station { + Station { + Id: 15280, + Routes: []Route { + Route { // 46 westbound + Id: 46, + Dir: "Edina", + }, + }, + }, + Station { + Id: 15412, + Routes: []Route { + Route { // 46 eastbound + Id: 46, + Dir: "St Paul - 7th St", + }, + }, + }, + }, }, - Route{ - Id: 15412, - Name: "46 EB", - Dir: "s", - }, - Route{ - Id: 51430, - Name: "Blue line", - }, - Route{ - Id: 51415, - Name: "Blue line", + Group { + Name: "46th Street Station", + Stations: []Station { + Station { + Id: 51430, + Routes: []Route { + Route { // blue line northbound + Id: 901, + Name: "Blue line", + Dir: "Downtown Mpls", + }, + }, + }, + Station { + Id: 51415, + Routes: []Route { + Route { // blue line southbound + Id: 901, + Name: "Blue line", + Dir: "Airport / Mall", + }, + }, + }, + + Station { + Id: 51546, + Routes: []Route { + Route { Id: 7 }, + Route { Id: 9 }, + Route { Id: 74 }, + Route { Id: 921 }, // A line + }, + }, + }, }, } diff --git a/main.go b/main.go index 040f39e..5ba7f1e 100644 --- a/main.go +++ b/main.go @@ -6,80 +6,89 @@ import ( "log" "math" "net/http" + "strconv" "time" ) +// configuration query format +type Group struct { // list related stops under a section title + Name string // friendly readable name for this group + Stations []Station +} +type Station struct { + Id uint // number found on the sign or on nextrip / g00gle maps + Routes []Route +} type Route struct { - Id uint // stop ID shown on sign or google maps - Name string // friendly readable name (optional) - Dir string // cardinal direction the bus or train is going (optional, will use value from API) - // Route string // filter by route name (optional) /!\ not yet implemented /!\ + Id uint // route number (may need to dig through the API for this one) + Name string // friendly readable name (optional) + Dir string // direction or location it's heading (optional) } +// response format +type EtaList struct { + Name string `json:"name"` + Etas []Eta `json:"etas"` +} type Eta struct { - Name string `json:"name"` - Dir string `json:"dir"` - Class string `json:"class"` - Delta string `json:"delta"` -} -var NilEta = Eta{} - -type StrSlice []string -func (slice StrSlice) indexOf(value string) int { - for p, v := range slice { - if (v == value) { - return p - } - } - return -1 + Name string `json:"name"` + Heading string `json:"heading"` + Class string `json:"class"` + Delta string `json:"delta"` } +// TODO? arrows -var cardinals = StrSlice{"n","e","s","w"} -var bounds = StrSlice{"NB","EB","SB","WB"} -var arrows = StrSlice{"↑","->","↓","<-"} -func arrow(dir string) string { - var format *StrSlice - if len(dir) < 2 { - format = &cardinals - } else { - format = &bounds - } - going := (format.indexOf(dir) + cardinals.indexOf(screenDir)) % 4 - return arrows[going] -} +// type StrSlice []string +// func (slice StrSlice) indexOf(value string) int { +// for p, v := range slice { +// if (v == value) { +// return p +// } +// } +// return -1 +// } + +// var cardinals = StrSlice{"n","e","s","w"} +// var bounds = StrSlice{"NB","EB","SB","WB"} +// var arrows = StrSlice{"↑","->","↓","<-"} +// func arrow(dir string) string { +// var format *StrSlice +// if len(dir) < 2 { +// format = &cardinals +// } else { +// format = &bounds +// } +// going := (format.indexOf(dir) + cardinals.indexOf(screenDir)) % 4 +// return arrows[going] +// } -func (r Route) getETA() Eta { - d := fetchRouteData(r.Id) +func (d Departure) parseDeparture(name, dir string) Eta { e := Eta{} - if len(d.Departures) == 0 { - return e - } - next := d.Departures[0] // route name - if r.Name == "" { - e.Name = next.RouteShortName + if name == "" { + e.Name = d.RouteShortName } else { - e.Name = r.Name + e.Name = name } - // direction arrow - if r.Dir == "" { - e.Dir = arrow(next.DirectionText) + // direction + if dir == "" { + e.Heading = d.Heading } else { - e.Dir = arrow(r.Dir) + e.Heading = dir } // time delta - t := time.Unix(next.DepartureTime, 0) + t := time.Unix(d.DepartureTime, 0) dur := time.Until(t) if dur.Minutes() <= 1 { e.Delta = "Due" } else if dur.Hours() < 60 { - e.Delta = fmt.Sprintf("%.0f minutes", dur.Minutes()) + e.Delta = fmt.Sprintf("%.0f min", dur.Minutes()) } else { e.Delta = fmt.Sprintf("%.0fh %.0fm", dur.Hours(), dur.Minutes() - (math.Floor(dur.Hours())*60)) } @@ -95,6 +104,77 @@ func (r Route) getETA() Eta { return e } +func (s Station) getETAs() []Eta { + r := fetchRouteData(s.Id) + e := []Eta{} + if len(r.Departures) == 0 { + return e + } + + if len(s.Routes) == 0 { + e = append(e, r.Departures[0].parseDeparture("","")) + } else { + // create a map for storing an ETA for each requested route + re := make(map[uint]Eta) + for _, reqRoute := range s.Routes { + // loop through r.Departures + for _, d := range r.Departures { + // if departure's route id matches the requested one, + dId, _ := strconv.Atoi(d.RouteId) + // and an ETA hasn't been found yet, + _, has := re[reqRoute.Id] + if dId == int(reqRoute.Id) && !has { + // save it to the map. + re[reqRoute.Id] = d.parseDeparture(reqRoute.Name, reqRoute.Dir) + } + } + } + // convert map to array in the order specified in config + for _, r := range s.Routes { + d, exists := re[r.Id] + if exists { + e = append(e, d) + } + } + } + + return e +} + + +func api(response http.ResponseWriter, request *http.Request){ + maxAge, _ := time.ParseDuration("29s") + fmt.Fprint(response, getCache(maxAge, func() string { + stations := []EtaList{} + + for _, group := range query { + etas := []Eta{} + for _, station := range group.Stations { + etas = append(etas, station.getETAs()...) + } + + stations = append(stations, EtaList{group.Name, etas}) + } + + data, err := json.Marshal(stations) + if err != nil { + return "[]" + } else { + return string(data) + } + })) +} + +var cacheValue string +var cacheUpdated time.Time +func getCache(maxAge time.Duration, f func() string) string { + if time.Since(cacheUpdated) >= maxAge { + cacheValue = f() + cacheUpdated = time.Now() + } + return cacheValue +} + func main(){ http.Handle("/", http.FileServer(http.Dir("./static"))) @@ -104,39 +184,3 @@ func main(){ log.Fatal(err) } } - -func api(response http.ResponseWriter, request *http.Request){ - var etas []Eta - - for _, route := range routes { - eta := route.getETA() - if eta != NilEta { - etas = append(etas, eta) - } - } - - data, err := json.Marshal(etas) - if err != nil { - fmt.Fprint(response, "[]") - } else { - fmt.Fprint(response, string(data)) - } -} - -// TODO: cache the ETA list - -// let etaCache = { -// value: null, -// createdAt: 0, -// } -// let getETAsFromCache = async (maxAge) => { -// if (Date.now() / 1000 - etaCache.createdAt >= maxAge) { -// etaCache.value = [] -// for (let r of conf.routes) { -// let e = await getETA(r) -// if (e !== null) etaCache.value.push(e) -// } -// etaCache.createdAt = Date.now() / 1000 -// } -// return etaCache.value -// } diff --git a/nextrip.go b/nextrip.go index 17e1dbd..094d19a 100644 --- a/nextrip.go +++ b/nextrip.go @@ -7,6 +7,7 @@ import ( "net/http" ) + // type Stop struct { // stop_id uint // latitude float32 @@ -23,11 +24,11 @@ type Departure struct { // stop_id uint DepartureText string `json:"departure_text"` DepartureTime int64 `json:"departure_time"` - // description string - // route_id uint + Heading string `json:"description"` + RouteId string `json:"route_id"` RouteShortName string `json:"route_short_name"` // direction_id uint8 - DirectionText string `json:"direction_text"` + // DirectionText string `json:"direction_text"` // schedule_relationship string } type RouteData struct { @@ -39,20 +40,20 @@ type RouteData struct { func fetchRouteData(id uint) RouteData { resp, err := http.Get(fmt.Sprintf("https://svc.metrotransit.org/nextripv2/%d", id)) if err != nil { - fmt.Printf("Unable to reach NexTrip API:\n%s", err) + fmt.Printf("Unable to reach NexTrip API:\n%s\n", err) return RouteData{} } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - fmt.Printf("Error when reading NexTrip API response:\n%s", err) + fmt.Printf("Error when reading NexTrip API response:\n%s\n", err) return RouteData{} } var data RouteData err = json.Unmarshal(body, &data) if err != nil { - fmt.Printf("Error when parsing NexTrip API data:\n%s", err) + fmt.Printf("Error when parsing NexTrip API data:\n%s\n", err) return RouteData{} } diff --git a/readme.md b/readme.md index 1fed633..201aed0 100644 --- a/readme.md +++ b/readme.md @@ -24,3 +24,60 @@ we're going to put this on a display in the window of layer zero so that people ### what's it look like? ![screenshot](screenshot.png) + +### request data format + +```js +groups: [ + { + name: "34th & 45th Street", + stations: [ + { + id: 15280, + routes: [ + { + id: 46, + name?: "46", + dir?: "WB" + } + ] + }, + { + id: 15412, + routes: [ + { + id: 46, + name?: "46", + dir?: "EB" + } + ] + } + ] + }, + { + name: "46th Street Station", + stations: [ + { + id: 51430, + routes: [ + { + id: 901, + name?: "Blue line", + dir?: "to Mpls-Target Field" + } + ] + }, + { + id: 51415, + routes: [ + { + id: 901, + name?: "Blue line", + dir?: "to Mall of America" + } + ] + } + ] + } +] +``` diff --git a/sass.sh b/sass.sh new file mode 100755 index 0000000..f86290c --- /dev/null +++ b/sass.sh @@ -0,0 +1,2 @@ +#!/bin/sh +sass --watch --no-source-map --style=compressed static/style.scss static/style.css diff --git a/screenshot.png b/screenshot.png index bc4266c..c6d3aa5 100644 Binary files a/screenshot.png and b/screenshot.png differ diff --git a/screenshot2.png b/screenshot2.png deleted file mode 100644 index 526f294..0000000 Binary files a/screenshot2.png and /dev/null differ diff --git a/static/FiraCode-Regular.woff b/static/FiraCode-Regular.woff deleted file mode 100644 index 8816b69..0000000 Binary files a/static/FiraCode-Regular.woff and /dev/null differ diff --git a/static/FiraCode-Regular.woff2 b/static/FiraCode-Regular.woff2 deleted file mode 100644 index f8b63fb..0000000 Binary files a/static/FiraCode-Regular.woff2 and /dev/null differ diff --git a/static/Inconsolata-Regular.ttf b/static/Inconsolata-Regular.ttf new file mode 100644 index 0000000..0d879bf Binary files /dev/null and b/static/Inconsolata-Regular.ttf differ diff --git a/static/Inconsolata-SemiBold.ttf b/static/Inconsolata-SemiBold.ttf new file mode 100644 index 0000000..71ba4f9 Binary files /dev/null and b/static/Inconsolata-SemiBold.ttf differ diff --git a/static/index.html b/static/index.html index 1f1733b..6f27092 100644 --- a/static/index.html +++ b/static/index.html @@ -9,14 +9,28 @@ let getETAs = async () => { let response = await fetch('/etas') let data = await response.json() - console.dir(data) - let tbody = document.getElementById('etas') - tbody.innerHTML = '' + let main = document.getElementById('etas') + let innerHTML = '' - for (let eta of data){ - tbody.innerHTML += `
${station.name} | ||
---|---|---|
route | +heading | +eta | +
${eta.name} | ${eta.heading} | ${eta.delta} |