374 lines
8.4 KiB
Go
374 lines
8.4 KiB
Go
package main
|
|
|
|
import "cmp"
|
|
import "encoding/json"
|
|
import "errors"
|
|
import "fmt"
|
|
import "net/http"
|
|
import "net/url"
|
|
import "slices"
|
|
import "strconv"
|
|
import "strings"
|
|
import "time"
|
|
|
|
type EFAClient interface {
|
|
GetName() string
|
|
BuildRequest(string) (*http.Request, error)
|
|
ParseResponse(*http.Response) (Departures, error)
|
|
}
|
|
|
|
func FetchDepartures(c EFAClient, stopId string) (Departures, error) {
|
|
req, err := c.BuildRequest(stopId)
|
|
if err != nil {
|
|
return Departures{}, err
|
|
}
|
|
|
|
// Send the request, wait max 10 seconds
|
|
client := http.Client{
|
|
Timeout: 10 * time.Second,
|
|
}
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
return Departures{}, err
|
|
}
|
|
|
|
return c.ParseResponse(res)
|
|
}
|
|
|
|
// JSON unmarshaling types for departure monitor API
|
|
|
|
type DMResponse struct {
|
|
DateTime DMDateTime `json:"dateTime"`
|
|
DepartureList []DMDeparture `json:"departureList"`
|
|
}
|
|
|
|
type DMDeparture struct {
|
|
StopName string `json:"stopName"`
|
|
Platform string `json:"platform"`
|
|
Countdown string `json:"countdown"`
|
|
DateTime DMDateTime `json:"dateTime"`
|
|
RealDateTime *DMDateTime `json:"realDateTime"`
|
|
ServingLine DMServingLine `json:"servingLine"`
|
|
OnwardStopSeq []DMStop `json:"onwardStopSeq"`
|
|
}
|
|
|
|
type DMServingLine struct {
|
|
Symbol string `json:"symbol"`
|
|
Direction string `json:"direction"`
|
|
}
|
|
|
|
type DMStop struct {
|
|
PlaceID string `json:"placeID"`
|
|
Place string `json:"place"`
|
|
NameWO string `json:"nameWO"`
|
|
}
|
|
|
|
type DMDateTime struct {
|
|
Hour string `json:"hour"`
|
|
Minute string `json:"minute"`
|
|
}
|
|
|
|
// Types for JSON marshaling
|
|
|
|
type Departures struct {
|
|
Departures []Departure `json:"departures"`
|
|
ServerTime string `json:"serverTime"`
|
|
}
|
|
|
|
type Departure struct {
|
|
Platform string `json:"platform"`
|
|
Symbol string `json:"symbol"`
|
|
Direction string `json:"direction"`
|
|
Leaving string `json:"leaving"`
|
|
Countdown int `json:"-"`
|
|
}
|
|
|
|
func ParseDMResponse(res *http.Response) (Departures, error) {
|
|
defer res.Body.Close()
|
|
var dmResponse DMResponse
|
|
err := json.NewDecoder(res.Body).Decode(&dmResponse)
|
|
if err != nil {
|
|
return Departures{}, err
|
|
}
|
|
|
|
return parseDepartures(dmResponse)
|
|
}
|
|
|
|
func parseDepartures(response DMResponse) (Departures, error) {
|
|
var ds []Departure
|
|
|
|
for _, d := range response.DepartureList {
|
|
direction := d.ServingLine.Direction
|
|
// If available, use name of last stop as direction
|
|
// Note that OnwardStopSeq is only populated if includeCompleteStopSeq=1 is set.
|
|
if len(d.OnwardStopSeq) != 0 {
|
|
last := d.OnwardStopSeq[len(d.OnwardStopSeq)-1]
|
|
if slices.Contains([]string{"5", "6"}, last.PlaceID) {
|
|
direction = last.NameWO
|
|
}
|
|
}
|
|
|
|
leaving := fmt.Sprintf("%s min", d.Countdown)
|
|
countdown, err := strconv.Atoi(d.Countdown)
|
|
if err != nil {
|
|
return Departures{}, errors.New("could not parse countdown")
|
|
}
|
|
|
|
isInThePast := countdown < 0
|
|
if isInThePast {
|
|
continue
|
|
}
|
|
|
|
if countdown == 0 {
|
|
leaving = "sofort"
|
|
}
|
|
if countdown > 20 {
|
|
dt := d.DateTime
|
|
if d.RealDateTime != nil {
|
|
dt = *d.RealDateTime
|
|
}
|
|
leaving = fmt.Sprintf("%02s:%02s", dt.Hour, dt.Minute)
|
|
}
|
|
|
|
ds = append(ds, Departure{
|
|
d.Platform,
|
|
d.ServingLine.Symbol,
|
|
direction,
|
|
leaving,
|
|
countdown,
|
|
})
|
|
}
|
|
|
|
slices.SortFunc(ds, func(a Departure, b Departure) int {
|
|
return cmp.Compare(a.Countdown, b.Countdown)
|
|
})
|
|
|
|
dt := response.DateTime
|
|
|
|
return Departures{
|
|
ds,
|
|
fmt.Sprintf("%02s:%02s", dt.Hour, dt.Minute),
|
|
}, nil
|
|
}
|
|
|
|
var allEfaClients []EFAClient = []EFAClient{
|
|
IVBEFAClient{},
|
|
BwegtEFAClient{},
|
|
KVVEFAClient{},
|
|
VAGEFAClient{},
|
|
VRNEFAClient{},
|
|
}
|
|
|
|
type IVBEFAClient struct {
|
|
}
|
|
|
|
func (c IVBEFAClient) GetName() string {
|
|
return "IVB"
|
|
}
|
|
|
|
func (c IVBEFAClient) BuildRequest(stopId string) (*http.Request, error) {
|
|
// curl https://smartinfo.ivb.at/api/JSON/PASSAGE'?stopID=64003&count=3'
|
|
req, err := http.NewRequest("GET", "https://smartinfo.ivb.at/api/JSON/PASSAGE", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
query := url.Values{}
|
|
query.Set("stopID", stopId)
|
|
req.URL.RawQuery = query.Encode()
|
|
|
|
return req, nil
|
|
}
|
|
|
|
func (c IVBEFAClient) ParseResponse(res *http.Response) (Departures, error) {
|
|
defer res.Body.Close()
|
|
var response []IVBResponseElem
|
|
err := json.NewDecoder(res.Body).Decode(&response)
|
|
if err != nil {
|
|
return Departures{}, err
|
|
}
|
|
|
|
var ds []Departure
|
|
|
|
for _, responseElem := range response {
|
|
if responseElem.Smartinfo == nil {
|
|
continue
|
|
}
|
|
si := responseElem.Smartinfo
|
|
|
|
leaving := si.Time
|
|
if leaving == "0 min" {
|
|
leaving = "sofort"
|
|
}
|
|
|
|
ds = append(ds, Departure{
|
|
si.Plabel,
|
|
si.Route,
|
|
si.Direction,
|
|
leaving,
|
|
// TODO
|
|
0,
|
|
})
|
|
}
|
|
|
|
return Departures{
|
|
ds,
|
|
fmt.Sprintf("%02d:%02d", 0, 0),
|
|
}, nil
|
|
}
|
|
|
|
// IVB smartinfo API unmarshaling types
|
|
type IVBResponseElem struct {
|
|
Smartinfo *IVBSmartInfo `json:"smartinfo"`
|
|
}
|
|
|
|
type IVBSmartInfo struct {
|
|
Uid string `json:"uid"`
|
|
Route string `json:"route"`
|
|
Direction string `json:"direction"`
|
|
Time string `json:"time"`
|
|
Plabel string `json:"plabel"`
|
|
}
|
|
|
|
type VRNEFAClient struct {
|
|
}
|
|
|
|
func (c VRNEFAClient) GetName() string {
|
|
return "VRN"
|
|
}
|
|
|
|
func (c VRNEFAClient) BuildRequest(stopId string) (*http.Request, error) {
|
|
// Create request object
|
|
req, err := http.NewRequest("GET", "https://www.vrn.de/mngvrn/XML_DM_REQUEST", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Configure our request
|
|
query := url.Values{}
|
|
query.Set("coordOutputFormat", "EPSG:4326")
|
|
query.Set("depType", "stopEvents")
|
|
query.Set("includeCompleteStopSeq", "0")
|
|
query.Set("limit", "10")
|
|
query.Set("locationServerActive", "0")
|
|
query.Set("mode", "direct")
|
|
query.Set("name_dm", stopId)
|
|
query.Set("outputFormat", "json")
|
|
query.Set("type_dm", "stop")
|
|
query.Set("useOnlyStops", "1")
|
|
query.Set("useRealtime", "1")
|
|
req.URL.RawQuery = query.Encode()
|
|
|
|
return req, nil
|
|
}
|
|
|
|
func (c VRNEFAClient) ParseResponse(res *http.Response) (Departures, error) {
|
|
return ParseDMResponse(res)
|
|
}
|
|
|
|
type KVVEFAClient struct {
|
|
}
|
|
|
|
func (c KVVEFAClient) GetName() string {
|
|
return "KVV"
|
|
}
|
|
|
|
func (c KVVEFAClient) BuildRequest(stopId string) (*http.Request, error) {
|
|
form := url.Values{}
|
|
form.Set("action", "XSLT_DM_REQUEST")
|
|
form.Set("name_dm", stopId)
|
|
form.Set("type_dm", "stop")
|
|
form.Set("useRealtime", "1")
|
|
form.Set("limit", "10")
|
|
form.Set("mode", "direct")
|
|
form.Set("useRealtime", "1")
|
|
form.Set("outputFormat", "json")
|
|
body := strings.NewReader(form.Encode())
|
|
|
|
req, err := http.NewRequest("POST", "https://www.kvv.de/tunnelEfaDirect.php", body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.Header.Set("User-Agent", "coolio/1.0")
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
return req, nil
|
|
}
|
|
|
|
func (c KVVEFAClient) ParseResponse(res *http.Response) (Departures, error) {
|
|
return ParseDMResponse(res)
|
|
}
|
|
|
|
type BwegtEFAClient struct {
|
|
}
|
|
|
|
func (c BwegtEFAClient) GetName() string {
|
|
return "bwegt"
|
|
}
|
|
|
|
func (c BwegtEFAClient) BuildRequest(stopId string) (*http.Request, error) {
|
|
// Create request object
|
|
req, err := http.NewRequest("GET", "https://www.bwegt.de/bwegt-efa/XML_DM_REQUEST", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Configure our request
|
|
query := url.Values{}
|
|
query.Set("coordOutputFormat", "EPSG:4326")
|
|
query.Set("depType", "stopEvents")
|
|
query.Set("includeCompleteStopSeq", "0")
|
|
query.Set("limit", "10")
|
|
query.Set("locationServerActive", "0")
|
|
query.Set("mode", "direct")
|
|
query.Set("name_dm", stopId)
|
|
query.Set("outputFormat", "json")
|
|
query.Set("type_dm", "stop")
|
|
query.Set("useOnlyStops", "1")
|
|
query.Set("useRealtime", "1")
|
|
req.URL.RawQuery = query.Encode()
|
|
|
|
return req, nil
|
|
}
|
|
|
|
func (c BwegtEFAClient) ParseResponse(res *http.Response) (Departures, error) {
|
|
return ParseDMResponse(res)
|
|
}
|
|
|
|
type VAGEFAClient struct {
|
|
}
|
|
|
|
func (c VAGEFAClient) GetName() string {
|
|
return "VAG"
|
|
}
|
|
|
|
func (c VAGEFAClient) BuildRequest(stopId string) (*http.Request, error) {
|
|
// Create request object
|
|
req, err := http.NewRequest("GET", "https://efa.vagfr.de/vagfr3/XSLT_DM_REQUEST", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Configure our request
|
|
query := url.Values{}
|
|
query.Set("coordOutputFormat", "EPSG:4326")
|
|
query.Set("depType", "stopEvents")
|
|
query.Set("includeCompleteStopSeq", "0")
|
|
query.Set("limit", "10")
|
|
query.Set("locationServerActive", "0")
|
|
query.Set("mode", "direct")
|
|
query.Set("name_dm", stopId)
|
|
query.Set("outputFormat", "json")
|
|
query.Set("type_dm", "stop")
|
|
query.Set("useOnlyStops", "1")
|
|
query.Set("useRealtime", "1")
|
|
req.URL.RawQuery = query.Encode()
|
|
|
|
return req, nil
|
|
}
|
|
|
|
func (c VAGEFAClient) ParseResponse(res *http.Response) (Departures, error) {
|
|
return ParseDMResponse(res)
|
|
}
|