aboutsummaryrefslogtreecommitdiffstats
path: root/entur/entur.go
blob: d68c76443bb8bf5a1bbce944814f1f81ccdbd290 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
package entur

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"
	"time"
)

// DefaultURL is the default Entur Journey Planner API URL. Documentation at
// https://developer.entur.org/pages-journeyplanner-journeyplanner-v2.
const DefaultURL = "https://api.entur.io/journey-planner/v2/graphql"

// Client implements a client for the Entur Journey Planner API.
type Client struct{ URL string }

// New creates a new client using the API found at url.
func New(url string) *Client {
	if url == "" {
		url = DefaultURL
	}
	return &Client{URL: url}
}

// Departure represents a bus departure from a stop.
type Departure struct {
	Line                    string
	RegisteredDepartureTime time.Time
	ScheduledDepartureTime  time.Time
	Destination             string
	IsRealtime              bool
	Inbound                 bool
}

type response struct {
	Data data `json:"data"`
}

type data struct {
	StopPlace stopPlace `json:"stopPlace"`
}

type stopPlace struct {
	ID             string          `json:"id"`
	Name           string          `json:"name"`
	EstimatedCalls []estimatedCall `json:"estimatedCalls"`
}

type estimatedCall struct {
	Realtime              bool               `json:"realtime"`
	ExpectedDepartureTime string             `json:"expectedDepartureTime"`
	ActualDepartureTime   string             `json:"actualDepartureTime"`
	DestinationDisplay    destinationDisplay `json:"destinationDisplay"`
	ServiceJourney        serviceJourney     `json:"serviceJourney"`
}

type destinationDisplay struct {
	FrontText string `json:"frontText"`
}

type serviceJourney struct {
	Operator operator `json:"operator"`
	JourneyPattern journeyPattern `json:"journeyPattern"`
}

type operator struct {
	Id string `json:"id"`
}

type journeyPattern struct {
	DirectionType string `json:"directionType"`
	Line          line   `json:"line"`
}

type line struct {
	PublicCode string `json:"publicCode"`
}

// Departures returns departures from the given stop ID. Use https://stoppested.entur.org/ to determine stop IDs.
func (c *Client) Departures(count, stopID int) ([]Departure, error) {
	// https://api.entur.io/journey-planner/v2/ide/ for query testing
	query := fmt.Sprintf(`{"query":"{stopPlace(id:\"NSR:StopPlace:%d\"){id name estimatedCalls(numberOfDepartures:%d){realtime expectedDepartureTime actualDepartureTime destinationDisplay{frontText}serviceJourney{operator{id}journeyPattern{directionType line{publicCode}}}}}}"}`, stopID, count)
	req, err := http.NewRequest("POST", c.URL, strings.NewReader(query))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")
	// Identify this client. See https://developer.entur.org/pages-journeyplanner-journeyplanner-v2
	req.Header.Set("ET-Client-Name", "github_mpolden-atb")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	json, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	return parseDepartures(json)
}

func parseDepartures(jsonData []byte) ([]Departure, error) {
	var r response
	if err := json.Unmarshal(jsonData, &r); err != nil {
		return nil, err
	}
	const operatorPrefix = "ATB:"
	const timeLayout = "2006-01-02T15:04:05-0700"
	departures := make([]Departure, 0, len(r.Data.StopPlace.EstimatedCalls))
	for _, ec := range r.Data.StopPlace.EstimatedCalls {
		if !strings.HasPrefix(ec.ServiceJourney.Operator.Id, operatorPrefix) {
			continue // Skip other operators
		}
		scheduledDepartureTime, err := time.Parse(timeLayout, ec.ExpectedDepartureTime)
		if err != nil {
			return nil, err
		}
		registeredDepartureTime := time.Time{}
		if ec.ActualDepartureTime != "" {
			t, err := time.Parse(timeLayout, ec.ActualDepartureTime)
			if err != nil {
				return nil, err
			}
			registeredDepartureTime = t
		}
		inbound := ec.ServiceJourney.JourneyPattern.DirectionType == "inbound"
		d := Departure{
			Line:                    ec.ServiceJourney.JourneyPattern.Line.PublicCode,
			RegisteredDepartureTime: registeredDepartureTime,
			ScheduledDepartureTime:  scheduledDepartureTime,
			Destination:             ec.DestinationDisplay.FrontText,
			IsRealtime:              ec.Realtime,
			Inbound:                 inbound,
		}
		departures = append(departures, d)
	}
	return departures, nil
}