From 90020a7c5741721232c8c62c7ba0028da7771377 Mon Sep 17 00:00:00 2001 From: Martin Polden Date: Fri, 14 Apr 2023 21:09:54 +0200 Subject: all: remove non-functional /api/v1/ --- README.md | 138 ++------------------------------------- atb/atb.go | 157 -------------------------------------------- atb/atb_test.go | 147 ----------------------------------------- cmd/atb/main.go | 8 +-- http/http.go | 168 +---------------------------------------------- http/http_test.go | 187 ++--------------------------------------------------- http/types.go | 169 ----------------------------------------------- http/types_test.go | 173 ------------------------------------------------- 8 files changed, 13 insertions(+), 1134 deletions(-) delete mode 100644 atb/atb.go delete mode 100644 atb/atb_test.go delete mode 100644 http/types_test.go diff --git a/README.md b/README.md index 2431afa..cf6751c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ![Build Status](https://github.com/mpolden/atb/workflows/ci/badge.svg) A minimal API for bus data in Trondheim, Norway. This API proxies requests to -AtB/Entur APIs and converts the responses into a sane JSON format. +Entur APIs and converts the responses into a sane JSON format. Responses from the proxied APIs are cached. By default bus stops will be cached for 1 week and departures for 1 minute. @@ -11,13 +11,11 @@ for 1 week and departures for 1 minute. As of mid-August 2021 the SOAP-based AtB API no longer returns any departure data. According to [this blog post on open data](https://beta.atb.no/blogg/apne-data-og-atb) it appears the preferred API -is now [Entur](https://developer.entur.org/). +is now [Entur](https://developer.entur.org/). The `/api/v1/` paths have +therefore been removed. -Version 1 of this API will remain implemented for now, but likely won't return -any usable data. - -Version 2 has been implemented and proxies requests to Entur. These are the -changes in version 2: +Version 2 has been implemented and proxies requests to Entur instead. These are +the changes in version 2: * There is no version 2 variant of `/api/v1/busstops`. Use https://stoppested.entur.org/ to find valid stop IDs. @@ -27,7 +25,7 @@ changes in version 2: * The `registeredDepartureTime` field may be omitted. * The `isGoingTowardsCentrum` field has moved to the departure object. -Both version 1 and 2 of this API aims to be compatible with +This API aims to be compatible with [BusBuddy](https://github.com/norrs/busbuddy) (which appears to be defunct). ## Usage @@ -35,8 +33,6 @@ Both version 1 and 2 of this API aims to be compatible with ``` $ atb -h Usage of atb: - -c string - Path to config file (default "config.json") -d string Departure cache duration (default "1m") -l string @@ -46,15 +42,6 @@ Usage of atb: -x Allow requests from other domains ``` -## Example config - -``` -{ - "Username": "username", - "Password": "password" -} -``` - ## API ### `/` @@ -67,8 +54,6 @@ Example: $ curl https://mpolden.no/atb/ | jq . { "urls": [ - "https://mpolden.no/atb/v1/busstops", - "https://mpolden.no/atb/v1/departures", "https://mpolden.no/atb/v2/departures" ] } @@ -105,114 +90,3 @@ $ curl 'https://mpolden.no/atb/v2/departures/41613?direction=inbound' | jq . ] } ``` - - -### `/api/v1/busstops` - -Lists all known bus stops. - -Example: - -``` -$ curl https://mpolden.no/atb/v1/busstops | jq . -{ - "stops": [ - { - "stopId": 100633, - "nodeId": 16011376, - "description": "Prof. Brochs gt", - "longitude": 10.398125177823237, - "latitude": 63.4155348940887, - "mobileCode": "16011376 (Prof.)", - "mobileName": "Prof. (16011376)" - }, - ... - ] -} -``` - -### `/api/v1/busstops/{node-id}` - -Information about the given bus stop, identified by a node ID. - -Example: - -``` -$ curl https://mpolden.no/atb/v1/busstops/16011376 | jq . -{ - "stopId": 100633, - "nodeId": 16011376, - "description": "Prof. Brochs gt", - "longitude": 10.398126, - "latitude": 63.415535, - "mobileCode": "16011376 (Prof.)", - "mobileName": "Prof. (16011376)" -} -``` - -As [GeoJSON](http://geojson.org/): - -``` -$ curl https://mpolden.no/atb/v1/busstops/16011376?geojson | jq . -{ - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - 10.398126, - 63.415535 - ] - }, - "properties": { - "busstop": { - "stopId": 100633, - "nodeId": 16011376, - "description": "Prof. Brochs gt", - "longitude": 10.398126, - "latitude": 63.415535, - "mobileCode": "16011376 (Prof.)", - "mobileName": "Prof. (16011376)" - }, - "name": "Prof. Brochs gt" - } -} -``` - -### `/api/v1/departures` - -Lists departure URLs for all known bus stops. - -Example: - -``` -$ curl -s https://mpolden.no/atb/v1/departures | jq . -{ - "urls": [ - "https://mpolden.no/atb/v1/departures/15057011", - ... - ] -} -``` - -### `/api/v1/departures/{node-id}` - -Lists all departures for the given bus stop, identified by a node ID. - -Example: - -``` -$ curl https://mpolden.no/atb/v1/departures/16011376 | jq . -{ - "isGoingTowardsCentrum": true, - "departures": [ - { - "line": "36", - "registeredDepartureTime": "2015-02-26T22:55:00.000", - "scheduledDepartureTime": "2015-02-26T22:54:00.000", - "destination": "Munkegata M4", - "isRealtimeData": true - }, - ... - ] -} -``` diff --git a/atb/atb.go b/atb/atb.go deleted file mode 100644 index eb629a4..0000000 --- a/atb/atb.go +++ /dev/null @@ -1,157 +0,0 @@ -package atb - -import ( - "encoding/json" - "encoding/xml" - "fmt" - "io/ioutil" - "net/http" - "strings" -) - -// DefaultURL is the default AtB API URL. -const DefaultURL = "http://st.atb.no/New/InfoTransit/UserServices.asmx" - -// Client represents a client which communicates with AtBs API. -type Client struct { - Username string - Password string - URL string -} - -// BusStops represents a list of bus stops. -type BusStops struct { - Stops []BusStop `json:"Fermate"` -} - -// BusStop represents a bus stop. -type BusStop struct { - StopID int `json:"cinFermata"` - NodeID string `json:"codAzNodo"` - Description string `json:"descrizione"` - Longitude string `json:"lon"` - Latitude int `json:"lat"` - MobileCode string `json:"codeMobile"` - MobileName string `json:"nomeMobile"` -} - -// Forecasts represents a list of forecasts. -type Forecasts struct { - Nodes []NodeInfo `json:"InfoNodo"` - Forecasts []Forecast `json:"Orari"` - Total int `json:"total"` -} - -// NodeInfo represents a bus stop, returned as a part of a forecast. -type NodeInfo struct { - Name string `json:"nome_Az"` - NodeID string `json:"codAzNodo"` - NodeName string `json:"nomeNodo"` - NodeDescription string `json:"descrNodo"` - BitMaskProperties string `json:"bitMaskProprieta"` - MobileCode string `json:"codeMobile"` - Longitude string `json:"coordLon"` - Latitude string `json:"coordLat"` -} - -// Forecast represents a single forecast. -type Forecast struct { - LineID string `json:"codAzLinea"` - LineDescription string `json:"descrizioneLinea"` - RegisteredDepartureTime string `json:"orario"` - ScheduledDepartureTime string `json:"orarioSched"` - StationForecast string `json:"statoPrevisione"` - Destination string `json:"capDest"` -} - -type busStopsRequest struct { - XMLName xml.Name `xml:"Envelope"` - Result []byte `xml:"Body>GetBusStopsListResponse>GetBusStopsListResult"` -} - -type forecastRequest struct { - XMLName xml.Name `xml:"Envelope"` - Result []byte `xml:"Body>getUserRealTimeForecastByStopResponse>getUserRealTimeForecastByStopResult"` -} - -// NewFromConfig creates a new client where name is the path to the config file. -func NewFromConfig(name string) (*Client, error) { - data, err := ioutil.ReadFile(name) - if err != nil { - return nil, err - } - var client Client - if err := json.Unmarshal(data, &client); err != nil { - return nil, err - } - if client.URL == "" { - client.URL = DefaultURL - } - return &client, nil -} - -func (c *Client) postXML(body string, dst interface{}) error { - resp, err := http.Post(c.URL, "application/soap+xml", strings.NewReader(body)) - if err != nil { - return err - } - defer resp.Body.Close() - dec := xml.NewDecoder(resp.Body) - if err := dec.Decode(&dst); err != nil { - return err - } - return nil -} - -// BusStops retrieves bus stops from AtBs API. -func (c *Client) BusStops() (BusStops, error) { - req := fmt.Sprintf(` - - - - - %s - %s - - - -`, c.Username, c.Password) - - var stopsRequest busStopsRequest - if err := c.postXML(req, &stopsRequest); err != nil { - return BusStops{}, err - } - - var stops BusStops - if err := json.Unmarshal(stopsRequest.Result, &stops); err != nil { - return BusStops{}, err - } - return stops, nil -} - -// Forecasts retrieves forecasts from AtBs API, using nodeID to identify the bus stop. -func (c *Client) Forecasts(nodeID int) (Forecasts, error) { - req := fmt.Sprintf(` - - - - - %s - %s - - %d - - -`, c.Username, c.Password, nodeID) - - var forecastRequest forecastRequest - if err := c.postXML(req, &forecastRequest); err != nil { - return Forecasts{}, err - } - - var forecasts Forecasts - if err := json.Unmarshal(forecastRequest.Result, &forecasts); err != nil { - return Forecasts{}, err - } - return forecasts, nil -} diff --git a/atb/atb_test.go b/atb/atb_test.go deleted file mode 100644 index a39989f..0000000 --- a/atb/atb_test.go +++ /dev/null @@ -1,147 +0,0 @@ -package atb - -import ( - "fmt" - "net/http" - "net/http/httptest" - "reflect" - "testing" -) - -func newTestServer(path string, body string) *httptest.Server { - handler := func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8") - fmt.Fprint(w, body) - } - mux := http.NewServeMux() - mux.HandleFunc(path, handler) - return httptest.NewServer(mux) -} - -func TestGetBusStops(t *testing.T) { - server := newTestServer("/", busStopsResponse) - defer server.Close() - atb := Client{URL: server.URL} - expected := BusStops{ - Stops: []BusStop{ - { - StopID: 100633, - NodeID: "16011376", - Description: "Prof. Brochs gt", - Longitude: "1157514", - Latitude: 9202874, - MobileCode: "16011376 (Prof.)", - MobileName: "Prof. (16011376)", - }, - }, - } - stops, err := atb.BusStops() - if err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(stops, expected) { - t.Fatalf("Expected %+v, got %+v", expected, stops) - } -} - -func TestGetRealTimeForecast(t *testing.T) { - server := newTestServer("/", forecastResponse) - defer server.Close() - atb := Client{URL: server.URL} - forecasts, err := atb.Forecasts(16011376) - expected := Forecasts{ - Total: 1, - Nodes: []NodeInfo{ - { - Name: "AtB", - NodeID: "16011376", - NodeName: "Prof.", - NodeDescription: "Prof. Brochs gt", - BitMaskProperties: "0", - Longitude: "10.398126", - Latitude: "63.415535", - MobileCode: "Prof. Brochs gt", - }, - }, - Forecasts: []Forecast{ - { - LineID: "6", - LineDescription: "6", - RegisteredDepartureTime: "26.02.2015 18:38", - ScheduledDepartureTime: "26.02.2015 18:01", - StationForecast: "Prev", - Destination: "Munkegata M5", - }, - }, - } - if err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(forecasts, expected) { - t.Fatalf("Expected %+v, got %+v", expected, forecasts) - } -} - -const busStopsResponse = ` - - - - -{ - "Fermate": [ - { - "cinAzienda": 1, - "nomeAzienda": "AtB", - "cinFermata": 100633, - "codAzNodo": "16011376", - "descrizione": "Prof. Brochs gt", - "lon": "1157514", - "lat": 9202874, - "name": "Prof.", - "codeMobile": "16011376 (Prof.)", - "nomeMobile": "Prof. (16011376)" - } - ] -} - - - -` - -const forecastResponse = ` - - - - -{ - "total": 1, - "timeServer": "2015-02-26 18:37", - "InfoNodo": [ - { - "nome_Az": "AtB", - "codAzNodo": "16011376", - "nomeNodo": "Prof.", - "descrNodo": "Prof. Brochs gt", - "bitMaskProprieta": "0", - "codeMobile": "Prof. Brochs gt", - "coordLon": "10.398126", - "coordLat": "63.415535" - } - ], - "Orari": [ - { - "codAzLinea": "6", - "descrizioneLinea": "6", - "orario": "26.02.2015 18:38", - "orarioSched": "26.02.2015 18:01", - "statoPrevisione": "Prev", - "capDest": "Munkegata M5", - "turnoMacchina": "57", - "descrizionePercorso": "39" - } - ] -} - - - -` diff --git a/cmd/atb/main.go b/cmd/atb/main.go index 7b7e131..6335c1f 100644 --- a/cmd/atb/main.go +++ b/cmd/atb/main.go @@ -5,7 +5,6 @@ import ( "log" "time" - "github.com/mpolden/atb/atb" "github.com/mpolden/atb/entur" "github.com/mpolden/atb/http" ) @@ -25,18 +24,13 @@ func mustParseDuration(s string) time.Duration { func main() { listen := flag.String("l", ":8080", "Listen address") - config := flag.String("c", "config.json", "Path to config file") stopTTL := flag.String("s", "168h", "Bus stop cache duration") departureTTL := flag.String("d", "1m", "Departure cache duration") cors := flag.Bool("x", false, "Allow requests from other domains") flag.Parse() - atb, err := atb.NewFromConfig(*config) - if err != nil { - log.Fatal(err) - } entur := entur.New("") - server := http.New(atb, entur, mustParseDuration(*stopTTL), mustParseDuration(*departureTTL), *cors) + server := http.New(entur, mustParseDuration(*stopTTL), mustParseDuration(*departureTTL), *cors) log.Printf("Listening on %s", *listen) if err := server.ListenAndServe(*listen); err != nil { diff --git a/http/http.go b/http/http.go index a72f89e..09a3064 100644 --- a/http/http.go +++ b/http/http.go @@ -10,7 +10,6 @@ import ( "strconv" "time" - "github.com/mpolden/atb/atb" "github.com/mpolden/atb/cache" "github.com/mpolden/atb/entur" ) @@ -22,7 +21,6 @@ const ( // Server represents an Server server. type Server struct { - ATB *atb.Client Entur *entur.Client CORS bool cache *cache.Cache @@ -70,52 +68,6 @@ func filterDepartures(departures []Departure, direction string) []Departure { return departures } -func (s *Server) getBusStops(urlPrefix string) (BusStops, bool, error) { - const cacheKey = "stops" - cached, hit := s.cache.Get(cacheKey) - if hit { - return cached.(BusStops), hit, nil - } - atbBusStops, err := s.ATB.BusStops() - if err != nil { - return BusStops{}, hit, err - } - busStops, err := convertBusStops(atbBusStops) - if err != nil { - return BusStops{}, hit, err - } - for i := range busStops.Stops { - busStops.Stops[i].URL = fmt.Sprintf("%s/api/v1/busstops/%d", urlPrefix, busStops.Stops[i].NodeID) - } - // Create a map of nodeIds - busStops.nodeIDs = make(map[int]*BusStop, len(busStops.Stops)) - for i, s := range busStops.Stops { - // Store a pointer to the BusStop struct - busStops.nodeIDs[s.NodeID] = &busStops.Stops[i] - } - s.cache.Set(cacheKey, busStops, s.ttl.stops) - return busStops, hit, nil -} - -func (s *Server) atbDepartures(urlPrefix string, nodeID int) (Departures, bool, error) { - cacheKey := strconv.Itoa(nodeID) - cached, hit := s.cache.Get(cacheKey) - if hit { - return cached.(Departures), hit, nil - } - forecasts, err := s.ATB.Forecasts(nodeID) - if err != nil { - return Departures{}, hit, err - } - departures, err := convertForecasts(forecasts) - if err != nil { - return Departures{}, hit, err - } - departures.URL = fmt.Sprintf("%s/api/v1/departures/%d", urlPrefix, nodeID) - s.cache.Set(cacheKey, departures, s.ttl.departures) - return departures, hit, nil -} - func (s *Server) enturDepartures(urlPrefix string, stopID int, direction string) (Departures, bool, error) { cacheKey := strconv.Itoa(stopID) cached, hit := s.cache.Get(cacheKey) @@ -143,94 +95,6 @@ func (s *Server) setCacheHeader(w http.ResponseWriter, hit bool) { w.Header().Set("X-Cache", v) } -// BusStopsHandler is a handler for retrieving bus stops. -func (s *Server) BusStopsHandler(w http.ResponseWriter, r *http.Request) (interface{}, *Error) { - busStops, hit, err := s.getBusStops(urlPrefix(r)) - if err != nil { - return nil, &Error{ - err: err, - Status: http.StatusInternalServerError, - Message: "Failed to get bus stops from AtB", - } - } - s.setCacheHeader(w, hit) - _, geojson := r.URL.Query()["geojson"] - if geojson { - return busStops.GeoJSON(), nil - } - return busStops, nil -} - -// BusStopHandler is a handler for retrieving info about a bus stop. -func (s *Server) BusStopHandler(w http.ResponseWriter, r *http.Request) (interface{}, *Error) { - nodeID, err := strconv.Atoi(filepath.Base(r.URL.Path)) - if err != nil { - return nil, &Error{ - err: err, - Status: http.StatusBadRequest, - Message: "Invalid nodeID", - } - } - busStops, hit, err := s.getBusStops(urlPrefix(r)) - if err != nil { - return nil, &Error{ - err: err, - Status: http.StatusInternalServerError, - Message: "Failed to get bus stops from AtB", - } - } - busStop, ok := busStops.nodeIDs[nodeID] - if !ok { - return nil, &Error{ - Status: http.StatusNotFound, - Message: "Unknown bus stop", - } - } - s.setCacheHeader(w, hit) - _, geojson := r.URL.Query()["geojson"] - if geojson { - return busStop.GeoJSON(), nil - } - return busStop, nil -} - -// DepartureHandler is a handler for retrieving departures for a given bus stop. -func (s *Server) DepartureHandler(w http.ResponseWriter, r *http.Request) (interface{}, *Error) { - nodeID, err := strconv.Atoi(filepath.Base(r.URL.Path)) - if err != nil { - return nil, &Error{ - err: err, - Status: http.StatusBadRequest, - Message: "Invalid nodeID", - } - } - busStops, _, err := s.getBusStops(urlPrefix(r)) - if err != nil { - return nil, &Error{ - err: err, - Status: http.StatusInternalServerError, - Message: "Failed to get bus stops from AtB", - } - } - _, ok := busStops.nodeIDs[nodeID] - if !ok { - return nil, &Error{ - Status: http.StatusNotFound, - Message: "Unknown bus stop", - } - } - departures, hit, err := s.atbDepartures(urlPrefix(r), nodeID) - if err != nil { - return nil, &Error{ - err: err, - Status: http.StatusInternalServerError, - Message: "Failed to get departures from AtB", - } - } - s.setCacheHeader(w, hit) - return departures, nil -} - // DepartureHandlerV2 is a handler which retrieves departures for a given bus stop through Entur. func (s *Server) DepartureHandlerV2(w http.ResponseWriter, r *http.Request) (interface{}, *Error) { stopID, err := strconv.Atoi(filepath.Base(r.URL.Path)) @@ -254,49 +118,25 @@ func (s *Server) DepartureHandlerV2(w http.ResponseWriter, r *http.Request) (int return departures, nil } -// DeparturesHandler lists all known departures. -func (s *Server) DeparturesHandler(w http.ResponseWriter, r *http.Request) (interface{}, *Error) { - busStops, hit, err := s.getBusStops(urlPrefix(r)) - if err != nil { - return nil, &Error{ - err: err, - Status: http.StatusInternalServerError, - Message: "Failed to get bus stops from AtB", - } - } - s.setCacheHeader(w, hit) - var urls struct { - URLs []string `json:"urls"` - } - urls.URLs = make([]string, len(busStops.Stops)) - for i, stop := range busStops.Stops { - urls.URLs[i] = fmt.Sprintf("%s/api/v1/departures/%d", urlPrefix(r), stop.NodeID) - } - return urls, nil -} - // DefaultHandler lists known URLs. func (s *Server) DefaultHandler(w http.ResponseWriter, r *http.Request) (interface{}, *Error) { if r.URL.Path != "/" { return nil, &Error{Status: http.StatusNotFound, Message: "Resource not found"} } prefix := urlPrefix(r) - busStopsURL := fmt.Sprintf("%s/api/v1/busstops", prefix) - departuresURL := fmt.Sprintf("%s/api/v1/departures", prefix) departuresV2URL := fmt.Sprintf("%s/api/v2/departures", prefix) return struct { URLs []string `json:"urls"` }{ - []string{busStopsURL, departuresURL, departuresV2URL}, + []string{departuresV2URL}, }, nil } // New returns a new Server using given clients to communicate with AtB and Entur. stopTTL and departureTTL control the // cache TTL bus stops and departures. -func New(atb *atb.Client, entur *entur.Client, stopTTL, departureTTL time.Duration, cors bool) *Server { +func New(entur *entur.Client, stopTTL, departureTTL time.Duration, cors bool) *Server { cache := cache.New(time.Minute) return &Server{ - ATB: atb, Entur: entur, CORS: cors, cache: cache, @@ -345,10 +185,6 @@ func requestFilter(next http.Handler, cors bool) http.Handler { // Handler returns a root handler for the API. func (s *Server) Handler() http.Handler { mux := http.NewServeMux() - mux.Handle("/api/v1/busstops", appHandler(s.BusStopsHandler)) - mux.Handle("/api/v1/busstops/", appHandler(s.BusStopHandler)) - mux.Handle("/api/v1/departures", appHandler(s.DeparturesHandler)) - mux.Handle("/api/v1/departures/", appHandler(s.DepartureHandler)) mux.Handle("/api/v2/departures", appHandler(s.DepartureHandlerV2)) mux.Handle("/api/v2/departures/", appHandler(s.DepartureHandlerV2)) mux.Handle("/", appHandler(s.DefaultHandler)) diff --git a/http/http_test.go b/http/http_test.go index f74db42..b3792b9 100644 --- a/http/http_test.go +++ b/http/http_test.go @@ -7,35 +7,16 @@ import ( "log" "net/http" "net/http/httptest" - "strings" "testing" "time" - "github.com/mpolden/atb/atb" "github.com/mpolden/atb/entur" ) func apiTestServer() *httptest.Server { handler := func(w http.ResponseWriter, r *http.Request) { - isSOAP := r.Header.Get("Content-Type") == "application/soap+xml" - if isSOAP { - b, err := ioutil.ReadAll(r.Body) - if err != nil { - panic(err) - } - xml := string(b) - w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8") - if strings.Contains(xml, "GetBusStopsList") { - fmt.Fprint(w, busStopsResponse) - } else if strings.Contains(xml, "getUserRealTimeForecastByStop") { - fmt.Fprint(w, forecastResponse) - } else { - panic("unknown request body: " + xml) - } - } else { - w.Header().Set("Content-Type", "application/json; charset=utf-8") - fmt.Fprint(w, enturResponse) - } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + fmt.Fprint(w, enturResponse) } mux := http.NewServeMux() mux.HandleFunc("/", handler) @@ -44,9 +25,8 @@ func apiTestServer() *httptest.Server { func testServers() (*httptest.Server, *Server) { apiServer := apiTestServer() - atb := &atb.Client{URL: apiServer.URL} entur := &entur.Client{URL: apiServer.URL} - server := New(atb, entur, 168*time.Hour, 1*time.Minute, false) + server := New(entur, 168*time.Hour, 1*time.Minute, false) return apiServer, server } @@ -78,21 +58,7 @@ func TestAPI(t *testing.T) { // Unknown resources {"/not-found", `{"status":404,"message":"Resource not found"}`, 404}, // List know URLs - {"/", fmt.Sprintf(`{"urls":["%s/api/v1/busstops","%s/api/v1/departures","%s/api/v2/departures"]}`, httpSrv.URL, httpSrv.URL, httpSrv.URL), 200}, - // List all bus stops - {"/api/v1/busstops", fmt.Sprintf(`{"stops":[{"url":"%s/api/v1/busstops/16011376","stopId":100633,"nodeId":16011376,"description":"Prof. Brochs gt","longitude":10.398126,"latitude":63.415535,"mobileCode":"16011376 (Prof.)","mobileName":"Prof. (16011376)"}]}`, httpSrv.URL), 200}, - // List all departures - {"/api/v1/departures", fmt.Sprintf(`{"urls":["%s/api/v1/departures/16011376"]}`, httpSrv.URL), 200}, - // Show specific bus stop - {"/api/v1/busstops/", `{"status":400,"message":"Invalid nodeID"}`, 400}, - {"/api/v1/busstops/foo", `{"status":400,"message":"Invalid nodeID"}`, 400}, - {"/api/v1/busstops/42", `{"status":404,"message":"Unknown bus stop"}`, 404}, - {"/api/v1/busstops/16011376", fmt.Sprintf(`{"url":"%s/api/v1/busstops/16011376","stopId":100633,"nodeId":16011376,"description":"Prof. Brochs gt","longitude":10.398126,"latitude":63.415535,"mobileCode":"16011376 (Prof.)","mobileName":"Prof. (16011376)"}`, httpSrv.URL), 200}, - // Show specific departure - {"/api/v1/departures/", `{"status":400,"message":"Invalid nodeID"}`, 400}, - {"/api/v1/departures/foo", `{"status":400,"message":"Invalid nodeID"}`, 400}, - {"/api/v1/departures/42", `{"status":404,"message":"Unknown bus stop"}`, 404}, - {"/api/v1/departures/16011376", fmt.Sprintf(`{"url":"%s/api/v1/departures/16011376","isGoingTowardsCentrum":true,"departures":[{"line":"6","registeredDepartureTime":"2015-02-26T18:38:00.000","scheduledDepartureTime":"2015-02-26T18:01:00.000","destination":"Munkegata M5","isRealtimeData":true}]}`, httpSrv.URL), 200}, + {"/", fmt.Sprintf(`{"urls":["%s/api/v2/departures"]}`, httpSrv.URL), 200}, // Show specific departure (v2) {"/api/v2/departures", `{"status":400,"message":"Invalid stop ID. Use https://stoppested.entur.org/ to find stop IDs."}`, 400}, {"/api/v2/departures/", `{"status":400,"message":"Invalid stop ID. Use https://stoppested.entur.org/ to find stop IDs."}`, 400}, @@ -135,151 +101,6 @@ func TestURLPrefix(t *testing.T) { } } -func TestGetBusStops(t *testing.T) { - apiServer, server := testServers() - defer apiServer.Close() - _, _, err := server.getBusStops("") - if err != nil { - t.Fatal(err) - } - cached, ok := server.cache.Get("stops") - if !ok { - t.Fatal("Expected true") - } - busStops, ok := cached.(BusStops) - if !ok { - t.Fatal("Expected true") - } - if len(busStops.Stops) != 1 { - t.Fatal("Expected length to be 1") - } - if len(busStops.nodeIDs) != 1 { - t.Fatal("Expected length to be 1") - } -} - -func TestGetBusStopsCache(t *testing.T) { - apiServer, server := testServers() - defer apiServer.Close() - _, hit, err := server.getBusStops("") - if err != nil { - t.Fatal(err) - } - if hit { - t.Error("Expected false") - } - _, hit, err = server.getBusStops("") - if err != nil { - t.Fatal(err) - } - if !hit { - t.Error("Expected true") - } -} - -func TestGetDepartures(t *testing.T) { - apiServer, server := testServers() - defer apiServer.Close() - _, _, err := server.atbDepartures("", 16011376) - if err != nil { - t.Fatal(err) - } - cached, ok := server.cache.Get("16011376") - if !ok { - t.Fatal("Expected true") - } - departures, ok := cached.(Departures) - if !ok { - t.Fatal("Expected true") - } - if len(departures.Departures) != 1 { - t.Fatal("Expected length to be 1") - } -} - -func TestGetDeparturesCache(t *testing.T) { - apiServer, server := testServers() - defer apiServer.Close() - _, hit, err := server.atbDepartures("", 16011376) - if err != nil { - t.Fatal(err) - } - if hit { - t.Error("Expected false") - } - _, hit, err = server.atbDepartures("", 16011376) - if err != nil { - t.Fatal(err) - } - if !hit { - t.Error("Expected true") - } -} - -const busStopsResponse = ` - - - - -{ - "Fermate": [ - { - "cinAzienda": 1, - "nomeAzienda": "AtB", - "cinFermata": 100633, - "codAzNodo": "16011376", - "descrizione": "Prof. Brochs gt", - "lon": "1157514", - "lat": 9202874, - "name": "Prof.", - "codeMobile": "16011376 (Prof.)", - "nomeMobile": "Prof. (16011376)" - } - ] -} - - - -` - -const forecastResponse = ` - - - - -{ - "total": 1, - "timeServer": "2015-02-26 18:37", - "InfoNodo": [ - { - "nome_Az": "AtB", - "codAzNodo": "16011376", - "nomeNodo": "Prof.", - "descrNodo": "Prof. Brochs gt", - "bitMaskProprieta": "0", - "codeMobile": "Prof. Brochs gt", - "coordLon": "10.398126", - "coordLat": "63.415535" - } - ], - "Orari": [ - { - "codAzLinea": "6", - "descrizioneLinea": "6", - "orario": "26.02.2015 18:38", - "orarioSched": "26.02.2015 18:01", - "statoPrevisione": "Prev", - "capDest": "Munkegata M5", - "turnoMacchina": "57", - "descrizionePercorso": "39" - } - ] -} - - - -` - const enturResponse = `{ "data": { "stopPlace": { diff --git a/http/types.go b/http/types.go index 2ed25e3..de6d652 100644 --- a/http/types.go +++ b/http/types.go @@ -1,12 +1,6 @@ package http import ( - "math" - "strconv" - "strings" - "time" - - "github.com/mpolden/atb/atb" "github.com/mpolden/atb/entur" ) @@ -28,25 +22,6 @@ type BusStop struct { MobileName string `json:"mobileName"` } -// GeoJSON represents the top-level object of the GeoJSON format. -type GeoJSON struct { - Type string `json:"type"` - Geometry `json:"geometry"` - Properties map[string]interface{} `json:"properties"` -} - -// Geometry represents the geometry object of the GeoJSON format. -type Geometry struct { - Type string `json:"type"` - Coordinates []float64 `json:"coordinates"` -} - -// GeoJSONCollection represents a collection of GeoJSON feature objects. -type GeoJSONCollection struct { - Type string `json:"type"` - Features []GeoJSON `json:"features"` -} - // Departures represents a list of departures, from a given bus stop. type Departures struct { URL string `json:"url"` @@ -71,120 +46,6 @@ type Error struct { Message string `json:"message"` } -func convertBusStop(s atb.BusStop) (BusStop, error) { - nodeID, err := strconv.Atoi(s.NodeID) - if err != nil { - return BusStop{}, err - } - longitude, err := strconv.Atoi(s.Longitude) - if err != nil { - return BusStop{}, err - } - lat, lon := ConvertCoordinates(s.Latitude, longitude) - return BusStop{ - StopID: s.StopID, - NodeID: nodeID, - Description: s.Description, - Longitude: ceilN(lon, 6), - Latitude: ceilN(lat, 6), - MobileCode: s.MobileCode, - MobileName: s.MobileName, - }, nil -} - -func convertBusStops(s atb.BusStops) (BusStops, error) { - stops := make([]BusStop, 0, len(s.Stops)) - for _, stop := range s.Stops { - converted, err := convertBusStop(stop) - if err != nil { - return BusStops{}, err - } - stops = append(stops, converted) - } - return BusStops{Stops: stops}, nil -} - -// ConvertTime converts time from AtBs format to ISO 8601. -func ConvertTime(src string) (string, error) { - t, err := time.Parse("02.01.2006 15:04", src) - if err != nil { - return "", err - } - return t.Format("2006-01-02T15:04:05.000"), nil -} - -// IsRealtime returns a boolean indicating whether stationForecast is realtime. -func IsRealtime(stationForecast string) bool { - return strings.EqualFold(stationForecast, "prev") -} - -func convertForecast(f atb.Forecast) (Departure, error) { - registeredDeparture, err := ConvertTime(f.RegisteredDepartureTime) - if err != nil { - return Departure{}, err - } - scheduledDeparture, err := ConvertTime(f.ScheduledDepartureTime) - if err != nil { - return Departure{}, err - } - return Departure{ - LineID: f.LineID, - Destination: f.Destination, - RegisteredDepartureTime: registeredDeparture, - ScheduledDepartureTime: scheduledDeparture, - IsRealtimeData: IsRealtime(f.StationForecast), - }, nil -} - -// IsTowardsCentrum returns a boolean indicating whether a bus stop, identified -// by nodeID, is going to the centrum. -func IsTowardsCentrum(nodeID int) bool { - return (nodeID/1000)%2 == 1 -} - -// ConvertCoordinates converts latitude and longitude from EPSG:3785 to -// EPSG:4326. -func ConvertCoordinates(latitude, longitude int) (float64, float64) { - const earthRadius = 6378137 - - originShift := (2 * math.Pi * earthRadius) / 2 - - lat := (float64(latitude) / originShift) * 180 - lon := (float64(longitude) / originShift) * 180 - - lat = 180 / math.Pi * (2*math.Atan( - math.Exp(lat*math.Pi/180)) - math.Pi/2) - return lat, lon -} - -func ceilN(f float64, n int) float64 { - shift := math.Pow(10, float64(n)) - return math.Ceil(f*shift) / shift -} - -func convertForecasts(f atb.Forecasts) (Departures, error) { - towardsCentrum := false - if len(f.Nodes) > 0 { - nodeID, err := strconv.Atoi(f.Nodes[0].NodeID) - if err != nil { - return Departures{}, err - } - towardsCentrum = IsTowardsCentrum(nodeID) - } - departures := make([]Departure, 0, len(f.Forecasts)) - for _, forecast := range f.Forecasts { - departure, err := convertForecast(forecast) - if err != nil { - return Departures{}, err - } - departures = append(departures, departure) - } - return Departures{ - TowardsCentrum: &towardsCentrum, - Departures: departures, - }, nil -} - func convertDepartures(enturDepartures []entur.Departure) Departures { departures := make([]Departure, 0, len(enturDepartures)) const timeLayout = "2006-01-02T15:04:05.000" @@ -209,33 +70,3 @@ func convertDepartures(enturDepartures []entur.Departure) Departures { Departures: departures, } } - -// GeoJSON converts BusStop into the GeoJSON format. -func (s *BusStop) GeoJSON() GeoJSON { - geometry := Geometry{ - Type: "Point", - Coordinates: []float64{s.Longitude, s.Latitude}, - } - properties := map[string]interface{}{ - "name": s.Description, - "busstop": s, - } - return GeoJSON{ - Type: "Feature", - Geometry: geometry, - Properties: properties, - } -} - -// GeoJSON converts BusStops into a GeoJSON feature collection. -func (s *BusStops) GeoJSON() GeoJSONCollection { - features := make([]GeoJSON, 0, len(s.Stops)) - for i := range s.Stops { - geoJSON := s.Stops[i].GeoJSON() - features = append(features, geoJSON) - } - return GeoJSONCollection{ - Type: "FeatureCollection", - Features: features, - } -} diff --git a/http/types_test.go b/http/types_test.go deleted file mode 100644 index 9550001..0000000 --- a/http/types_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package http - -import ( - "reflect" - "testing" - - "github.com/mpolden/atb/atb" -) - -func TestConvertBusStop(t *testing.T) { - stop := atb.BusStop{ - StopID: 100633, - NodeID: "16011376", - Description: "Prof. Brochs gt", - Longitude: "1157514", - Latitude: 9202874, - MobileCode: "16011376 (Prof.)", - MobileName: "Prof. (16011376)", - } - expected := BusStop{ - StopID: 100633, - NodeID: 16011376, - Description: "Prof. Brochs gt", - Longitude: 10.398126, - Latitude: 63.415535, - MobileCode: "16011376 (Prof.)", - MobileName: "Prof. (16011376)", - } - actual, err := convertBusStop(stop) - if err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("Expected %+v, got %+v", expected, actual) - } -} - -func TestConvertBusStops(t *testing.T) { - stops := atb.BusStops{ - Stops: []atb.BusStop{{ - NodeID: "16011376", - Longitude: "1157514", - Latitude: 9202874, - }}} - expected := BusStops{ - Stops: []BusStop{{ - NodeID: 16011376, - Longitude: 10.398126, - Latitude: 63.415535, - }}} - actual, err := convertBusStops(stops) - if err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("Expected %+v, got %+v", expected, actual) - } -} - -func TestConvertTime(t *testing.T) { - time, err := ConvertTime("26.02.2015 18:38") - if err != nil { - t.Fatal(err) - } - expected := "2015-02-26T18:38:00.000" - if time != expected { - t.Fatalf("Expected %s, got %s", expected, time) - } -} - -func TestIsRealtime(t *testing.T) { - if !IsRealtime("prev") { - t.Fatal("Expected true") - } - if !IsRealtime("Prev") { - t.Fatal("Expected true") - } - if IsRealtime("foo") { - t.Fatal("Expected false") - } -} - -func TestConvertForecast(t *testing.T) { - forecast := atb.Forecast{ - LineID: "6", - LineDescription: "6", - RegisteredDepartureTime: "26.02.2015 18:38", - ScheduledDepartureTime: "26.02.2015 18:01", - StationForecast: "Prev", - Destination: "Munkegata M5", - } - expected := Departure{ - LineID: "6", - Destination: "Munkegata M5", - RegisteredDepartureTime: "2015-02-26T18:38:00.000", - ScheduledDepartureTime: "2015-02-26T18:01:00.000", - IsRealtimeData: true, - } - actual, err := convertForecast(forecast) - if err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("Expected %+v, got %+v", expected, actual) - } -} - -func TestIsTowardsCentrum(t *testing.T) { - if !IsTowardsCentrum(16011376) { - t.Fatal("Expected true") - } - if IsTowardsCentrum(16010376) { - t.Fatal("Expected false") - } -} - -func TestConvertForecasts(t *testing.T) { - forecasts := atb.Forecasts{ - Nodes: []atb.NodeInfo{{NodeID: "16011376"}}, - Forecasts: []atb.Forecast{{ - RegisteredDepartureTime: "26.02.2015 18:38", - ScheduledDepartureTime: "26.02.2015 18:01", - }}} - b := true - expected := Departures{TowardsCentrum: &b, - Departures: []Departure{{ - RegisteredDepartureTime: "2015-02-26T18:38:00.000", - ScheduledDepartureTime: "2015-02-26T18:01:00.000", - IsRealtimeData: false, - }}} - actual, err := convertForecasts(forecasts) - if err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("Expected %+v, got %+v", expected, actual) - } -} - -func TestConvertCoordinates(t *testing.T) { - // Prof. Brochs gate - latitude, longitude := 9202565, 1157522 - lat, lon := ConvertCoordinates(latitude, longitude) - if expected := 63.41429265308724; lat != expected { - t.Fatalf("Expected %f, got %f", expected, lat) - } - if expected := 10.398197043045966; lon != expected { - t.Fatalf("Expected %f, got %f", expected, lon) - } - - // Ilsvika - latitude, longitude = 9206756, 1152920 - lat, lon = ConvertCoordinates(latitude, longitude) - if expected := 63.43113671582598; lat != expected { - t.Fatalf("Expected %f, got %f", expected, lat) - } - if expected := 10.356856573670786; lon != expected { - t.Fatalf("Expected %f, got %f", expected, lon) - } -} - -func TestCeilN(t *testing.T) { - expected := 1.234567 - actual := ceilN(1.2345661, 6) - if actual != expected { - t.Fatalf("Expected %f, got %f", expected, actual) - } - expected = 1.234567 - actual = ceilN(1.2345665, 6) - if actual != expected { - t.Fatalf("Expected %f, got %f", expected, actual) - } -} -- cgit v1.2.3