package tfl
import (
"context"
"fmt"
)
// GetAccidentDetails gets all accident details for accidents occurring in the specified year.
//
// https://api.tfl.gov.uk/swagger/ui/index.html?url=/swagger/docs/v1#!/AccidentStats/AccidentStats_Get
func (c *Client) GetAccidentDetails(ctx context.Context, year int) ([]AccidentDetail, error) {
path := fmt.Sprintf("/AccidentStats/%d", year)
accidentDetails := []AccidentDetail{}
err := c.get(ctx, path, &accidentDetails)
if err != nil {
return nil, err
}
return accidentDetails, err
}
package tfl
import (
"context"
"fmt"
)
// GetAllBikePoints gets all bike point locations.
// The Place object has an addtionalProperties array which contains the nbBikes, nbDocks and nbSpaces numbers which
// give the status of the BikePoint. A mismatch in these numbers i.e. nbDocks - (nbBikes + nbSpaces) != 0 indicates
// broken docks.
//
// https://api.tfl.gov.uk/swagger/ui/index.html?url=/swagger/docs/v1#!/BikePoint/BikePoint_GetAll
func (c *Client) GetAllBikePoints(ctx context.Context) ([]Place, error) {
path := "/BikePoint"
places := []Place{}
err := c.get(ctx, path, &places)
if err != nil {
return nil, err
}
return places, err
}
// GetBikePoint gets the bike point with the given ID.
//
// https://api.tfl.gov.uk/swagger/ui/index.html?url=/swagger/docs/v1#!/BikePoint/BikePoint_Get
func (c *Client) GetBikePoint(ctx context.Context, bikePointID string) (*Place, error) {
path := fmt.Sprintf("/BikePoint/%s", bikePointID)
place := Place{}
err := c.get(ctx, path, &place)
if err != nil {
return nil, err
}
return &place, err
}
// SearchBikePoint searches for bike stations by their name, a bike point's name often contains information about the
// name of the street or nearby landmarks, for example. Note that the search result does not contain the
// PlaceProperties i.e. the status or occupancy of the BikePoint, to get that information you should retrieve the
// BikePoint by its id on /BikePoint/id.
//
// https://api.tfl.gov.uk/swagger/ui/index.html?url=/swagger/docs/v1#!/BikePoint/BikePoint_Search
func (c *Client) SearchBikePoint(ctx context.Context, searchQuery string) ([]Place, error) {
path := "/BikePoint/Search"
places := []Place{}
err := c.getWithQueryParams(ctx, path, map[string]string{
"query": searchQuery,
}, &places)
if err != nil {
return nil, err
}
return places, err
}
package tfl
import "fmt"
// HTTPError can be returned to represent a failed HTTP call.
type HTTPError struct {
Status string
Body string
}
func (e HTTPError) Error() string {
return fmt.Sprintf("%s: %s", e.Status, e.Body)
}
package conv
import "strings"
// StringSliceToString converts a string slice to a string.
func StringSliceToString(s []string) string {
return strings.Join(s, ",")
}
package test
import (
"os"
"testing"
"github.com/jamesalexatkin/tfl-golang"
"github.com/stretchr/testify/require"
)
const latLonEpsilon = 0.005
// SkipIfShort can be used to skip a test if the tests are run with the `-short` flag.
func SkipIfShort(t *testing.T) {
t.Helper()
if testing.Short() {
t.Skip()
}
}
// NewTestClient creates a new client for integration tests.
func NewTestClient() *tfl.Client {
appID := os.Getenv("APP_ID")
appKey := os.Getenv("APP_KEY")
return tfl.New(appID, appKey)
}
// RequireBikePointsEqual ensures that two bike points are equal.
// This uses a shallow comparison, leaving out additional properties which have modification timestamps
// and would cause tests to be more brittle.
func RequireBikePointsEqual(t *testing.T, expectedBikePoint *tfl.Place, bikePoint *tfl.Place) {
t.Helper()
require.Equal(t, expectedBikePoint.ID, bikePoint.ID)
require.Equal(t, expectedBikePoint.URL, bikePoint.URL)
require.Equal(t, expectedBikePoint.CommonName, bikePoint.CommonName)
require.Equal(t, expectedBikePoint.Distance, bikePoint.Distance)
require.Equal(t, expectedBikePoint.PlaceType, bikePoint.PlaceType)
require.Equal(t, expectedBikePoint.Children, bikePoint.Children)
require.Equal(t, expectedBikePoint.ChildrenURLs, bikePoint.ChildrenURLs)
require.InEpsilon(t, expectedBikePoint.Lat, bikePoint.Lat, latLonEpsilon)
require.InEpsilon(t, expectedBikePoint.Lon, bikePoint.Lon, latLonEpsilon)
}
package tfl
import (
"context"
"fmt"
"github.com/jamesalexatkin/tfl-golang/internal/conv"
)
// GetLineStatusByMode gets the line status of all lines for the given modes.
//
// https://api.tfl.gov.uk/swagger/ui/index.html?url=/swagger/docs/v1#!/Line/Line_StatusByMode
func (c *Client) GetLineStatusByMode(ctx context.Context, modes []string) ([]Status, error) {
path := fmt.Sprintf("/Line/Mode/%s/Status", conv.StringSliceToString(modes))
statuses := []Status{}
err := c.get(ctx, path, &statuses)
if err != nil {
return nil, err
}
return statuses, err
}
package tfl
import (
"context"
"fmt"
"strconv"
)
// GetActiveServiceTypes returns the service type active for a mode. Currently only supports tube.
//
// https://api.tfl.gov.uk/swagger/ui/index.html?url=/swagger/docs/v1#!/Mode/Mode_GetActiveServiceTypes
func (c *Client) GetActiveServiceTypes(ctx context.Context) ([]ActiveServiceType, error) {
path := "/Mode/ActiveServiceTypes"
activeServiceTypes := []ActiveServiceType{}
err := c.get(ctx, path, &activeServiceTypes)
if err != nil {
return nil, err
}
return activeServiceTypes, err
}
// GetArrivalPredictionsForMode gets the next arrival predictions for all stops of a given mode.
//
// https://api.tfl.gov.uk/swagger/ui/index.html?url=/swagger/docs/v1#!/Mode/Mode_Arrivals
func (c *Client) GetArrivalPredictionsForMode(ctx context.Context, mode string, count int) ([]Prediction, error) {
path := fmt.Sprintf("/Mode/%s/Arrivals", mode)
predictions := []Prediction{}
err := c.getWithQueryParams(ctx, path, map[string]string{"count": strconv.Itoa(count)}, &predictions)
if err != nil {
return nil, err
}
return predictions, err
}
package tfl
import (
"context"
"fmt"
"strings"
)
// GetCarParkOccupancy returns the occupancy for a car park with a given ID.
//
// https://api.tfl.gov.uk/swagger/ui/index.html?url=/swagger/docs/v1#!/Occupancy/Occupancy_Get
func (c *Client) GetCarParkOccupancy(ctx context.Context, carParkID string) (*CarParkOccupancy, error) {
path := fmt.Sprintf("/Occupancy/CarPark/%s", carParkID)
carParkOccupancy := CarParkOccupancy{}
err := c.get(ctx, path, &carParkOccupancy)
if err != nil {
return nil, err
}
return &carParkOccupancy, err
}
// GetAllCarParkOccupancies returns the occupancy for all car parks that have occupancy data.
//
// https://api.tfl.gov.uk/swagger/ui/index.html?url=/swagger/docs/v1#!/Occupancy/Occupancy_Get_0
func (c *Client) GetAllCarParkOccupancies(ctx context.Context) ([]CarParkOccupancy, error) {
path := "/Occupancy/CarPark"
carParkOccupancies := []CarParkOccupancy{}
err := c.get(ctx, path, &carParkOccupancies)
if err != nil {
return nil, err
}
return carParkOccupancies, err
}
// GetChargeConnectorOccupancy gets the occupancy for a charge connector with a given ID (sourceSystemPlaceId).
//
// https://api.tfl.gov.uk/swagger/ui/index.html?url=/swagger/docs/v1#!/Occupancy/Occupancy_GetChargeConnectorStatus
func (c *Client) GetChargeConnectorOccupancy(
ctx context.Context,
chargeConnectorIDs []string,
) (
[]ChargeConnectorOccupancy,
error,
) {
path := fmt.Sprintf("/Occupancy/ChargeConnector/%s", strings.Join(chargeConnectorIDs, ","))
chargeConnectorOccupancies := []ChargeConnectorOccupancy{}
err := c.get(ctx, path, &chargeConnectorOccupancies)
if err != nil {
return []ChargeConnectorOccupancy{}, err
}
return chargeConnectorOccupancies, err
}
// GetAllChargeConnectorOccupancies gets the occupancy for all charge connectors.
//
// https://api.tfl.gov.uk/swagger/ui/index.html?url=/swagger/docs/v1#!/Occupancy/Occupancy_GetAllChargeConnectorStatus
func (c *Client) GetAllChargeConnectorOccupancies(ctx context.Context) ([]ChargeConnectorOccupancy, error) {
path := "/Occupancy/ChargeConnector"
chargeConnectorOccupancies := []ChargeConnectorOccupancy{}
err := c.get(ctx, path, &chargeConnectorOccupancies)
if err != nil {
return nil, err
}
return chargeConnectorOccupancies, err
}
// GetBikePointOccupancies gets the occupancy for bike points.
//
// https://api.tfl.gov.uk/swagger/ui/index.html?url=/swagger/docs/v1#!/Occupancy/Occupancy_GetBikePointsOccupancies
func (c *Client) GetBikePointOccupancies(ctx context.Context, bikePointIDs []string) ([]BikePointOccupancy, error) {
path := fmt.Sprintf("/Occupancy/ChargeConnector/%s", strings.Join(bikePointIDs, ","))
bikePointOccupancies := []BikePointOccupancy{}
err := c.get(ctx, path, &bikePointOccupancies)
if err != nil {
return nil, err
}
return bikePointOccupancies, err
}
package tfl
import (
"context"
"encoding/json"
"io"
"net/http"
)
// APIBaseURL is the base URL domain for the TfL API.
const APIBaseURL = "https://api.tfl.gov.uk"
// Client provides a mechanism to interact with the TfL API.
type Client struct {
AppID string
AppKey string
APIBaseURL string
HTTPClient *http.Client
}
// New returns a new client.
func New(appID string, appKey string) *Client {
return &Client{
AppID: appID,
AppKey: appKey,
APIBaseURL: APIBaseURL,
HTTPClient: &http.Client{},
}
}
func (c *Client) getWithQueryParams(
ctx context.Context,
path string,
params map[string]string,
responseBody any,
) error {
path = c.APIBaseURL + path
req, err := http.NewRequestWithContext(ctx, http.MethodGet, path, nil)
if err != nil {
return err
}
req.Header.Add("User-Agent", "go")
q := req.URL.Query()
q.Add("app_id", c.AppID)
q.Add("app_key", c.AppKey)
for key, value := range params {
q.Add(key, value)
}
req.URL.RawQuery = q.Encode()
resp, err := c.HTTPClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyStr, _ := io.ReadAll(resp.Body)
return HTTPError{Status: resp.Status, Body: string(bodyStr)}
}
err = json.NewDecoder(resp.Body).Decode(responseBody)
if err != nil {
return err
}
return nil
}
func (c *Client) get(ctx context.Context, path string, responseBody any) error {
return c.getWithQueryParams(ctx, path, map[string]string{}, responseBody)
}
package tfl
import (
"context"
"fmt"
"strings"
)
// GetVehiclePredictions gets the predictions for a given list of vehicle IDs.
//
// https://api.tfl.gov.uk/swagger/ui/index.html?url=/swagger/docs/v1#!/Vehicle/Vehicle_Get
func (c *Client) GetVehiclePredictions(ctx context.Context, vehicleIDs []string) ([]Prediction, error) {
joinedIDs := strings.Join(vehicleIDs, ",")
path := fmt.Sprintf("/Vehicle/%s/Arrivals", joinedIDs)
predictions := []Prediction{}
err := c.get(ctx, path, &predictions)
if err != nil {
return nil, err
}
return predictions, err
}