initial db-vendo with /journeys (wip)

This commit is contained in:
Traines 2024-12-07 16:16:31 +00:00
parent e9211e8105
commit 2e094c2b78
357 changed files with 6867 additions and 95442 deletions

4
.gitmodules vendored
View file

@ -1,4 +0,0 @@
[submodule "tools/transport-apis"]
path = tools/transport-apis
url = https://github.com/public-transport/transport-apis.git
branch = v1

26
api.js Normal file
View file

@ -0,0 +1,26 @@
import {createClient} from './index.js'
import {profile as dbProfile} from './p/db/index.js'
import {createHafasRestApi as createApi} from 'hafas-rest-api'
const config = {
hostname: process.env.HOSTNAME || 'localhost',
port: process.env.PORT ? parseInt(process.env.PORT) : 3000,
name: "db-vendo-client",
description: "db-vendo-client",
homepage: "https://github.com/public-transport/db-vendo-client",
version: "7",
docsLink: 'https://github.com/public-transport/db-vendo-client',
openapiSpec: true,
logging: true,
aboutPage: true,
etags: 'strong',
csp: `default-src 'none' style-src 'self' 'unsafe-inline' img-src https:`,
}
const vendo = createClient(dbProfile, 'my-hafas-rest-api')
const api = await createApi(vendo, config)
api.listen(3000, (err) => {
if (err) console.error(err)
})

View file

@ -1,17 +0,0 @@
# `hafas-client` API
- [`journeys(from, to, [opt])`](journeys.md) get journeys between locations
- [`refreshJourney(refreshToken, [opt])`](refresh-journey.md) fetch up-to-date/more details of a `journey`
- [`journeysFromTrip(tripId, previousStopover, to, [opt])`](journeys-from-trip.md) get journeys from a trip to a location
- [`trip(id, lineName, [opt])`](trip.md) get details for a trip
- [`tripsByName(lineNameOrFahrtNr, [opt])`](trips-by-name.md) get all trips matching a name
- [`departures(station, [opt])`](departures.md) query the next departures at a station
- [`arrivals(station, [opt])`](arrivals.md) query the next arrivals at a station
- [`locations(query, [opt])`](locations.md) find stations, POIs and addresses
- [`stop(id, [opt])`](stop.md) get details about a stop/station
- [`nearby(location, [opt])`](nearby.md) show stations & POIs around
- [`radar(north, west, south, east, [opt])`](radar.md) find all vehicles currently in a certain area
- [`reachableFrom(address, [opt])`](reachable-from.md)  get all stations reachable from an address within `n` minutes
- [`remarks([opt])`](remarks.md) get all remarks
- [`lines(query, [opt])`](lines.md) get all lines matching a name
- [`serverInfo([opt])`](server-info.md) fetch meta information from HAFAS

View file

@ -1,3 +0,0 @@
# `arrivals(station, [opt])`
Just like [`departures(station, [opt])`](departures.md), except that it resolves with arrival times instead of departure times.

File diff suppressed because it is too large Load diff

View file

@ -1,225 +0,0 @@
# `departures(station, [opt])`
`station` must be in one of these formats:
```js
// a station ID, in a format compatible to the profile you use
'900000013102'
// an FPTF `station` object
{
type: 'station',
id: '900000013102',
name: 'foo station',
location: {
type: 'location',
latitude: 1.23,
longitude: 3.21
}
}
```
With `opt`, you can override the default options, which look like this:
```js
{
when: new Date(),
direction: null, // only show departures heading to this station
line: null, // filter by line ID
duration: 10, // show departures for the next n minutes
results: null, // max. number of results; `null` means "whatever HAFAS wants"
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
linesOfStops: false, // parse & expose lines at the stop/station?
remarks: true, // parse & expose hints & warnings?
stopovers: false, // fetch & parse previous/next stopovers?
// departures at related stations
// e.g. those that belong together on the metro map.
includeRelatedStations: true,
language: 'en' // language to get results in
}
```
If you pass an object `opt.products`, its fields will partially override the default products defined in the profile. An example with the [BVG profile](../p/bvg):
```js
import {createClient} from 'hafas-client'
import {profile as vbbProfile} from 'hafas-client/p/vbb/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
const client = createClient(vbbProfile, userAgent)
// will query with these products: suburban, subway, bus, express, regional
await client.departures('900000024101', {products: {tram: false, ferry: false}})
```
## Response
*Note:* As stated in the [*Friendly Public Transport Format* v2 draft spec](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md), the `when` field includes the current delay. The `delay` field, if present, expresses how much the former differs from the schedule.
You may pass a departure's `tripId` into [`trip(id, lineName, [opt])`](trip.md) to get details on the whole trip.
As an example, we're going to use the [VBB profile](../p/vbb):
```js
import {createClient} from 'hafas-client'
import {profile as vbbProfile} from 'hafas-client/p/vbb/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
const client = createClient(vbbProfile, userAgent)
// S Charlottenburg
const {
departures,
realtimeDataUpdatedAt,
} = await client.departures('900000024101', {duration: 3})
```
`realtimeDataUpdatedAt` is a UNIX timestamp reflecting the latest moment when (at least some) of the response's realtime data have been updated.
`departures` may look like this:
```js
[ {
tripId: '1|31431|28|86|17122017',
trip: 31431,
direction: 'S Spandau',
// Depending on the HAFAS endpoint, the destination may be present:
destination: {
type: 'stop',
id: '900000029101',
name: 'S Spandau',
location: {
type: 'location',
id: '900029101',
latitude: 52.534794,
longitude: 13.197477
},
products: {
suburban: true,
subway: true,
tram: false,
bus: true,
ferry: false,
express: true,
regional: true,
},
},
line: {
type: 'line',
id: '18299',
fahrtNr: '12345',
mode: 'train',
product: 'suburban',
public: true,
name: 'S9',
symbol: 'S',
nr: 9,
metro: false,
express: false,
night: false,
operator: {
type: 'operator',
id: 's-bahn-berlin-gmbh',
name: 'S-Bahn Berlin GmbH'
}
},
currentTripPosition: {
type: 'location',
latitude: 52.500851,
longitude: 13.283755,
},
stop: {
type: 'station',
id: '900000024101',
name: 'S Charlottenburg',
location: {
type: 'location',
latitude: 52.504806,
longitude: 13.303846
},
products: {
suburban: true,
subway: false,
tram: false,
bus: true,
ferry: false,
express: false,
regional: true
}
},
when: '2017-12-17T19:32:00+01:00',
plannedWhen: '2017-12-17T19:32:00+01:00',
delay: null,
platform: '2',
plannedPlatform: '2'
}, {
cancelled: true,
tripId: '1|30977|8|86|17122017',
trip: 30977,
direction: 'S Westkreuz',
line: {
type: 'line',
id: '16441',
fahrtNr: '54321',
mode: 'train',
product: 'suburban',
public: true,
name: 'S5',
symbol: 'S',
nr: 5,
metro: false,
express: false,
night: false,
operator: { /* … */ }
},
currentTripPosition: {
type: 'location',
latitude: 52.505004,
longitude: 13.322391,
},
stop: { /* … */ },
when: null,
plannedWhen: '2017-12-17T19:33:00+01:00'
delay: null,
platform: null,
plannedPlatform: '2',
prognosedPlatform: '2'
}, {
tripId: '1|28671|4|86|17122017',
trip: 28671,
direction: 'U Rudow',
line: {
type: 'line',
id: '19494',
fahrtNr: '11111',
mode: 'train',
product: 'subway',
public: true,
name: 'U7',
symbol: 'U',
nr: 7,
metro: false,
express: false,
night: false,
operator: { /* … */ }
},
currentTripPosition: {
type: 'location',
latitude: 52.49864,
longitude: 13.307622,
},
stop: { /* … */ },
when: '2017-12-17T19:35:00+01:00',
plannedWhen: '2017-12-17T19:35:00+01:00',
delay: 0,
platform: null,
plannedPlatform: null
} ]
```

View file

@ -1,160 +0,0 @@
# HAFAS `mgate.exe` protocol
The protocol of `mgate.exe` HAFAS endpoints is not openly (and freely) documented. The following documentation is based on general observations and reverse-engineering.
*Note:* There are also `rest.exe` (a.k.a. "open API", a.k.a. "REST API") endpoints. This documentation is *not* about them.
## date & time format
Dates are encoded as `YYYYMMDD`, time strings as `HHMMSS`. These are in the timezone configured on the HAFAS/server side, *per endpoint*.
Whenever HAFAS returns a time string that exceeds the day the response describes, it will add a "day offset". As an example, when you query departures at `2019-12-12T23:50+01:00` for the next 30 minutes, it will encode the departure at `2019-12-13T00:13+01:00` as `20191212` & `01001300`.
For working code, check out [`parseDateTime()`](../parse/date-time.js).
## coordinate format
All endpoints I've seen so far use [WGS84](http://wiki.gis.com/wiki/index.php/WGS84). Values are multiplied by `10^6` though, so you would encode `{latitude: 1.23, longitude: -2.34}` as `{Y: 1230000: X: -2340000}`. There's an optional parameter `z` with the elevation.
For working code, check out [`formatAddress()`](../format/address.js).
## querying the API
In many aspects, the API looks and feels like [RPCs](https://en.wikipedia.org/wiki/Remote_procedure_call). You must send queries via HTTP `POST`, with the minimal JSON body looking like this:
```js
{
"auth": {
"type": "AID",
"aid": "…" // endpoint-specific authentication token, e.g. `1Rxs112shyHLatUX4fofnmdxK`
},
"ver": "…", // endpoint-specific string, e.g. `1.15`
"ext": "…", // endpoint-specific string, e.g. `BVG.1`
"client": {
"type": "IPA", // might also be `IPH` for "iPhone" or `WEB` for "web client"
"id": "…", // endpoint-specific string, e.g. `BVG`
"name": "…", // endpoint-specific string, e.g. `FahrInfo`
"v": "…" // endpoint-specific string, e.g. `4070700`
},
"lang": "…", // language, sometimes 2-digit (e.g. `de`), sometimes 3-digit (e.g. `deu`)
"svcReqL": [
{
"meth": "…", // name of the API call, supported values depend on the endpoint
"req": {
// actual request parameters…
}
// some endpoints also require this:
"cfg": {
"cfgGrpL": [],
"cfgHash": "…" // endpoint-specific string
}
}
]
}
```
- The data in `client` must be correct, otherwise HAFAS will reject your request.
- HAFAS will return slightly different response formats (and slightly different levels of detail) for different `ver`, `ext` and `client.v` values.
- All endpoints known support JSON & UTF-8, so make sure to send `Accept: application/json` & `Accept-Charset: utf-8` headers.
- Most endpoints support at least GZIP compression, so make sure to send a `Accept-Encoding: gzip` header.
For working code, check out [`request()`](lib/request.js).
## Authentication
There are three known types of authentication used among `mgate.exe` endpoints.
For working code, check out [`hafas-client`'s `request()`](lib/request.js), [`public-transport-enabler`'s Java implementation](https://github.com/schildbach/public-transport-enabler/blob/69614c87af627e2feafc576882f2ccccdbf4b7e6/src/de/schildbach/pte/AbstractHafasClientInterfaceProvider.java#L845-L860), [`TripKit`'s Swift implementation](https://github.com/alexander-albers/tripkit/blob/724b6cd8c258c9c61e7443c81e914618b79393cb/TripKit/AbstractHafasClientInterfaceProvider.swift#L1473-L1495) or [`marudor.de`'s TypeScript implementation](https://github.com/marudor/BahnhofsAbfahrten/blob/cf64d53c6902981ec529d3952253b2c83bff9221/src/server/HAFAS/profiles.ts#L30-L54).
### unprotected endpoints
You can just query these, as long as you send a formally correct request.
### endpoints using the `checksum` query parameter
`checksum` is a [message authentication code](https://en.wikipedia.org/wiki/Message_authentication_code): You can compute it by [hashing](https://en.wikipedia.org/wiki/Hash_function) the request body and a secret *salt*.
This secret can be read from the config file inside the accompanying client app. There is no guide for this yet, so please [open an issue](https://github.com/public-transport/hafas-client/issues/new).
### endpoints using the `mic` & `mac` query parameters
`mic` is a [message integrity code](https://en.wikipedia.org/wiki/Message_authentication_code), the [hash](https://en.wikipedia.org/wiki/Hash_function) of the request body.
`mac` is a [message authentication code](https://en.wikipedia.org/wiki/Message_authentication_code), the hash of `mic` and a secret *salt*.
This secret can be read from the config file inside the accompanying client app. There is no guide for this yet, so please [open an issue](https://github.com/public-transport/hafas-client/issues/new).
## API responses
A minimal valid response looks like this:
```js
{
"ver": "…", // endpoint-specific string, e.g. `1.15`
"lang": "…", // language
"ext": "…", // endpoint-specific string, e.g. `BVG.1`
"id": "…", // unique ID for each response?
"svcResL": [
{
"meth": "StationBoard",
"err": "OK",
"res": {
// result of the API call
}
}
]
}
```
For working code, check out [`request()`](lib/request.js).
### parse error
todo: generic server error
```js
{
"ver": "…", // endpoint-specific string, e.g. `1.15`
"lang": "…", // language, sometimes 2-digit (e.g. `de`), sometimes 3-digit (e.g. `deu`)
"err": "PARSE", // error code
"errTxt": "…", // error message, not always present
"svcResL": []
}
```
### authentication error
```js
{
"ver": "…", // endpoint-specific string, e.g. `1.15`
"lang": "…", // language
"ext": "…", // endpoint-specific string, e.g. `BVG.1`
"err": "AUTH", // error code
"errTxt": "…", // error message, not always present
"svcResL": []
}
```
### API-call-specific error
```js
{
"ver": "…", // endpoint-specific string, e.g. `1.15`
"lang": "…", // language
"ext": "…", // endpoint-specific string, e.g. `BVG.1`
"svcResL": [
{
"meth": "StationBoard",
"err": "…", // error code, e.g. `H9300`
"errTxt": "…", // error message, e.g. `Unknown arrival station`
"res": {}
}
]
}
```
## more ressources
- [@Nakaner's `strecken.info` API docs](https://github.com/Nakaner/bahnstoerungen/tree/62a72b1e0f0255668500b438187ff65aef39242a/api-doc/db-strecken-info)
- [unfinished HAFAS glossary](https://gist.github.com/derhuerst/74b703e2a0fc64e4a0fa8fbb1f3a61b4)
- [various `mgate.exe` HTTP traffic recordings](https://gist.github.com/search?q=post+mgate.exe&ref=searchresults)

View file

@ -1,51 +0,0 @@
# `journeysFromTrip(tripId, previousStopover, to, [opt])`
`to` must be an [*Friendly Public Transport Format* (FPTF) `stop`](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md#stop) or [`station`](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md#station). See [`journeys()`](journeys.md) for details.
With `opt`, you can override the default options, which look like this:
```js
{
accessibility: 'none', // 'none', 'partial' or 'complete'
stopovers: false, // return stations on the way?
polylines: false, // return leg shapes?
transferTime: 0, // minimum time for a single transfer in minutes
tickets: false, // return tickets?
remarks: true // parse & expose hints & warnings?
}
```
## Response
*Note:* The returned `departure` and `arrival` times include the current delay. The `departureDelay`/`arrivalDelay` fields express how much they differ from `plannedDeparture`/`plannedArrival`, respectively.
As an example, we're going to use the [*Deutsche Bahn* profile](../p/db):
```js
import {createClient} from 'hafas-client'
import {profile as dbProfile} from 'hafas-client/p/db/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
const client = createClient(dbProfile, userAgent)
const berlinSüdkreuz = '8011113'
const münchenHbf = '8000261'
const kölnHbf = '8000207'
// find any journey from Berlin Südkreuz to München Hbf
const [journey] = await client.journeys(berlinSüdkreuz, münchenHbf, {results: 1, stopovers: true})
// find the ICE leg
const leg = journey.legs.find(l => l.line.product === 'nationalExpress')
// find the stopover at the stop you've just passed
const previousStopover = leg.stopovers.find(st => st.departure && new Date(st.departure) < Date.now())
// find journeys from the ICE train to Köln Hbf
const {
journeys,
realtimeDataUpdatedAt,
} = await client.journeysFromTrip(leg.id, previousStopover, kölnHbf)
```
`journeys` is an array of [FPTF `journey`s](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md#journey), as documented in [`journeys()`](journeys.md).
`realtimeDataUpdatedAt` is a UNIX timestamp reflecting the latest moment when (at least some of) the response's realtime data have been updated.

View file

@ -1,357 +0,0 @@
# `journeys(from, to, [opt])`
`from` and `to` each must be in one of these formats:
```js
// a station ID, in a format compatible to the profile you use
'900000013102'
// an FPTF `station` object
{
type: 'station',
id: '900000013102',
name: 'foo station',
location: {
type: 'location',
latitude: 1.23,
longitude: 3.21
}
}
// a point of interest, which is an FPTF `location` object
{
type: 'location',
id: '123',
poi: true,
name: 'foo restaurant',
latitude: 1.23,
longitude: 3.21
}
// an address, which is an FPTF `location` object
{
type: 'location',
address: 'foo street 1',
latitude: 1.23,
longitude: 3.21
}
```
With `opt`, you can override the default options, which look like this:
```js
{
// Use either `departure` or `arrival` to specify a date/time.
departure: new Date(),
arrival: null,
earlierThan: null, // ref to get journeys earlier than the last query
laterThan: null, // ref to get journeys later than the last query
results: null, // number of journeys  `null` means "whatever HAFAS returns"
via: null, // let journeys pass this station
stopovers: false, // return stations on the way?
transfers: -1, // Maximum nr of transfers. Default: Let HAFAS decide.
transferTime: 0, // minimum time for a single transfer in minutes
accessibility: 'none', // 'none', 'partial' or 'complete'
bike: false, // only bike-friendly journeys
walkingSpeed: 'normal', // 'slow', 'normal', 'fast'
// Consider walking to nearby stations at the beginning of a journey?
startWithWalking: true,
products: {
// these entries may vary from profile to profile
suburban: true,
subway: true,
tram: true,
bus: true,
ferry: true,
express: true,
regional: true
},
tickets: false, // return tickets? only available with some profiles
polylines: false, // return a shape for each leg?
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
remarks: true, // parse & expose hints & warnings?
scheduledDays: false, // parse which days each journey is valid on
language: 'en', // language to get results in
}
```
## Response
*Note:* As stated in the [*Friendly Public Transport Format* v2 draft spec](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md), the returned `departure` and `arrival` times include the current delay. The `departureDelay`/`arrivalDelay` fields express how much they differ from the schedule.
As an example, we're going to use the [VBB profile](../p/vbb):
```js
import {createClient} 'hafas-client'
import {profile as vbbProfile} from 'hafas-client/p/vbb/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
const client = createClient(vbbProfile, userAgent)
// Hauptbahnhof to Heinrich-Heine-Str.
await client.journeys('900000003201', '900000100008', {
results: 1,
stopovers: true
})
```
`journeys()` will resolve with an object with the following fields:
- `journeys`
- `earlierRef`/`laterRef` pass them as `opt.earlierThan`/`opt.laterThan` into another `journeys()` call to retrieve the next "page" of journeys
- `realtimeDataUpdatedAt` a UNIX timestamp reflecting the latest moment when (at least some of) the response's realtime data have been updated
This object might look like this:
```js
{
journeys: [ {
legs: [ {
tripId: '1|32615|6|86|10072018',
direction: 'S Ahrensfelde',
line: {
type: 'line',
id: '16845',
fahrtNr: '12345',
name: 'S7',
public: true,
mode: 'train',
product: 'suburban',
operator: {
type: 'operator',
id: 's-bahn-berlin-gmbh',
name: 'S-Bahn Berlin GmbH'
},
symbol: 'S',
nr: 7,
metro: false,
express: false,
night: false
},
currentLocation: {
type: 'location',
latitude: 52.51384,
longitude: 13.526806,
},
origin: {
type: 'station',
id: '900000003201',
name: 'S+U Berlin Hauptbahnhof',
location: {
type: 'location',
latitude: 52.52585,
longitude: 13.368928
},
products: {
suburban: true,
subway: true,
tram: true,
bus: true,
ferry: false,
express: true,
regional: true
}
},
departure: '2018-07-10T23:54:00+02:00',
plannedDeparture: '2018-07-10T23:53:00+02:00',
departureDelay: 60,
departurePlatform: '15',
plannedDeparturePlatform: '14',
destination: {
type: 'station',
id: '900000100004',
name: 'S+U Jannowitzbrücke',
location: {
type: 'location',
latitude: 52.504806,
longitude: 13.303846
},
products: { /* … */ }
},
arrival: '2018-07-11T00:02:00+02:00',
plannedArrival: '2018-07-11T00:01:00+02:00',
arrivalDelay: 60,
arrivalPlatform: '3',
plannedArrivalPlatform: '3',
stopovers: [ {
stop: {
type: 'station',
id: '900000003201',
name: 'S+U Berlin Hauptbahnhof',
/* … */
},
arrival: null,
plannedArrival: null,
arrivalPlatform: null,
plannedArrivalPlatform: null,
departure: null,
plannedDeparture: null,
departurePlatform: null,
plannedDeparturePlatform: null,
remarks: [
{type: 'hint', code: 'bf', text: 'barrier-free'},
{type: 'hint', code: 'FB', text: 'Bicycle conveyance'}
]
}, {
stop: {
type: 'station',
id: '900000100001',
name: 'S+U Friedrichstr.',
/* … */
},
cancelled: true,
arrival: null,
plannedArrival: '2018-07-10T23:55:00+02:00',
prognosedArrival: '2018-07-10T23:56:00+02:00',
arrivalDelay: 60,
arrivalPlatform: null,
plannedArrivalPlatform: null,
departure: null,
plannedDeparture: '2018-07-10T23:56:00+02:00',
prognosedDeparture: '2018-07-10T23:57:00+02:00',
departureDelay: 60,
departurePlatform: null,
plannedDeparturePlatform: null,
remarks: [ /* … */ ]
},
/* … */
{
stop: {
type: 'station',
id: '900000100004',
name: 'S+U Jannowitzbrücke',
/* … */
},
arrival: '2018-07-11T00:02:00+02:00',
plannedArrival: '2018-07-11T00:01:00+02:00',
arrivalDelay: 60,
arrivalPlatform: null,
plannedArrivalPlatform: null,
departure: '2018-07-11T00:02:00+02:00',
plannedDeparture: '2018-07-11T00:02:00+02:00',
departureDelay: null,
departurePlatform: null,
plannedDeparturePlatform: null,
remarks: [ /* … */ ]
} ]
}, {
public: true,
walking: true,
distance: 558,
origin: {
type: 'station',
id: '900000100004',
name: 'S+U Jannowitzbrücke',
location: { /* … */ },
products: { /* … */ }
},
departure: '2018-07-11T00:01:00+02:00',
destination: {
type: 'station',
id: '900000100008',
name: 'U Heinrich-Heine-Str.',
location: { /* … */ },
products: { /* … */ }
},
arrival: '2018-07-11T00:10:00+02:00'
} ]
} ],
earlierRef: '…', // use with the `earlierThan` option
laterRef: '…' // use with the `laterThan` option
realtimeDataUpdatedAt: 1531259400, // 2018-07-10T23:50:00+02
}
```
Some [profiles](../p) are able to parse the ticket information, if returned by the API. For example, if you pass `tickets: true` with the [VBB profile](../p/vbb), each `journey` will have a tickets array that looks like this:
```js
[ {
name: 'Berlin Tarifgebiet A-B: Einzelfahrausweis Regeltarif',
price: 2.8,
tariff: 'Berlin',
coverage: 'AB',
variant: 'adult',
amount: 1
}, {
name: 'Berlin Tarifgebiet A-B: Einzelfahrausweis Ermäßigungstarif',
price: 1.7,
tariff: 'Berlin',
coverage: 'AB',
variant: 'reduced',
amount: 1,
reduced: true
}, /* … */ {
name: 'Berlin Tarifgebiet A-B: Tageskarte Ermäßigungstarif',
price: 4.7,
tariff: 'Berlin',
coverage: 'AB',
variant: '1 day, reduced',
amount: 1,
reduced: true,
fullDay: true
}, /* … */ {
name: 'Berlin Tarifgebiet A-B: 4-Fahrten-Karte Regeltarif',
price: 9,
tariff: 'Berlin',
coverage: 'AB',
variant: '4x adult',
amount: 4
} ]
```
If a journey leg has been cancelled, a `cancelled: true` will be added. Also, `departure`/`departureDelay`/`departurePlatform` and `arrival`/`arrivalDelay`/`arrivalPlatform` will be `null`.
To get more journeys earlier/later than the current set of results, pass `earlierRef`/`laterRef` into `opt.earlierThan`/`opt.laterThan`. For example, query *later* journeys as follows:
```js
const hbf = '900000003201'
const heinrichHeineStr = '900000100008'
const res1 = await client.journeys(hbf, heinrichHeineStr)
const lastJourney = res1.journeys[res1.journeys.length - 1]
console.log('departure of last journey', lastJourney.legs[0].departure)
// get later journeys
const res2 = await client.journeys(hbf, heinrichHeineStr, {
laterThan: res1.laterRef
})
const firstLaterJourney = res2.journeys[res2.journeys.length - 1]
console.log('departure of first (later) journey', firstLaterJourney.legs[0].departure)
```
```
departure of last journey 2017-12-17T19:07:00+01:00
departure of first (later) journey 2017-12-17T19:19:00+01:00
```
If you pass `polylines: true`, each journey leg will have a `polyline` field. Refer to [the section in the `trip()` docs](trip.md#polyline-option) for details.
If you pass `scheduledDays: true`, each journey will have a `scheduledDays` object looking like this:
```js
{
'2018-01-01': true,
'2018-01-02': false,
// …
'2018-10-12': true,
'2018-10-13': true,
// …
'2019-01-02': false,
'2019-01-03': false
}
```

View file

@ -1,70 +0,0 @@
# `lines([opt])`
**Fetches all lines known to the HAFAS endpoint**, e.g. warnings about disruptions, planned construction work, and general notices about the operating situation.
## Example
As an example, we're going to use the [SVV profile](../p/svv):
```js
import {createClient} from 'hafas-client'
import {profile as svvProfile} from 'hafas-client/p/svv/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
const client = createClient(svvProfile, userAgent)
const {
lines,
realtimeDataUpdatedAt,
} = await client.lines('S1')
```
`realtimeDataUpdatedAt` is a UNIX timestamp reflecting the latest moment when (at least some of) the response's realtime data have been updated.
`lines` may look like this:
```js
[
{
"id": "obb-1-S1-V-j20-1",
"type": "line",
"name": "S1",
"public": true,
"mode": "train",
"product": "bahn-s-bahn",
"operator": {
"type": "operator",
"id": "montafonerbahn-ag",
"name": "Montafonerbahn AG"
},
"directions": [
"Bludenz Bahnhof",
"Bregenz Hafen Bahnhof",
"Lindau Hbf",
"Bregenz Bahnhof",
"Schruns Bahnhof",
"Lochau Bahnhof"
],
},
// …
{
"id": "svv-42-50-j20-2",
"type": "line",
"name": "S1",
"public": true,
"mode": "train",
"product": "bahn-s-bahn",
"operator": {
"type": "operator",
"id": "salzburg-ag-salzburger-lokalbahn",
"name": "Salzburg AG - Salzburger Lokalbahn"
},
"directions": [
"Lamprechtshausen Bahnhof",
"Salzburg Hauptbahnhof",
"Acharting S-Bahn",
"Weitwörth-Nussdorf Bahnhof"
],
},
]
```

View file

@ -1,71 +0,0 @@
# `locations(query, [opt])`
`query` must be an string (e.g. `'Alexanderplatz'`).
With `opt`, you can override the default options, which look like this:
```js
{
fuzzy: true // find only exact matches?
, results: 5 // how many search results?
, stops: true // return stops/stations?
, addresses: true
, poi: true // points of interest
, subStops: true // parse & expose sub-stops of stations?
, entrances: true // parse & expose entrances of stops/stations?
, linesOfStops: false // parse & expose lines at each stop/station?
, language: 'en' // language to get results in
}
```
## Response
As an example, we're going to use the [VBB profile](../p/vbb):
```js
import {createClient} from 'hafas-client'
import {profile as vbbProfile} from 'hafas-client/p/vbb/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
const client = createClient(vbbProfile, userAgent)
await client.locations('Alexanderplatz', {results: 3})
```
The result may look like this:
```js
[ {
type: 'stop',
id: '900000100003',
name: 'S+U Alexanderplatz',
location: {
type: 'location',
latitude: 52.521508,
longitude: 13.411267
},
products: {
suburban: true,
subway: true,
tram: true,
bus: true,
ferry: false,
express: false,
regional: true
}
}, { // point of interest
type: 'location',
id: '900980709',
poi: true,
name: 'Berlin, Holiday Inn Centre Alexanderplatz****',
latitude: 52.523549,
longitude: 13.418441
}, { // point of interest
type: 'location',
id: '900980176',
poi: true,
name: 'Berlin, Hotel Agon am Alexanderplatz',
latitude: 52.524556,
longitude: 13.420266
} ]
```

View file

@ -1,108 +0,0 @@
# Migrating to `hafas-client@5`
## If you use Node `8` ("Carbon")…
…migrate to Node `10` ("Dubnium"), sorry. [Node `8` is out of maintenance now](https://nodejs.org/en/about/releases/). 83f43c6
## new fields for departure/arrival time & delays
An arrival/departure now looks like this:
```js
{
when: null, // realtime/prognosed
plannedWhen: '2019-10-10T10:10+10:00',
platform: '3', // realtime/prognosed
plannedPlatform: '4'
}
```
A stopover/journey leg now will look like this:
```js
{
arrival: null, // realtime/prognosed
plannedArrival: '2019-10-10T10:10+10:00',
arrivalDelay: null,
arrivalPlatform: '3', // realtime/prognosed
plannedArrivalPlatform: '4',
departure: '2019-10-10T10:12+10:00', // realtime/prognosed
plannedDeparture: '2019-10-10T10:10+10:00',
departureDelay: 120, // seconds
departurePlatform: null, // realtime/prognosed
plannedDeparturePlatform: null
}
```
If the same stopover/journey leg is `cancelled: true`, it will look like this:
```js
{
arrival: null,
prognosedArrival: null,
plannedArrival: '2019-10-10T10:10+10:00',
arrivalDelay: null,
arrivalPlatform: null,
prognosedArrivalPlatform: '3',
plannedArrivalPlatform: '4',
departure: null,
prognosedDeparture: '2019-10-10T10:12+10:00',
plannedDeparture: '2019-10-10T10:10+10:00',
departureDelay: 120, // seconds
departurePlatform: null,
prognosedDeparturePlatform: null,
plannedDeparturePlatform: null
}
```
## If you use `journeys()`
…with the `walkingSpeed` option and a custom profile, add `journeysWalkingSpeed` to your profile. 937583e
…without the `results` option, but *expect* a certain number of results, you must pass `results` now. 0045587
## If you use `departures()`/`arrivals()` with the [BVG profile](../p/bvg)…
With the latest protocol version, the BVG endpoint doesn't support these options anymore:
- `stopovers`  Fetch & parse previous/next stopovers? Default: `false`
- `includeRelatedStations` Fepartures at related stations, e.g. those that belong together on the metro map? Default: `true`
2d72391
## If you use a custom profile…
Let's assume you have parse function looking like this:
```js
const createParseLine = (profile, opt, data) => (rawLine) => {
const operator = data.operators[rawLine.oprX] || null
if (operator && operator.name === 'foo') {
return {
type: 'line',
name: 'really special tram line',
mode: 'tram',
product: 'special-tram',
operator
}
}
return defaultParseLine(rawLine)
}
```
Adapt your parse function like this:
```diff
const createParseLine = (profile, opt, _) => (rawLine) => {
- const operator = data.operators[rawLine.oprX] || null
+ const operator = rawLine.operator || null
```
See also [`#127`](https://github.com/public-transport/hafas-client/pull/127).
If you use `icons` in `parseWarning`/`parseHint`, adapt the function(s) to take an object `data` as the first argument. You can access the list of *parsed* icons via `data.icons`, *parsed* warnings via `data.warnings`, etc. a229205 b36f0e3
## Other breaking changes
- `journey.price` will be `null` if there's no pricing data returned by the endpoint, instead of `{amount: null}`. 8fe277d

View file

@ -1,93 +0,0 @@
# Migrating to `hafas-client@6`
## If you use Node.js <16
… migrate to Node `16` ("Gallium"), sorry. [Node `10`, `12` & `14` are out of (active) LTS now](https://nodejs.org/en/about/releases/).
## If you use `hafas-client` via [CommonJS](https://en.wikipedia.org/wiki/CommonJS) …
… you'll have to either
- migrate your code to ECMAScript Modules (ESM), or
- use [dynamic `import()`](https://nodejs.org/docs/latest-v16.x/api/esm.html#import-expressions), or
- use a (somewhat hacky) tool like [`esm`](https://www.npmjs.com/package/esm).
For more background information, check out [MDN's ESM explainer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) and [Node.js's ESM docs](https://nodejs.org/docs/latest-v16.x/api/esm.html).
## If you use `departures()` or `arrivals()`
… adapt your code as follows:
- `departures()` now returns an object `{departures: […], realtimeDataUpdatedAt: …}`
- `arrivals()` now returns an object `{arrivals: […], realtimeDataUpdatedAt: …}`
### … with `opt.stopovers: true`
… check if this still works. If `hafas-client` throws "`opt.stopovers` is not supported by this endpoint", you'll have to use `trip()` for each departure/arrival to get its trip's stopovers.
Most profiles had to be upgraded to a newer HAFAS protocol version to still work, and newer HAFAS protocol versions don't support this flag anymore.
## If you use `journeys()`, `refreshJourney()` or `journeysFromTrip()`
… use `res.realtimeDataUpdatedAt` instead of `res.realtimeDataFrom`, it has been renamed.
## If you use `refreshJourney()`
… adapt your code as follows: it now returns an object `{journey: …, realtimeDataUpdatedAt: …}`.
## If you use `trip()`
… adapt your code as follows: it now returns an object `{trip: …, realtimeDataUpdatedAt: …}`.
… don't pass the `lineName` parameter anymore, it is not needed anymore and has been removed.
## If you use `tripsByName()`
… adapt your code as follows: it now returns an object `{trips: […], realtimeDataUpdatedAt: …}`.
## If you use `radar()`
… adapt your code as follows: it now returns an object `{movements: […], realtimeDataUpdatedAt: …}`.
## If you use `reachableFrom()`
… and it sometimes fails with a server error (a.k.a. HAFAS is unable to process the request), wrap it in a retry logic ([open an Issue](https://github.com/public-transport/hafas-client/issues/new) to get help). Automatic retries have been removed.
## If you use `remarks()`
… adapt your code as follows: it now returns an object `{remarks: […], realtimeDataUpdatedAt: …}`.
## If you use `lines()`
… adapt your code as follows: it now returns an object `{lines: […], realtimeDataUpdatedAt: …}`.
## If you use the DB profile …
… be aware that the `regionalExp` product has been renamed to `regionalExpress`. Among other places, you will notice this in `line.product`.
## If you use the BVG or VBB profile …
### … and rely on `stop.weight`
… use [`vbb-stations`](https://npmjs.com/package/vbb-stations) to get it instead. It has been removed from `hafas-client`.
### … and rely on 12-digit stop IDs …
… adapt your code to handle 9-digit (and sometimes 6-digit?) stop IDs. The translation logic has been removed from `hafas-client`.
## If you rely on `line.adminCode`
… be aware that `hafas-client` now doesn't remove trailing `-` characters anymore (e.g. `DBS---` instead of `DBS`).
## If you use the VBB profile …
### … and rely on `line.{symbol,nr,metro,express,night}`
… use [`vbb-parse-line`](https://npmjs.com/package/vbb-parse-line) with `line.name` by yourself. It has been removed from `hafas-client`.
### … and rely on `ticket.{amount,fullDay,group,tariff,coverage,variant}`
… use [`vbb-parse-ticket`](https://npmjs.com/package/vbb-parse-ticket) to parse details from the ticket identifier ([open an Issue](https://github.com/public-transport/hafas-client/issues/new) to get help). It has been removed from `hafas-client`.
## Other breaking changes
- `warning.fromLoc`/`warning.toLoc` are now called `warning.fromLocation`/`warning.toLocation`
- `trip()`/`tripsByName()`: remove `trip.reachable` (it didn't make sense anyways)

View file

@ -1,85 +0,0 @@
# `nearby(location, [opt])`
This method can be used to find stops/stations & POIs close to a location. Note that it is not supported by every profile/endpoint.
`location` must be an [*FPTF* `location` object](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md#location-objects).
With `opt`, you can override the default options, which look like this:
```js
{
results: 8, // maximum number of results
distance: null, // maximum walking distance in meters
poi: false, // return points of interest?
stops: true, // return stops/stations?
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
linesOfStops: false, // parse & expose lines at each stop/station?
language: 'en' // language to get results in
}
```
## Response
As an example, we're going to use the [VBB profile](../p/vbb):
```js
import {createClient} from 'hafas-client'
import {profile as vbbProfile} from 'hafas-client/p/vbb/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
const client = createClient(vbbProfile, userAgent)
await client.nearby({
type: 'location',
latitude: 52.5137344,
longitude: 13.4744798
}, {distance: 400})
```
The result may look like this:
```js
[ {
type: 'stop',
id: '900000120001',
name: 'S+U Frankfurter Allee',
location: {
type: 'location',
latitude: 52.513616,
longitude: 13.475298
},
products: {
suburban: true,
subway: true,
tram: true,
bus: true,
ferry: false,
express: false,
regional: false
},
distance: 56
}, {
type: 'stop',
id: '900000120540',
name: 'Scharnweberstr./Weichselstr.',
location: {
type: 'location',
latitude: 52.512339,
longitude: 13.470174
},
products: { /* … */ },
distance: 330
}, {
type: 'stop',
id: '900000160544',
name: 'Rathaus Lichtenberg',
location: {
type: 'location',
latitude: 52.515908,
longitude: 13.479073
},
products: { /* … */ },
distance: 394
} ]
```

View file

@ -1,45 +0,0 @@
// Refer to the the ./writing-a-profile.md guide.
const products = [
{
id: 'commuterTrain',
mode: 'train',
bitmasks: [16],
name: 'ACME Commuter Rail',
short: 'CR',
default: true,
},
{
id: 'metro',
mode: 'train',
bitmasks: [8],
name: 'Foo Bar Metro',
short: 'M',
default: true,
},
];
const transformReqBody = (body) => {
// get these from the recorded app requests
// body.client = { … }
// body.ver = …
// body.auth = { … }
// body.lang = …
return body;
};
const insaProfile = {
// locale: …,
// timezone: …,
// endpoint: …,
transformReqBody,
products: products,
trip: false,
radar: false,
};
export {
insaProfile,
};

View file

@ -1,191 +0,0 @@
# `radar({north, west, south, east}, [opt])`
Use this method to find all vehicles currently in an area. Note that it is not supported by every profile/endpoint.
`north`, `west`, `south` and `eath` must be numbers (e.g. `52.52411`). Together, they form a [bounding box](https://en.wikipedia.org/wiki/Minimum_bounding_box).
With `opt`, you can override the default options, which look like this:
```js
{
results: 256, // maximum number of vehicles
duration: 30, // compute frames for the next n seconds
frames: 3, // nr of frames to compute
polylines: true, // return a track shape for each vehicle?
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
language: 'en' // language to get results in
}
```
## Response
*Note:* As stated in the [*Friendly Public Transport Format* v2 draft spec](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md), the returned `departure` and `arrival` times include the current delay. The `departureDelay`/`arrivalDelay` fields express how much they differ from the schedule.
As an example, we're going to use the [VBB profile](../p/vbb):
```js
import {createClient} from 'hafas-client'
import {profile as vbbProfile} from 'hafas-client/p/vbb/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
const client = createClient(vbbProfile, userAgent)
const {
movements,
realtimeDataUpdatedAt,
} = await client.radar({
north: 52.52411,
west: 13.41002,
south: 52.51942,
east: 13.41709
}, {results: 5})
```
`realtimeDataUpdatedAt` is a UNIX timestamp reflecting the latest moment when (at least some of) the response's realtime data have been updated.
`movements` may look like this:
```js
[ {
location: {
type: 'location',
latitude: 52.521508,
longitude: 13.411267
},
line: {
type: 'line',
id: 's9',
fahrtNr: '12345',
name: 'S9',
public: true,
mode: 'train',
product: 'suburban',
symbol: 'S',
nr: 9,
metro: false,
express: false,
night: false,
operator: {
type: 'operator',
id: 's-bahn-berlin-gmbh',
name: 'S-Bahn Berlin GmbH'
}
},
direction: 'S Flughafen Berlin-Schönefeld',
trip: 31463, // todo: outdated, should be tripId!
nextStopovers: [ {
stop: {
type: 'stop',
id: '900000029101',
name: 'S Spandau',
location: {
type: 'location',
latitude: 52.534794,
longitude: 13.197477
},
products: {
suburban: true,
subway: false,
tram: false,
bus: true,
ferry: false,
express: true,
regional: true
}
},
arrival: null,
plannedArrival: null,
arrivalDelay: null,
arrivalPlatform: null,
plannedArrivalPlatform: null,
departure: null,
plannedDeparture: '2017-12-17T19:16:00+01:00',
departureDelay: null,
departurePlatform: null,
plannedDeparturePlatform: '1'
} /* … */ ],
frames: [ {
origin: {
type: 'stop',
id: '900000100003',
name: 'S+U Alexanderplatz',
location: { /* … */ },
products: { /* … */ }
},
destination: {
type: 'stop',
id: '900000100004',
name: 'S+U Jannowitzbrücke',
location: { /* … */ },
products: { /* … */ }
},
t: 0
}, /* … */ {
origin: { /* Alexanderplatz */ },
destination: { /* Jannowitzbrücke */ },
t: 30000
} ]
}, {
location: {
type: 'location',
latitude: 52.523297,
longitude: 13.411151
},
line: {
type: 'line',
id: 'm2',
fahrtNr: '54321',
name: 'M2',
public: true,
mode: 'train',
product: 'tram',
symbol: 'M',
nr: 2,
metro: true,
express: false,
night: false,
operator: {
type: 'operator',
id: 'berliner-verkehrsbetriebe',
name: 'Berliner Verkehrsbetriebe'
}
},
direction: 'Heinersdorf',
trip: 26321,
nextStopovers: [ {
stop: { /* S+U Alexanderplatz/Dircksenstr. */ },
arrival: null,
plannedArrival: null,
arrivalDelay: null,
departure: null,
plannedAeparture: '2017-12-17T19:52:00+01:00',
departureDelay: null
}, {
stop: { /* Memhardstr. */ },
arrival: null,
plannedArrival: '2017-12-17T19:54:00+01:00',
arrivalDelay: null,
arrivalPlatform: null,
plannedArrivalPlatform: null,
departure: null,
plannedDeparture: '2017-12-17T19:54:00+01:00',
departureDelay: null,
departurePlatform: null,
plannedDeparturePlatform: '1'
}, /* … */ ],
frames: [ {
origin: { /* S+U Alexanderplatz/Dircksenstr. */ },
destination: { /* Memhardstr. */ },
t: 0
}, /* … */ {
origin: { /* Memhardstr. */ },
destination: { /* Mollstr./Prenzlauer Allee */ },
t: 30000
} ]
}, /* … */ ]
```
If you pass `polylines: true`, each movement will have a `polyline` field, as documented in [the corresponding section in the `trip()` docs](trip.md#polyline-option), with the exception that station info is missing.

View file

@ -1,98 +0,0 @@
# `reachableFrom(address, [opt])`
This method can be used to get stations reachable within a certain time from an address. This concept is called [isochrone diagram](https://en.wikipedia.org/wiki/Isochrone_map#Transportation_planning).
*Note*: It appears that HAFAS cannot generate actual isochrones, but only the list of reachable stations, which you can estimate the isochrone(s) from.
`address` must be an [*FPTF* `location` object](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md#location-objects).
With `opt`, you can override the default options, which look like this:
```js
{
when: new Date(),
maxTransfers: 5, // maximum of 5 transfers
maxDuration: 20, // maximum travel duration in minutes, pass `null` for infinite
products: {
// These entries may vary from profile to profile!
suburban: true,
subway: true
// …
},
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
}
```
## Response
`reachableFrom(address, [opt])` returns an array, in which each item has a `duration` and an array of [*Friendly Public Transport Format* `station`s](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md#station).
As an example, we're going to use the [VBB profile](../p/vbb):
```js
import {createClient} from 'hafas-client'
import {profile as vbbProfile} from 'hafas-client/p/vbb/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
const client = createClient(vbbProfile, userAgent)
const {
reachable,
realtimeDataUpdatedAt,
} = await client.reachableFrom({
type: 'location',
address: '13353 Berlin-Wedding, Torfstr. 17',
latitude: 52.541797,
longitude: 13.350042
}, {
maxDuration: 10 // minutes
})
```
`realtimeDataUpdatedAt` is a UNIX timestamp reflecting the latest moment when (at least some of) the response's realtime data have been updated.
`reachable` may look like this:
```js
[
{
duration: 2,
stations: [
{
type: 'stop',
id: '900000009101',
name: 'U Amrumer Str.',
location: {type: 'location', latitude: 52.542201, longitude: 13.34953},
products: { /* … */ }
}
]
}, {
duration: 3,
stations: [
{
type: 'stop',
id: '900000001201',
name: 'S+U Westhafen',
location: {type: 'location', latitude: 52.536179, longitude: 13.343839},
products: { /* … */ }
}
// …
]
},
// …
{
duration: 10,
stations: [
{
type: 'stop',
id: '900000001203',
name: 'Döberitzer Str.',
location: {type: 'location', latitude: 52.530668, longitude: 13.36811},
products: { /* … */ }
}
// …
]
}
]
```

View file

@ -1,189 +0,0 @@
# `hafas-client` documentation
**[API documentation](api.md)**
## Migrating from an old `hafas-client` version
- [`4` → `5` migration guide](migrating-to-5.md)
## Throttling requests
There's opt-in support for throttling requests to the endpoint.
```js
import {createClient} from 'hafas-client'
import {withThrottling} from 'hafas-client/throttle.js'
import {profile as dbProfile} from 'hafas-client/p/db/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
// create a throttled HAFAS client with Deutsche Bahn profile
const client = createClient(withThrottling(dbProfile), userAgent)
// Berlin Jungfernheide to München Hbf
await client.journeys('8011167', '8000261', {results: 1})
```
You can also pass custom values for the nr of requests (`limit`) per interval into `withThrottling`:
```js
// 2 requests per second
const throttledDbProfile = withThrottling(dbProfile, 2, 1000)
const client = createClient(throttledDbProfile, userAgent)
```
## Retrying failed requests
There's opt-in support for retrying failed requests to the endpoint.
```js
import {createClient} from 'hafas-client'
import {withRetrying} from 'hafas-client/retry.js'
import {profile as dbProfile} from 'hafas-client/p/db/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
// create a client with Deutsche Bahn profile that will retry on HAFAS errors
const client = createClient(withRetrying(dbProfile), userAgent)
```
You can pass custom options into `withRetrying`. They will be passed into [`retry`](https://github.com/tim-kos/node-retry#tutorial).
```js
// retry 2 times, after 10 seconds & 30 seconds
const retryingDbProfile = withRetrying(dbProfile, {
retries: 2,
minTimeout: 10 * 1000,
factor: 3
})
const client = createClient(retryingDbProfile, userAgent)
```
## User agent randomization
By default, `hafas-client` randomizes the client name that you pass into `createClient`, and sends it as [`User-Agent`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) in a randomized form:
```js
import {createClient} from 'hafas-client'
// …
const userAgent = 'my-awesome-program'
const client = createClient(someProfile, userAgent)
await client.journeys(/* … */)
// User-Agent: my-awee70429some-pre70429ogram
await client.journeys(/* … */)
// User-Agent: my-awesom9bb8e2e-prog9bb8e2ram
```
You can turn this off by setting `profile.randomizeUserAgent` to `false`:
```js
const client = createClient({
...someProfile,
randomizeUserAgent: false,
}, userAgent)
```
## Logging requests
You can use `profile.logRequest` and `profile.logResponse` to process the raw [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response), respectively.
As an example, we can implement a custom logger:
```js
import {createClient} from 'hafas-client'
import {profile as dbProfile} from 'hafas-client/p/db/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
const logRequest = (ctx, fetchRequest, requestId) => {
// ctx looks just like with the other profile.* hooks:
const {dbProfile, opt} = ctx
console.debug(requestId, fetchRequest.headers, fetchRequest.body + '')
}
const logResponse = (ctx, fetchResponse, body, requestId) => {
console.debug(requestId, fetchResponse.headers, body + '')
}
// create a client with Deutsche Bahn profile that debug-logs
const client = createClient({
...dbProfile,
logRequest,
logResponse,
}, userAgent)
```
```js
// logRequest output:
'29d0e3' {
accept: 'application/json',
'accept-encoding': 'gzip, br, deflate',
'content-type': 'application/json',
connection: 'keep-alive',
'user-agent': 'hafas842c51-clie842c51nt debug C842c51LI'
} {"lang":"de","svcReqL":[{"cfg":{"polyEnc":"GPA"},"meth":"LocMatch",…
// logResponse output:
'29d0e3' {
'content-encoding': 'gzip',
'content-length': '1010',
'content-type': 'application/json; charset=utf-8',
date: 'Thu, 06 Oct 2022 12:31:09 GMT',
server: 'Apache',
vary: 'User-Agent'
} {"ver":"1.45","lang":"deu","id":"sb42zgck4mxtxm4s","err":"OK","graph"…
```
The default `profile.logRequest` [`console.error`](https://nodejs.org/docs/latest-v10.x/api/console.html#console_console_error_data_args)s the request body, if you have set `$DEBUG` to `hafas-client`. Likewise, `profile.logResponse` `console.error`s the response body.
## Error handling
Unexpected errors e.g. due to bugs in `hafas-client` itself aside, its methods may reject with the following errors:
- `HafasError` A generic error to signal that something HAFAS-related went wrong, either in the client, or in the HAFAS endpoint.
- `HafasAccessDeniedError` The HAFAS endpoint has rejected your request because you're not allowed to access it (or the specific API call). Subclass of `HafasError`.
- `HafasInvalidRequestError` The HAFAS endpoint reports that an invalid request has been sent. Subclass of `HafasError`.
- `HafasNotFoundError` The HAFAS endpoint does not know about such stop/trip/etc. Subclass of `HafasError`.
- `HafasServerError` An error occured within the HAFAS endpoint, so that it can't fulfill the request; For example, this happens when HAFAS' internal backend is unavailable. Subclass of `HafasError`.
Each error has the following properties:
- `isHafasError` Always `true`. Allows you to differente HAFAS-related errors from e.g. network errors.
- `code` A string representing the error type for all other error classes, e.g. `INVALID_REQUEST` for `HafasInvalidRequestError`. `null` for plain `HafasError`s.
- `isCausedByServer` Boolean, telling you if the HAFAS endpoint says that it couldn't process your request because *it* is unavailable/broken.
- `hafasCode` A HAFAS-specific error code, if the HAFAS endpoint returned one; e.g. `H890` when no journeys could be found. `null` otherwise.
- `request` The [Fetch API `Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) of the request.
- `url` The URL of the request.
- `response` The [Fetch API `Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response).
To check **if an error from `hafas-client` is HAFAS-specific, use `error instanceof HafasError`**:
```js
import {HafasError} from 'hafas-client/lib/errors.js'
try {
await client.journeys(/* … */)
} catch (err) {
if (err instanceof HafasError) {
// HAFAS-specific error
} else {
// different error, e.g. network (ETIMEDOUT, ENETDOWN)
}
}
```
To determine **if you should automatically retry an error, use `!error.causedByServer`**.
## Using `hafas-client` from another language
If you want to use `hafas-client` to access HAFAS APIs but work with non-Node.js environments, you can use [`hafas-client-rpc`](https://github.com/derhuerst/hafas-client-rpc) to create a [JSON-RPC](https://www.jsonrpc.org) interface that you can send commands to.
## Writing a profile
Check [the guide](writing-a-profile.md).
## General documentation for `mgate.exe` APIs
[`hafas-mgate-api.md`](hafas-mgate-api.md)

View file

@ -1,42 +0,0 @@
# `refreshJourney(refreshToken, [opt])`
`refreshToken` must be a string, taken from `journey.refreshToken`.
With `opt`, you can override the default options, which look like this:
```js
{
stopovers: false, // return stations on the way?
polylines: false, // return a shape for each leg?
tickets: false, // return tickets? only available with some profiles
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
remarks: true, // parse & expose hints & warnings?
language: 'en' // language to get results in
}
```
## Response
As an example, we're going to use the [VBB profile](../p/vbb):
```js
import {createClient} from 'hafas-client'
import {profile as vbbProfile} from 'hafas-client/p/vbb/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
const client = createClient(vbbProfile, userAgent)
// Hauptbahnhof to Heinrich-Heine-Str.
const {journeys} = await client.journeys('900000003201', '900000100008', {results: 1})
// later, fetch up-to-date info on the journey
const {
journey,
realtimeDataUpdatedAt,
} = await client.refreshJourney(journeys[0].refreshToken, {stopovers: true, remarks: true})
```
`journey` is a *single* [*Friendly Public Transport Format* v2 draft](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md) `journey`, in the same format as returned by [`journeys()`](journeys.md).
`realtimeDataUpdatedAt` is a UNIX timestamp reflecting the latest moment when (at least some of) the response's realtime data have been updated.

View file

@ -1,150 +0,0 @@
# `remarks([opt])`
**Fetches all remarks known to the HAFAS endpoint**, e.g. warnings about disruptions, planned construction work, and general notices about the operating situation.
With `opt`, you can override the default options, which look like this:
```js
{
results: 100, // maximum number of remarks
// filter by time
from: Date.now(),
to: null,
products: null, // filter by affected products
language: 'en', // depends on the profile
}
```
## Example
As an example, we're going to use the [SVV profile](../p/svv):
```js
import {createClient} from 'hafas-client'
import {profile as svvProfile} from 'hafas-client/p/svv/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
const client = createClient(svvProfile, userAgent)
const {
remarks,
realtimeDataUpdatedAt,
} = await client.remarks()
```
`realtimeDataUpdatedAt` is a UNIX timestamp reflecting the latest moment when (at least some of) the response's realtime data have been updated.
`remarks` may look like this:
```js
[
{
id: 'HIM_FREETEXT_110342',
type: 'warning',
summary: 'Bus will be running at different times',
text: 'Due to operational changes, this bus will be running at different times. We apologise for any inconvenience this may cause.',
priority: 50,
company: 'KGÖVV',
validFrom: '2020-07-04T00:00:00+02:00',
validUntil: '2020-08-09T23:59:00+02:00',
modified: '2020-07-01T14:39:12+02:00',
products: {
'bahn-s-bahn': true,
'u-bahn': true,
strassenbahn: true,
fernbus: true,
regionalbus: true,
stadtbus: true,
'seilbahn-zahnradbahn': true,
schiff: true,
},
categories: [1],
icon: {type: 'HIM1', title: null},
},
// …
{
id: 'HIM_FREETEXT_110235',
type: 'warning',
summary: 'Linie 7 - Umleitungen',
text: 'Aufgrund einer Baustelle gibt es bei der Linie 7 umfangreiche Umleitungen.',
priority: 100,
company: 'VOR',
validFrom: '2020-07-13T00:00:00+02:00',
validUntil: '2020-08-31T23:59:00+02:00',
modified: '2020-06-30T12:37:38+02:00',
affectedLines: [{
type: 'line',
id: '7',
name: '7',
public: true,
}],
products: {
'bahn-s-bahn': true,
'u-bahn': true,
strassenbahn: true,
fernbus: true,
regionalbus: true,
stadtbus: true,
'seilbahn-zahnradbahn': false,
schiff: true,
},
categories: [1],
icon: {type: 'HIM1', title: null},
},
// …
{
id: 'HIM_FREETEXT_106619',
type: 'warning',
summary: 'Stop Bad Hall Bahnhofstraße can not be approached',
text: 'The stop at Bad Hall Bahnhofstraße can not be approached during 21.04.-24.07.2020. Please use alternatively the stop at Bad Hall Busterminal (Abzw Bahnhofstraße).',
priority: 100,
company: 'OÖVG',
validFrom: '2020-04-21T00:00:00+02:00',
validUntil: '2020-07-24T23:59:00+02:00',
modified: '2020-07-08T12:52:13+02:00',
affectedLines: [{
type: 'line',
id: '452',
name: '452',
public: true,
}],
products: {
'bahn-s-bahn': false,
'u-bahn': false,
strassenbahn: false,
fernbus: false,
regionalbus: true,
stadtbus: false,
'seilbahn-zahnradbahn': false,
schiff: false
},
categories: [1],
icon: {type: 'HIM1', title: null},
},
// …
{
id: 'HIM_FREETEXT_106671',
type: 'warning',
summary: 'Neue Haltestellennamen',
text: 'Im Zuge der Neuordnung der Regionalbusverkehre werden mit 6.7.2020 neue Fahrpläne und Liniennummern wirksam und dadurch können sich mitunter die Haltestellennamen verändern.',
priority: 100,
company: 'VOR',
validFrom: '2020-04-21T00:00:00+02:00',
validUntil: '2020-09-30T23:59:00+02:00',
modified: '2020-04-21T13:20:41+02:00',
products: {
'bahn-s-bahn': true,
'u-bahn': true,
strassenbahn: true,
fernbus: true,
regionalbus: true,
stadtbus: true,
'seilbahn-zahnradbahn': false,
schiff: true,
},
categories: [4],
icon: {type: 'HIM4', title: null},
},
// …
]
```

View file

@ -1,38 +0,0 @@
# `serverInfo([opt])`
**Fetches meta information from the HAFAS endpoint.**
With `opt`, you can override the default options, which look like this:
```js
{
versionInfo: true, // query HAFAS versions?
language: 'en', // depends on the profile
}
```
## Example
As an example, we're going to use the [SVV profile](../p/svv):
```js
import {createClient} from 'hafas-client'
import {profile as svvProfile} from 'hafas-client/p/svv/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
const client = createClient(svvProfile, userAgent)
await client.serverInfo()
```
```js
{
// version of the HAFAS Connection Interface (HCI), the API that hafas-client uses
hciVersion: '1.23',
timetableStart: '20200517',
timetableEnd: '20201212',
serverTime: '2020-07-19T21:32:12+02:00',
realtimeDataUpdatedAt: 1595187102,
}
```

View file

@ -1,106 +0,0 @@
# `stop(id, [opt])`
`id` must be in one of these formats:
```js
// a stop/station ID, in a format compatible with the profile you use
'900000123456'
// an FPTF `stop`/`station` object
{
type: 'station',
id: '900000123456',
name: 'foo station',
location: {
type: 'location',
latitude: 1.23,
longitude: 3.21
}
}
```
With `opt`, you can override the default options, which look like this:
```js
{
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
linesOfStops: false, // parse & expose lines at the stop/station?
language: 'en' // language to get results in
}
```
## Response
As an example, we're going to use the [VBB profile](../p/vbb):
```js
import {createClient} from 'hafas-client'
import {profile as vbbProfile} from 'hafas-client/p/vbb/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
const client = createClient(vbbProfile, userAgent)
await client.stop('900000042101') // U Spichernstr.
```
The result may look like this:
```js
{
type: 'stop',
id: '900000042101',
name: 'U Spichernstr.',
location: {
type: 'location',
latitude: 52.496581,
longitude: 13.330616
},
products: {
suburban: false,
subway: true,
tram: false,
bus: true,
ferry: false,
express: false,
regional: false
},
lines: [ {
type: 'line',
id: 'u1',
mode: 'train',
product: 'subway',
public: true,
name: 'U1',
symbol: 'U',
nr: 1,
metro: false,
express: false,
night: false
},
// …
{
type: 'line',
id: 'n9',
mode: 'bus',
product: 'bus',
public: true,
name: 'N9',
symbol: 'N',
nr: 9,
metro: false,
express: false,
night: true
} ]
}
```
If the endpoint returns a list of entrances for a station, the resulting station object will have an `entrances` array looking similar to this:
```js
[
{type: 'location', latitude: 47.411069, longitude: 10.277412},
{type: 'location', latitude: 47.410493, longitude: 10.277223},
{type: 'location', latitude: 47.410754, longitude: 10.278023}
]
```

View file

@ -1,26 +0,0 @@
# automated tests in `hafas-client`
Because transit data is inherently dynamic (e.g. a different set of departures being returned for a stop now than in 10 minutes), and because it is of paramount importance that `hafas-client` actually works with HAFAS endpoints *as they currently work*, its testing setup is a bit unusual.
`hafas-client` has three kinds of automated tests:
- unit tests, which test individual aspects of the case base in isolation (e.g. the parsing of HAFAS-formatted dates & times) run via `npm run test-unit`
- end-to-end (E2E) tests, which run actual HTTP requests against their respective profile's HAFAS endpoint run via `npm run test-e2e`
- integration tests, which are the E2E tests running against pre-recorded (and checked-in) HTTP request fixtures run via `npm run test-integration`
Because the E2E & integration tests are based on the same code, when changing this code, you should also update the integration test fixtures accordingly.
*Note:* In order to be as reproducible as possible, the tests query transit data for a certain *fixed* point in time on the future, hard-coded in each profile's test suite (a.k.a. each file `test/e2e/*.js`). In combination with the recording & mocking of HTTP requests, this effectively makes the integration tests deterministic.
## adding integration test fixtures
As an example, let's assume that we have added an entirely new test to [the *DB* profile's E2E tests](../test/e2e/db.js).
The behaviour of the HTTP request recording (into fixtures) and mocking (using the recorded fixtures) is controlled via an environment variable `$VCR_MODE`:
- By running the test(s) with `VCR_MODE=record`, we can record the HTTP requests being made. The tests will run just like without `$VCR_MODE`, except that they will query data for date+time specified in `T_MOCK` (e.g. [here](https://github.com/public-transport/hafas-client/blob/8ff945c07515155380de0acb33584e474d6d547c/test/e2e/db.js#L33)).
- Then, by running the test(s) with `VCR_MODE=playback`, because their HTTP requests match the pre-recorded fixtures, they work on the corresponding mocked HTTP responses.
Usually, you would not want to update all *already existing* recorded HTTP request fixtures of the test suite you have made changes in, as they are unrelated to the test you have added. To only record your *added* test, temporarily change `tap.test(…)` to read `tap.only(…)`, and run with `TAP_ONLY=1 VCR_MODE=record`; This will skip all unrelated tests entirely.
Then, check the augmented fixtures (in `test/e2e/fixtures`) into Git, and revert the `tap.only(…)` change. To make sure that everything works, run the entire test suite/file (*without `TAP_ONLY=1`*) with `VCR_MODE=playback`.
*Note:* It might be that the test suite/file you want to augment hasn't been updated in a while, so that `T_MOCK` is in the past. In this case, recording additional fixtures from actual HTTP requests of your added test is usually not possible because the HAFAS API is unable to serve old transit data. In this case, you will first have to change `T_MOCK` to a future date+time (a weekday as "normal" as possible) and re-record all tests' HTTP requests; Don't hesitate to get in touch with me if you have trouble with this.

View file

@ -1,205 +0,0 @@
# `trip(id, [opt])`
This method can be used to refetch information about a trip  a vehicle stopping at a set of stops at specific times.
*Note*: This method is not supported by every profile/endpoint.
Let's say you used [`journeys`](journeys.md) and now want to get more up-to-date data about the arrival/departure of a leg. You'd pass in the trip ID from `leg.tripId`, e.g. `'1|24983|22|86|18062017'`, and the name of the line from `leg.line.name` like this:
```js
import {createClient} from 'hafas-client'
import {profile as vbbProfile} from 'hafas-client/p/vbb/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
const client = createClient(vbbProfile, userAgent)
// Hauptbahnhof to Heinrich-Heine-Str.
const {journeys} = client.journeys('900000003201', '900000100008', {results: 1})
const leg = journeys[0].legs[0]
await client.trip(leg.tripId)
```
With `opt`, you can override the default options, which look like this:
```js
{
stopovers: true, // return stations on the way?
polyline: false, // return a shape for the trip?
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
remarks: true, // parse & expose hints & warnings?
language: 'en' // language to get results in
}
```
## Response
*Note:* As stated in the [*Friendly Public Transport Format* v2 draft spec](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md), the returned `departure` and `arrival` times include the current delay. The `departureDelay`/`arrivalDelay` fields express how much they differ from the schedule.
As an example, we're going to use the [VBB profile](../p/vbb):
```js
import {createClient} from 'hafas-client'
import {profile as vbbProfile} from 'hafas-client/p/vbb/index.js'
const client = createClient(vbbProfile)
const {
trip,
realtimeDataUpdatedAt,
} = await client.trip('1|31431|28|86|17122017', 'S9', {
when: 1513534689273,
})
```
`realtimeDataUpdatedAt` is a UNIX timestamp reflecting the latest moment when (at least some of) the response's realtime data have been updated.
When running the code above, `trip` looked like this:
```js
{
id: '1|31431|28|86|17122017',
direction: 'S Spandau',
line: {
type: 'line',
id: '18299',
fahrtNr: '12345',
name: 'S9',
public: true,
mode: 'train',
product: 'suburban',
symbol: 'S',
nr: 9,
metro: false,
express: false,
night: false,
operator: {
type: 'operator',
id: 's-bahn-berlin-gmbh',
name: 'S-Bahn Berlin GmbH'
}
},
currentLocation: {
type: 'location',
latitude: 52.447455,
longitude: 13.522464,
},
origin: {
type: 'station',
id: '900000260005',
name: 'S Flughafen Berlin-Schönefeld',
location: {
type: 'location',
latitude: 52.390796,
longitude: 13.51352
},
products: {
suburban: true,
subway: false,
tram: false,
bus: true,
ferry: false,
express: false,
regional: true
}
},
departure: '2017-12-17T18:37:00+01:00',
plannedDeparture: '2017-12-17T18:37:00+01:00',
departureDelay: null,
departurePlatform: '13',
plannedDeparturePlatform: '13',
destination: {
type: 'station',
id: '900000029101',
name: 'S Spandau',
location: {
type: 'location',
latitude: 52.534794,
longitude: 13.197477
},
products: {
suburban: true,
subway: false,
tram: false,
bus: true,
ferry: false,
express: true,
regional: true
}
},
arrival: '2017-12-17T19:50:30+01:00',
plannedArrival: '2017-12-17T19:49:00+01:00',
arrivalDelay: 90,
arrivalPlatform: '3a',
plannedArrivalPlatform: '2',
stopovers: [ /* … */ ]
}
```
### `polyline` option
If you pass `polyline: true`, the trip will have a `polyline` field, containing a [GeoJSON](http://geojson.org) [`FeatureCollection`](https://tools.ietf.org/html/rfc7946#section-3.3) of [`Point`s](https://tools.ietf.org/html/rfc7946#appendix-A.1). Every `Point` next to a station will have `properties` containing the station's metadata.
We'll look at an example for *U6* from *Alt-Mariendorf* to *Alt-Tegel*, taken from the [VBB profile](../p/vbb):
```js
{
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: {
type: 'station',
id: '900000070301',
name: 'U Alt-Mariendorf',
/* … */
},
geometry: {
type: 'Point',
coordinates: [13.3875, 52.43993] // longitude, latitude
}
},
/* … */
{
type: 'Feature',
properties: {
type: 'station',
id: '900000017101',
name: 'U Mehringdamm',
/* … */
},
geometry: {
type: 'Point',
coordinates: [13.38892, 52.49448] // longitude, latitude
}
},
/* … */
{
// intermediate point, without associated station
type: 'Feature',
properties: {},
geometry: {
type: 'Point',
coordinates: [13.28599, 52.58742] // longitude, latitude
}
},
{
type: 'Feature',
properties: {
type: 'station',
id: '900000089301',
name: 'U Alt-Tegel',
/* … */
},
geometry: {
type: 'Point',
coordinates: [13.28406, 52.58915] // longitude, latitude
}
}
]
}
```

View file

@ -1,114 +0,0 @@
# `tripsByName([lineNameOrFahrtNr], [opt])`
Get all trips matching one or more criteria, e.g. a specific name.
## Response
As an example, we're going to use the [VBB profile](../p/vbb):
```js
import {createClient} from 'hafas-client'
import {profile as vbbProfile} from 'hafas-client/p/vbb/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
const client = createClient(vbbProfile, userAgent)
const {
trips,
realtimeDataUpdatedAt,
} = await client.tripsByName('S1')
```
With `opt`, you can override the default options, which look like this:
```js
{
// use either this
when: null,
// or these
fromWhen: null, untilWhen: null,
onlyCurrentlyRunning: true,
products: {
// these entries may vary from profile to profile
suburban: true,
subway: true,
tram: true,
bus: true,
ferry: true,
express: true,
regional: true,
},
currentlyStoppingAt: null, // only show trips currently stopping at a stop/station, string
lineName: null, // only show trips with this line name, string
operatorNames: null, // only show trips with these operator names, array of strings
}
```
`realtimeDataUpdatedAt` is a UNIX timestamp reflecting the latest moment when (at least some of) the response's realtime data have been updated.
`trips` may look like this:
```js
[
{
id: '1|1214|0|86|16092020'
direction: null,
line: {
type: 'line',
id: 's1',
fahrtNr: '325',
name: 'S1',
mode: 'train',
product: 'suburban',
// …
},
origin: {
type: 'stop',
id: '900000550239',
name: 'Warnemünde, Bhf',
location: { /* … */ },
products: { /* … */ },
},
departure: '2020-09-16T04:03:00+02:00',
plannedDeparture: '2020-09-16T04:03:00+02:00',
departureDelay: null,
departurePlatform: null,
plannedDeparturePlatform: null,
destination: {
type: 'stop',
id: '900000550002',
name: 'Rostock, Hbf',
location: { /* … */ },
products: { /* … */ },
},
arrival: '2020-09-16T04:24:00+02:00',
plannedArrival: '2020-09-16T04:24:00+02:00',
arrivalDelay: null,
arrivalPlatform: null,
plannedArrivalPlatform: null,
},
// …
{
id: '1|62554|0|86|16092020'
direction: null,
line: {
type: 'line',
id: 's1',
fahrtNr: '2001',
name: 'S1',
public: true,
mode: 'train',
product: 'suburban',
// …
},
origin: { /* … */ },
destination: { /* … */ },
// …
}
]
```

View file

@ -1,163 +0,0 @@
# Writing a profile
**Per HAFAS endpoint, `hafas-client` has an endpoint-specific customisation called *profile*.** A profile may, for example, do the following:
- handle the additional requirements of the endpoint (e.g. authentication),
- extract additional information from the data provided by the endpoint,
- guard against triggering bugs of certain endpoints (e.g. time limits).
This guide is about writing such a profile. If you just want to use an already supported endpoint, refer to the [main readme](../readme.md) instead.
*Note*: **If you get stuck, ask for help by [creating an issue](https://github.com/public-transport/hafas-client/issues/new)**; We're happy to help you expand the scope of this library!
## 0. How do the profiles work?
A profile may consist of three things:
- **mandatory details about the HAFAS endpoint**
- `endpoint`: The protocol, host and path of the endpoint.
- `locale`: The [BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag) [locale](https://en.wikipedia.org/wiki/Locale_(computer_software)) of your endpoint (or the area that your endpoint covers).
- `timezone`: An [IANA-time-zone](https://www.iana.org/time-zones)-compatible [timezone](https://en.wikipedia.org/wiki/Time_zone) of your endpoint.
- **flags indicating which features are supported by the endpoint** e.g. `trip`
- **methods overriding the [default profile](../lib/default-profile.js)**
Let's use a fictional endpoint for [Austria](https://en.wikipedia.org/wiki/Austria) as an example:
```js
const myProfile = {
endpoint: 'https://example.org/bin/mgate.exe',
locale: 'de-AT',
timezone: 'Europe/Vienna'
}
```
Assuming their HAFAS endpoint returns all line names prefixed with `foo `, we can adapt our profile to clean them:
```js
// get the default line parser
import {parseLine} from 'hafas-client/parse/line.js'
// wrapper function with additional logic
const parseLineWithoutFoo = (ctx, rawLine) => {
const line = parseLine(ctx, rawLine)
line.name = line.name.replace(/foo /g, '')
return line
}
myProfile.parseLine = parseLineWithoutFoo
```
If you pass this profile into `hafas-client`, the `parseLine` method will override [the default one](../parse/line.js).
You can also use the `parseHook` helper to reduce boilerplate:
```js
import {parseHook} from 'hafas-client/lib/profile-hooks.js'
const removeFoo = (ctx, rawLine) => ({
...ctx.parsed,
name: line.name.replace(/foo /g, '')
})
myProfile.parseLine = parseHook(parseLine, removeFoo)
```
## 1. Setup
*Note*: There are many ways to find the required values. This way is rather easy and works with most endpoints by now.
1. **Find the journey planning webapp** corresponding to the API endpoint; Usually, you can find it on the public transport provider's website.
2. **Open your [browser's devtools](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_are_browser_developer_tools)**, switch to the "Network" tab, and **inspect the requests to the HAFAS API**.
If you can't find the webapp or your public transport provider doesn't have one, you can inspect their mobile app's traffic instead:
1. Get an iOS or Android device and **download the "official" app.**
2. **Configure a [man-in-the-middle HTTP proxy](https://docs.mitmproxy.org/stable/concepts-howmitmproxyworks/)** like [mitmproxy](https://mitmproxy.org).
- Configure your device to trust the self-signed SSL certificate, [as outlined in the mitmproxy docs](https://docs.mitmproxy.org/stable/concepts-certificates/).
- *Note*: This method does not work if the app uses [public key pinning](https://en.wikipedia.org/wiki/HTTP_Public_Key_Pinning). In this case (the app won't be able to query data), please [create an issue](https://github.com/public-transport/hafas-client/issues/new), so we can discuss other techniques.
3. **Record requests of the app.**
- [There's a video showing this step](https://stuff.jannisr.de/how-to-record-hafas-requests.mp4).
- Make sure to cover all relevant sections of the app, e.g. "journeys", "departures", "live map". Better record more than less!
- To help others in the future, post the requests (in their entirety!) on GitHub, e.g. in as format like [this](https://gist.github.com/derhuerst/5fa86ed5aec63645e5ae37e23e555886). This will also let us help you if you have any questions.
## 2. Basic profile
*Note:* You should have read the [general documentation on `mgate.exe` APIs](hafas-mgate-api.md) to make sense of the terminology used below.
You may want to start with the [profile boilerplate](profile-boilerplate.js).
- **Identify the `endpoint`.** The protocol, host and path of the endpoint, *but not* the query string.
- *Note*: **`hafas-client` for now only supports the interface providing JSON** (generated from XML), which is being used by the corresponding iOS/Android apps. It supports neither the JSONP, nor the XML, nor the HTML interface. If the endpoint does not end in `mgate.exe`, it mostly likely won't work.
- **Identify the `locale`.** Basically guess work; Use the date & time formats as an indicator.
- **Identify the `timezone`.** This may be tricky, a for example [Deutsche Bahn](https://en.wikipedia.org/wiki/Deutsche_Bahn) returns departures for Moscow as `+01:00` instead of `+03:00`.
- **Copy the authentication** and other meta fields, namely `ver`, `ext`, `client` and `lang`.
- You can find these fields in the root of each request JSON. Check [a VBB request](https://gist.github.com/derhuerst/ea5d6482b61aeb7384a2c788f43dc11d#file-0-serverinfo-http-L11-L33) and [the corresponding VBB profile](https://github.com/public-transport/hafas-client/blob/2baf2f6f0444ffc67317f8bafe0fe05f687e5fae/p/vbb/base.json#L2-L11) for an example.
- Add a function `transformReqBody(ctx, body)` to your profile, which adds the fields to `body`. todo: adapt this
- Some profiles have a `checksum` parameter (like [here](https://gist.github.com/derhuerst/2a735268bd82a0a6779633f15dceba33#file-journey-details-1-http-L1)) or two `mic` & `mac` parameters (like [here](https://gist.github.com/derhuerst/5fa86ed5aec63645e5ae37e23e555886#file-1-http-L1)). If you see one of them in your requests, jump to the [*Authentication* section of the `mgate.exe` docs](hafas-mgate-api.md#authentication). Unfortunately, this is necessary to get the profile working.
## 3. Products
In `hafas-client`, there's a distinction between the `mode` and the `product` fields:
- The `mode` field describes the mode of transport in general. [Standardised by the *Friendly Public Transport Format*](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md#modes), it is on purpose limited to a very small number of possible values, e.g. `train` or `bus`.
- The value for `product` relates to how a means of transport "works" *in local context*. Example: Even though [*S-Bahn*](https://en.wikipedia.org/wiki/Berlin_S-Bahn) and [*U-Bahn*](https://en.wikipedia.org/wiki/Berlin_U-Bahn) in Berlin are both `train`s, they have different operators, service patterns, stations and look different. Therefore, they are two distinct `product`s `subway` and `suburban`.
**Specify `product`s that appear in the app** you recorded requests of. For a fictional transit network, this may look like this:
```js
const products = [
{
id: 'commuterTrain',
mode: 'train',
bitmasks: [16],
name: 'ACME Commuter Rail',
short: 'CR',
default: true
},
{
id: 'metro',
mode: 'train',
bitmasks: [8],
name: 'Foo Bar Metro',
short: 'M',
default: true
}
]
```
Let's break this down:
- `id`: A sensible, [camelCased](https://en.wikipedia.org/wiki/Camel_case#Variations_and_synonyms), alphanumeric identifier. Use it for the key in the `products` array as well.
- `mode`: A [valid *Friendly Public Transport Format* mode](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md#modes).
- `bitmasks`: HAFAS endpoints work with a [bitmask](https://en.wikipedia.org/wiki/Mask_(computing)#Arguments_to_functions) that toggles the individual products. It should be an array of values that toggle the appropriate bit(s) in the bitmask (see below).
- `name`: A short, but distinct name for the means of transport, *just precise enough in local context*, and in the local language. In Berlin, `S-Bahn-Schnellzug` would be too much, because everyone knows what `S-Bahn` means.
- `short`: The shortest possible symbol that identifies the product.
- `default`: Should the product be used for queries (e.g. journeys) by default?
If you want, you can now **verify that the profile works**; We've prepared [a script](https://runkit.com/derhuerst/hafas-client-profile-example/0.2.1) for that. Alternatively, [submit a Pull Request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/) and we will help you out with testing and improvements.
### Finding the right values for the `bitmasks` field
As shown in [the video](https://stuff.jannisr.de/how-to-record-hafas-requests.mp4), search for a journey and toggle off one product at a time, recording the requests. After extracting the products bitmask ([example](https://gist.github.com/derhuerst/193ef489f8aa50c2343f8bf1f2a22069#file-via-http-L34)) you will end up with values looking like these:
```
toggles value binary subtraction bit(s)
all products 31 11111 31 - 0
all but ACME Commuter Rail 15 01111 31 - 2^4 2^4
all but Foo Bar Metro 23 10111 31 - 2^3 2^3
all but product E 25 11001 31 - 2^2 - 2^1 2^2, 2^1
all but product F 30 11110 31 - 2^0 2^0
```
## 4. Additional info
We consider these improvements to be *optional*:
- **Check if the endpoint supports the `trip()` call.**
- In the app, check if you can re-fetch details for the status of a single journey leg. It should load realtime delays and the current progress.
- If this feature is supported, add `trip: true` to the profile.
- **Check if the endpoint supports the live map call.** Does the app have a "live map" showing all vehicles within an area? If so, add `radar: true` to the profile.
- **Consider transforming station & line names** into the formats that's most suitable for *local users*. This is just an optimal optimisation that makes it easier for users of the profile to use the data. Some examples:
- `M13 (Tram)` -> `M13`. With Berlin context, it is obvious that `M13` is a tram.
- `Berlin Jungfernheide Bhf` -> `Berlin Jungfernheide`. With local context, it's obvious that *Jungfernheide* is a train station.
- **Check if the endpoint has non-obvious limitations** and let use know about these. Examples:
- Some endpoints have a time limit, after which they won't return more departures, but silently discard them.

View file

@ -16,7 +16,7 @@ const formatProductsFilter = (ctx, filter) => {
} }
filter = Object.assign({}, defaultProducts, filter); filter = Object.assign({}, defaultProducts, filter);
let res = 0, products = 0; let products = [];
for (let product in filter) { for (let product in filter) {
if (!hasProp(filter, product) || filter[product] !== true) { if (!hasProp(filter, product) || filter[product] !== true) {
continue; continue;
@ -24,20 +24,13 @@ const formatProductsFilter = (ctx, filter) => {
if (!byProduct[product]) { if (!byProduct[product]) {
throw new TypeError('unknown product ' + product); throw new TypeError('unknown product ' + product);
} }
products++; products.push(byProduct[product].vendo);
for (let bitmask of byProduct[product].bitmasks) {
res = res | bitmask;
}
} }
if (products === 0) { if (products.length === 0) {
throw new Error('no products used'); throw new Error('no products used');
} }
return { return products;
type: 'PROD',
mode: 'INC',
value: String(res),
};
}; };
export { export {

View file

@ -1,29 +0,0 @@
const formatRadarReq = (ctx, north, west, south, east) => {
const {profile, opt} = ctx;
return {
meth: 'JourneyGeoPos',
req: {
maxJny: opt.results,
onlyRT: false, // todo: does this mean "only realtime"?
date: profile.formatDate(profile, opt.when),
time: profile.formatTime(profile, opt.when),
// todo: would a ring work here as well?
rect: profile.formatRectangle(profile, north, west, south, east),
perSize: opt.duration * 1000,
perStep: Math.round(opt.duration / Math.max(opt.frames, 1) * 1000),
ageOfReport: true, // todo: what is this?
jnyFltrL: [
profile.formatProductsFilter(ctx, opt.products || {}),
],
// todo: what is this? what about realtime?
// - CALC
// - CALC_REPORT (as seen in the INSA Young app)
trainPosMode: 'CALC',
},
};
};
export {
formatRadarReq,
};

View file

@ -1,24 +0,0 @@
const formatReachableFromReq = (ctx, address) => {
const {profile, opt} = ctx;
return {
meth: 'LocGeoReach',
req: {
loc: profile.formatLocation(profile, address, 'address'),
maxDur: opt.maxDuration === null
? -1
: opt.maxDuration,
maxChg: opt.maxTransfers,
date: profile.formatDate(profile, opt.when),
time: profile.formatTime(profile, opt.when),
period: 120, // todo: what is this?
jnyFltrL: [
profile.formatProductsFilter(ctx, opt.products || {}),
],
},
};
};
export {
formatReachableFromReq,
};

View file

@ -16,7 +16,7 @@ const formatTime = (profile, when) => {
locale: profile.locale, locale: profile.locale,
zone: timezone, zone: timezone,
}) })
.toFormat('HHmmss'); .toISO({ includeOffset: false, suppressMilliseconds: true })
}; };
export { export {

View file

@ -185,86 +185,52 @@ const createClient = (profile, userAgent, opt = {}) => {
outFrwd = false; outFrwd = false;
} }
const filters = [ const filters = profile.formatProductsFilter({profile}, opt.products || {});
profile.formatProductsFilter({profile}, opt.products || {}), // TODO opt.accessibility
];
if (
opt.accessibility
&& profile.filters
&& profile.filters.accessibility
&& profile.filters.accessibility[opt.accessibility]
) {
filters.push(profile.filters.accessibility[opt.accessibility]);
}
if (!['slow', 'normal', 'fast'].includes(opt.walkingSpeed)) {
throw new Error('opt.walkingSpeed must be one of these values: "slow", "normal", "fast".');
}
const gisFltrL = [];
if (profile.journeysWalkingSpeed) {
gisFltrL.push({
meta: 'foot_speed_' + opt.walkingSpeed,
mode: 'FB',
type: 'M',
});
}
const query = { const query = {
getPasslist: Boolean(opt.stopovers), //maxUmstiege: opt.transfers,
maxChg: opt.transfers, //minUmstiegszeit: opt.transferTime,
minChgTime: opt.transferTime, deutschlandTicketVorhanden: false,
depLocL: [from], nurDeutschlandTicketVerbindungen: false,
viaLocL: opt.via reservierungsKontingenteVorhanden: false,
? [{loc: opt.via}] schnelleVerbindungen: true,
: [], sitzplatzOnly: false,
arrLocL: [to], abfahrtsHalt: from.lid,
jnyFltrL: filters, /*zwischenhalte: opt.via
gisFltrL, ? [{id: opt.via}]
getTariff: Boolean(opt.tickets), : [],*/
ankunftsHalt: to.lid,
produktgattungen: filters,
bikeCarriage: opt.bike,
// TODO
// todo: this is actually "take additional stations nearby the given start and destination station into account" // todo: this is actually "take additional stations nearby the given start and destination station into account"
// see rest.exe docs // see rest.exe docs
ushrp: Boolean(opt.startWithWalking), //ushrp: Boolean(opt.startWithWalking),
getPT: true, // todo: what is this?
getIV: false, // todo: walk & bike as alternatives?
getPolyline: Boolean(opt.polylines),
// todo: `getConGroups: false` what is this?
// todo: what is getEco, fwrd?
}; };
if (journeysRef) { /*if (journeysRef) { TODO
query.ctxScr = journeysRef; query.ctxScr = journeysRef;
} else { } else {*/
query.outDate = profile.formatDate(profile, when); query.anfrageZeitpunkt = profile.formatTime(profile, when);
query.outTime = profile.formatTime(profile, when); //}
} query.ankunftSuche = outFrwd ? 'ABFAHRT' : 'ANKUNFT';
if (opt.results !== null) { if (opt.results !== null) {
query.numF = opt.results; // TODO query.numF = opt.results;
}
if (profile.journeysOutFrwd) {
query.outFrwd = outFrwd;
} }
const {res, common} = await profile.request({profile, opt}, userAgent, { const {res, common} = await profile.request({profile, opt}, userAgent, {
cfg: {polyEnc: 'GPA'}, endpoint: profile.journeysEndpoint,
meth: 'TripSearch',
req: profile.transformJourneysQuery({profile, opt}, query), req: profile.transformJourneysQuery({profile, opt}, query),
}); });
if (!Array.isArray(res.outConL)) {
return [];
}
// todo: outConGrpL
const ctx = {profile, opt, common, res}; const ctx = {profile, opt, common, res};
const journeys = res.outConL const journeys = res.verbindungen
.map(j => profile.parseJourney(ctx, j)); .map(j => profile.parseJourney(ctx, j));
return { return {
earlierRef: res.outCtxScrB || null, earlierRef: res.verbindungReference?.earlier || null,
laterRef: res.outCtxScrF || null, laterRef: res.verbindungReference?.later || null,
journeys, journeys,
realtimeDataUpdatedAt: res.planrtTS && res.planrtTS !== '0' realtimeDataUpdatedAt: null // TODO
? parseInt(res.planrtTS)
: null,
}; };
}; };

View file

@ -3,10 +3,7 @@ import {request} from '../lib/request.js';
import {formatStationBoardReq} from '../format/station-board-req.js'; import {formatStationBoardReq} from '../format/station-board-req.js';
import {formatLocationsReq} from '../format/locations-req.js'; import {formatLocationsReq} from '../format/locations-req.js';
import {formatStopReq} from '../format/stop-req.js'; import {formatStopReq} from '../format/stop-req.js';
import {formatNearbyReq} from '../format/nearby-req.js';
import {formatTripReq} from '../format/trip-req.js'; import {formatTripReq} from '../format/trip-req.js';
import {formatRadarReq} from '../format/radar-req.js';
import {formatReachableFromReq} from '../format/reachable-from-req.js';
import {formatRefreshJourneyReq} from '../format/refresh-journey-req.js'; import {formatRefreshJourneyReq} from '../format/refresh-journey-req.js';
import {formatRemarksReq} from '../format/remarks-req.js'; import {formatRemarksReq} from '../format/remarks-req.js';
import {formatLinesReq} from '../format/lines-req.js'; import {formatLinesReq} from '../format/lines-req.js';
@ -14,10 +11,7 @@ import {formatLinesReq} from '../format/lines-req.js';
import {parseDateTime} from '../parse/date-time.js'; import {parseDateTime} from '../parse/date-time.js';
import {parsePlatform} from '../parse/platform.js'; import {parsePlatform} from '../parse/platform.js';
import {parseBitmask as parseProductsBitmask} from '../parse/products-bitmask.js'; import {parseBitmask as parseProductsBitmask} from '../parse/products-bitmask.js';
import {parseIcon} from '../parse/icon.js';
import {parseWhen} from '../parse/when.js'; import {parseWhen} from '../parse/when.js';
import {parsePrognosisType} from '../parse/prognosis-type.js';
import {parseScheduledDays} from '../parse/scheduled-days.js';
import {parseDeparture} from '../parse/departure.js'; import {parseDeparture} from '../parse/departure.js';
import {parseArrival} from '../parse/arrival.js'; import {parseArrival} from '../parse/arrival.js';
import {parseTrip} from '../parse/trip.js'; import {parseTrip} from '../parse/trip.js';
@ -25,13 +19,9 @@ import {parseJourneyLeg} from '../parse/journey-leg.js';
import {parseJourney} from '../parse/journey.js'; import {parseJourney} from '../parse/journey.js';
import {parseLine} from '../parse/line.js'; import {parseLine} from '../parse/line.js';
import {parseLocation} from '../parse/location.js'; import {parseLocation} from '../parse/location.js';
import {parseCommonData as parseCommon} from '../parse/common.js';
import {parsePolyline} from '../parse/polyline.js'; import {parsePolyline} from '../parse/polyline.js';
import {parseMovement} from '../parse/movement.js';
import {parseNearby} from '../parse/nearby.js';
import {parseOperator} from '../parse/operator.js'; import {parseOperator} from '../parse/operator.js';
import {parseHint} from '../parse/hint.js'; import {parseRemarks} from '../parse/remarks.js';
import {parseWarning} from '../parse/warning.js';
import {parseStopover} from '../parse/stopover.js'; import {parseStopover} from '../parse/stopover.js';
import {formatAddress} from '../format/address.js'; import {formatAddress} from '../format/address.js';
@ -70,10 +60,7 @@ const defaultProfile = {
formatStationBoardReq, formatStationBoardReq,
formatLocationsReq, formatLocationsReq,
formatStopReq, formatStopReq,
formatNearbyReq,
formatTripReq, formatTripReq,
formatRadarReq,
formatReachableFromReq,
formatRefreshJourneyReq, formatRefreshJourneyReq,
formatRemarksReq, formatRemarksReq,
formatLinesReq, formatLinesReq,
@ -82,10 +69,7 @@ const defaultProfile = {
parseDateTime, parseDateTime,
parsePlatform, parsePlatform,
parseProductsBitmask, parseProductsBitmask,
parseIcon,
parseWhen, parseWhen,
parsePrognosisType,
parseScheduledDays,
parseDeparture, parseDeparture,
parseArrival, parseArrival,
parseTrip, parseTrip,
@ -94,13 +78,9 @@ const defaultProfile = {
parseLine, parseLine,
parseStationName: (_, name) => name, parseStationName: (_, name) => name,
parseLocation, parseLocation,
parseCommon,
parsePolyline, parsePolyline,
parseMovement,
parseNearby,
parseOperator, parseOperator,
parseHint, parseRemarks,
parseWarning,
parseStopover, parseStopover,
formatAddress, formatAddress,
@ -128,11 +108,11 @@ const defaultProfile = {
refreshJourney: true, refreshJourney: true,
// refreshJourney(): use `outReconL[]` instead of `ctxRecon`? // refreshJourney(): use `outReconL[]` instead of `ctxRecon`?
refreshJourneyUseOutReconL: false, refreshJourneyUseOutReconL: false,
tripsByName: true, tripsByName: false,
remarks: true, remarks: false,
// `remarks()` method: support for `getPolyline` field? // `remarks()` method: support for `getPolyline` field?
remarksGetPolyline: true, // `remarks()` method: support for `getPolyline` field? remarksGetPolyline: false, // `remarks()` method: support for `getPolyline` field?
lines: true, lines: false,
}; };
export { export {

View file

@ -60,10 +60,6 @@ const randomizeUserAgent = (userAgent) => {
return ua; return ua;
}; };
const md5 = input => createHash('md5')
.update(input)
.digest();
const checkIfResponseIsOk = (_) => { const checkIfResponseIsOk = (_) => {
const { const {
body, body,
@ -81,56 +77,37 @@ const checkIfResponseIsOk = (_) => {
// but only return the constructor & error message. // but only return the constructor & error message.
const getError = (_) => { const getError = (_) => {
// mutating here is ugly but pragmatic // mutating here is ugly but pragmatic
if (_.errTxt) { if (_.fehlerNachricht.ueberschrift) {
errProps.hafasMessage = _.errTxt; errProps.hafasMessage = _.fehlerNachricht.ueberschrift;
} }
if (_.errTxtOut) { if (_.fehlerNachricht.text) {
errProps.hafasDescription = _.errTxtOut; errProps.hafasDescription = _.fehlerNachricht.text;
}
if (_.err in byErrorCode) {
return byErrorCode[_.err];
} }
return { return {
Error: HafasError, Error: HafasError,
message: body.errTxt || 'unknown error', message: errProps.hafasMessage || 'unknown error',
props: {}, props: {code: _.fehlerNachricht.code},
}; };
}; };
if (body.err && body.err !== 'OK') { if (body.fehlerNachricht) { // TODO better handling
const {Error: HafasError, message, props} = getError(body); const {Error: HafasError, message, props} = getError(body);
throw new HafasError(message, body.err, {...errProps, ...props}); throw new HafasError(message, body.err, {...errProps, ...props});
} }
if (!body.svcResL || !body.svcResL[0]) {
throw new HafasError('invalid/unsupported response structure', null, errProps);
}
if (body.svcResL[0].err !== 'OK') {
const {Error: HafasError, message, props} = getError(body.svcResL[0]);
throw new HafasError(message, body.svcResL[0].err, {...errProps, ...props});
}
}; };
const request = async (ctx, userAgent, reqData) => { const request = async (ctx, userAgent, reqData) => {
const {profile, opt} = ctx; const {profile, opt} = ctx;
const rawReqBody = profile.transformReqBody(ctx, { const endpoint = reqData.endpoint;
// todo: is it `eng` actually? delete reqData.endpoint;
// RSAG has `deu` instead of `de` const rawReqBody = profile.transformReqBody(ctx, reqData);
lang: opt.language || profile.defaultLanguage || 'en', //console.log(rawReqBody, JSON.stringify(rawReqBody.req.reisende));
svcReqL: [reqData],
client: profile.client, // client identification
ext: profile.ext, // ?
ver: profile.ver, // HAFAS protocol version
auth: profile.auth, // static authentication
});
const req = profile.transformReq(ctx, { const req = profile.transformReq(ctx, {
agent: getAgent(), agent: getAgent(),
method: 'post', method: 'post',
// todo: CORS? referrer policy? // todo: CORS? referrer policy?
body: JSON.stringify(rawReqBody), body: JSON.stringify(rawReqBody.req),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept-Encoding': 'gzip, br, deflate', 'Accept-Encoding': 'gzip, br, deflate',
@ -144,33 +121,10 @@ const request = async (ctx, userAgent, reqData) => {
query: {}, query: {},
}); });
if (profile.addChecksum || profile.addMicMac) { const url = endpoint + '?' + stringify(req.query);
if (!Buffer.isBuffer(profile.salt) && 'string' !== typeof profile.salt) {
throw new TypeError('profile.salt must be a Buffer or a string.');
}
// Buffer.from(buf, 'hex') just returns buf
const salt = Buffer.from(profile.salt, 'hex');
if (profile.addChecksum) {
const checksum = md5(Buffer.concat([
Buffer.from(req.body, 'utf8'),
salt,
]));
req.query.checksum = checksum.toString('hex');
}
if (profile.addMicMac) {
const mic = md5(Buffer.from(req.body, 'utf8'));
req.query.mic = mic.toString('hex');
const micAsHex = Buffer.from(mic.toString('hex'), 'utf8');
const mac = md5(Buffer.concat([micAsHex, salt]));
req.query.mac = mac.toString('hex');
}
}
const reqId = randomBytes(3) const reqId = randomBytes(3)
.toString('hex'); .toString('hex');
const url = profile.endpoint + '?' + stringify(req.query);
const fetchReq = new Request(url, req); const fetchReq = new Request(url, req);
profile.logRequest(ctx, fetchReq, reqId); profile.logRequest(ctx, fetchReq, reqId);
@ -200,6 +154,7 @@ const request = async (ctx, userAgent, reqData) => {
} }
const body = await res.text(); const body = await res.text();
//console.log(body);
profile.logResponse(ctx, res, body, reqId); profile.logResponse(ctx, res, body, reqId);
const b = JSON.parse(body); const b = JSON.parse(body);
@ -207,11 +162,9 @@ const request = async (ctx, userAgent, reqData) => {
body: b, body: b,
errProps, errProps,
}); });
const svcRes = b.svcResL[0].res;
return { return {
res: svcRes, res: b,
common: profile.parseCommon({...ctx, res: svcRes}), common: {},
}; };
}; };

View file

@ -9,10 +9,7 @@ const types = {
formatStationBoardReq: 'function', formatStationBoardReq: 'function',
formatLocationsReq: 'function', formatLocationsReq: 'function',
formatStopReq: 'function', formatStopReq: 'function',
formatNearbyReq: 'function',
formatTripReq: 'function', formatTripReq: 'function',
formatRadarReq: 'function',
formatReachableFromReq: 'function',
formatRefreshJourneyReq: 'function', formatRefreshJourneyReq: 'function',
transformJourneysQuery: 'function', transformJourneysQuery: 'function',
@ -27,11 +24,8 @@ const types = {
parseStationName: 'function', parseStationName: 'function',
parseLocation: 'function', parseLocation: 'function',
parsePolyline: 'function', parsePolyline: 'function',
parseMovement: 'function',
parseNearby: 'function',
parseOperator: 'function', parseOperator: 'function',
parseHint: 'function', parseRemarks: 'function',
parseWarning: 'function',
parseStopover: 'function', parseStopover: 'function',
formatAddress: 'function', formatAddress: 'function',

View file

@ -1,13 +0,0 @@
{
"auth": {
"type": "AID",
"aid": "4vV1AcH3N511icH"
},
"client": {
"type": "WEB",
"id": "AVV_AACHEN",
"name": "webapp"
},
"endpoint": "https://auskunft.avv.de/bin/mgate.exe",
"defaultLanguage": "de"
}

View file

@ -1,53 +0,0 @@
import {inspect} from 'util'
import {createClient} from '../../index.js'
import {profile as avvProfile} from './index.js'
// Pick a descriptive user agent! hafas-client won't work with this string.
const client = createClient(avvProfile, 'hafas-client-example')
const rwth = '1057'
const kronenberg = '1397'
let data = await client.locations('kronenberg', {results: 3})
// let data = await client.nearby({
// type: 'location',
// latitude: 50.770607,
// longitude: 6.104637,
// }, {distance: 500})
// let data = await client.reachableFrom({
// type: 'location',
// id: '990000745',
// address: 'Aachen, Charlottenstraße 11',
// latitude: 50.770607,
// longitude: 6.104637,
// }, {
// maxDuration: 8,
// })
// let data = await client.stop(rwth, {linesOfStops: true})
// let data = await client.departures(rwth, {duration: 1})
// let data = await client.arrivals(rwth, {duration: 10, linesOfStops: true})
// let data = await client.journeys(rwth, kronenberg, {results: 1, stopovers: true})
// {
// const [journey] = data.journeys
// data = await client.refreshJourney(journey.refreshToken, {
// stopovers: true,
// remarks: true,
// })
// }
// {
// const [journey] = data.journeys
// const leg = journey.legs[0]
// data = await client.trip(leg.tripId, {polyline: true})
// }
// let data = await client.radar({
// north: 50.78141,
// west: 6.06031,
// south: 50.75022,
// east: 6.10316,
// }, {results: 10})
console.log(inspect(data, {depth: null, colors: true}))

View file

@ -1,105 +0,0 @@
// todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
const baseProfile = require('./base.json');
const products = [{
id: 'regional-train',
mode: 'train',
bitmasks: [1],
name: 'Regionalzug',
short: 'Regionalzug',
default: true,
}, {
id: 'long-distance-train',
mode: 'train',
bitmasks: [2],
name: 'Fernzug',
short: 'Fernzug',
default: true,
}, {
id: 'express-train',
mode: 'train',
bitmasks: [4],
name: 'ICE/Thalys',
short: 'ICE/Thalys',
default: true,
}, {
id: 'fernbus',
mode: 'bus',
bitmasks: [8],
name: 'Fernbus',
short: 'Fernbus',
default: true,
}, {
id: 'suburban-train',
mode: 'train',
bitmasks: [16],
name: 'S-Bahn',
short: 'S',
default: true,
}, {
id: 'subway',
mode: 'train',
bitmasks: [32],
name: 'U-Bahn',
short: 'U',
default: true,
}, {
id: 'tram',
mode: 'train',
bitmasks: [64],
name: 'Straßenbahn',
short: 'Straßenbahn',
default: true,
}, {
id: 'bus',
mode: 'bus',
bitmasks: [128],
name: 'Bus',
short: 'Bus',
default: true,
}, {
id: 'added-bus',
mode: 'bus',
bitmasks: [256],
name: 'Bus, Verstärkerfahrt',
short: 'Bus V',
default: true,
}, {
id: 'on-call',
mode: 'taxi',
bitmasks: [512],
name: 'Bedarfsverkehr',
short: 'Bedarfsverkehr',
default: true,
}, {
id: 'ferry',
mode: 'watercraft',
bitmasks: [1024],
name: 'Fähre',
short: 'Fähre',
default: true,
}];
const profile = {
...baseProfile,
locale: 'de-DE',
timezone: 'Europe/Berlin',
ver: '1.26',
products,
refreshJourneyUseOutReconL: true,
trip: true,
radar: true,
reachableFrom: true,
remarks: true,
remarksGetPolyline: false,
};
export {
profile,
};

View file

@ -1,15 +0,0 @@
# AVV profile for `hafas-client`
[*Aachener Verkehrsverbund (AVV)*](https://de.wikipedia.org/wiki/Aachener_Verkehrsverbund) is the local transport provider of [Aachen](https://en.wikipedia.org/wiki/Aachen). This profile adds *AVV* support to `hafas-client`.
## Usage
```js
import {createClient} from 'hafas-client'
import {profile as avvProfile} from 'hafas-client/p/avv/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
// create a client with AVV profile
const client = createClient(avvProfile, userAgent)
```

View file

@ -1,13 +0,0 @@
{
"auth": {
"type": "AID",
"aid": "kEwHkFUCIL500dym"
},
"client": {
"type": "WEB",
"id": "BART",
"name": "webapp"
},
"endpoint": "https://planner.bart.gov/bin/mgate.exe",
"defaultLanguage": "en"
}

View file

@ -1,53 +0,0 @@
import {inspect} from 'util'
import {createClient} from '../../index.js'
import {profile as bartProfile} from './index.js'
// Pick a descriptive user agent! hafas-client won't work with this string.
const client = createClient(bartProfile, 'hafas-client-example')
const fremont = '100013296'
const embarcadero = '100013295'
let data = await client.locations('embarcadero', {results: 3})
// let data = await client.nearby({
// type: 'location',
// latitude: 38.554779,
// longitude: -121.738798,
// }, {distance: 500})
// let data = await client.reachableFrom({
// type: 'location',
// id: '980557173',
// address: '1000 Alice St, Davis, 95616',
// latitude: 38.554779,
// longitude: -121.738798,
// }, {
// maxDuration: 8,
// })
// let data = await client.stop(fremont, {linesOfStops: true})
// let data = await client.departures(fremont, {duration: 1})
// let data = await client.arrivals(fremont, {duration: 10, linesOfStops: true})
// let data = await client.journeys(fremont, embarcadero, {results: 1, stopovers: true})
// {
// const [journey] = data.journeys
// data = await client.refreshJourney(journey.refreshToken, {
// stopovers: true,
// remarks: true,
// })
// }
// {
// const [journey] = data.journeys
// const leg = journey.legs[0]
// data = await client.trip(leg.tripId, {polyline: true})
// }
// let data = await client.radar({
// north: 37.8735,
// west: -122.5250,
// south: 37.6884,
// east: -122.2105,
// }, {results: 10})
console.log(inspect(data, {depth: null, colors: true}))

View file

@ -1,69 +0,0 @@
// todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
const baseProfile = require('./base.json');
const products = [{
id: 'bart',
mode: 'train',
bitmasks: [128],
name: 'BART',
short: 'BART',
default: true,
}, {
id: 'regional-train',
mode: 'train',
bitmasks: [8],
name: 'regional trains (Caltrain, Capitol Corridor, ACE)',
short: 'regional trains',
default: true,
}, {
id: 'bus',
mode: 'bus',
bitmasks: [32],
name: 'Bus',
short: 'Bus',
default: true,
}, {
id: 'ferry',
mode: 'watercraft',
bitmasks: [64],
name: 'Ferry',
short: 'Ferry',
default: true,
}, {
id: 'tram',
mode: 'train',
bitmasks: [256],
name: 'Tram',
short: 'Tram',
default: true,
}, {
id: 'cable-car',
mode: 'train',
bitmasks: [4],
name: 'cable car',
short: 'cable car',
default: true,
}];
const profile = {
...baseProfile,
locale: 'en-US',
timezone: 'America/Los_Angeles',
ver: '1.40',
products,
trip: true,
radar: true,
reachableFrom: true,
refreshJourneyUseOutReconL: true,
};
export {
profile,
};

View file

@ -1,15 +0,0 @@
# BART profile for `hafas-client`
[*Bay Area Rapid Transit (BART)*](https://en.wikipedia.org/wiki/Bay_Area_Rapid_Transit) is the rapid transit public transportation system serving the [San Francisco Bay Area](https://en.wikipedia.org/wiki/San_Francisco_Bay_Area). This profile adds *BART* support to `hafas-client`.
## Usage
```js
import {createClient} from 'hafas-client'
import {profile as bartProfile} from 'hafas-client/p/bart/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
// create a client with BART profile
const client = createClient(bartProfile, userAgent)
```

View file

@ -1,13 +0,0 @@
{
"auth": {
"type": "AID",
"aid": "3jkAncud78HSoqclmN54812A"
},
"client": {
"type": "WEB",
"id": "HAFAS",
"name": "webapp"
},
"endpoint": "https://bls.hafas.de/bin/mgate.exe",
"defaultLanguage": "de"
}

View file

@ -1,56 +0,0 @@
import {inspect} from 'util'
import {createClient} from '../../index.js'
import {profile as blsProfile} from './index.js'
// Pick a descriptive user agent! hafas-client won't work with this string.
const client = createClient(blsProfile, 'hafas-client-example')
const bernDennigkofengässli = '8590093'
const münsingenSpital = '8578932'
let data = await client.locations('münsingen spital', {results: 3})
// let data = await client.nearby({
// type: 'location',
// latitude: 53.554422,
// longitude: 9.977934
// }, {distance: 500})
// let data = await client.reachableFrom({
// type: 'location',
// id: '990017698',
// address: 'Bern, Schänzlihalde 17',
// latitude: 46.952835,
// longitude: 7.447527,
// }, {
// maxDuration: 10,
// })
// let data = await client.stop(bernDennigkofengässli, {linesOfStops: true})
// let data = await client.departures(bernDennigkofengässli, {duration: 1})
// let data = await client.arrivals(bernDennigkofengässli, {duration: 10, linesOfStops: true})
// let data = await client.journeys(bernDennigkofengässli, münsingenSpital, {
// results: 1,
// stopovers: true,
// })
// {
// const [journey] = data.journeys
// data = await client.refreshJourney(journey.refreshToken, {
// stopovers: true,
// remarks: true,
// })
// }
// {
// const [journey] = data.journeys
// const leg = journey.legs[0]
// data = await client.trip(leg.tripId, {polyline: true})
// }
// let data = await client.radar({
// north: 46.969,
// west: 7.3941,
// south: 46.921,
// east: 7.5141,
// }, {results: 10})
console.log(inspect(data, {depth: null, colors: true}))

View file

@ -1,96 +0,0 @@
// todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
const baseProfile = require('./base.json');
const products = [{
id: 'ice',
mode: 'train',
bitmasks: [1],
name: 'ICE',
short: 'ICE',
default: true,
}, {
id: 'ic-ec',
mode: 'train',
bitmasks: [2],
name: 'IC/EC',
short: 'IC/EC',
default: true,
}, {
id: 'ir',
mode: 'train',
bitmasks: [4],
name: 'IR',
short: 'IR',
default: true,
}, {
id: 'local-train',
mode: 'train',
bitmasks: [8],
name: 'Nahverkehr',
short: 'Nahverkehr',
default: true,
}, {
id: 'watercraft',
mode: 'watercraft',
bitmasks: [16],
name: 'Schiff',
short: 'Schiff',
default: true,
}, {
id: 's-bahn',
mode: 'train',
bitmasks: [32],
name: 'S-Bahn',
short: 'S',
default: true,
}, {
id: 'bus',
mode: 'bus',
bitmasks: [64],
name: 'Bus',
short: 'Bus',
default: true,
}, {
id: 'funicular',
mode: 'gondola',
bitmasks: [128],
name: 'Seilbahn',
short: 'Seilbahn',
default: true,
}, {
id: 'tram',
mode: 'train',
bitmasks: [512],
name: 'Tram',
short: 'Tram',
default: true,
}, {
id: 'car-shuttle-train',
mode: 'train',
bitmasks: [4096],
name: 'Autoverlad',
short: 'Autoverlad',
default: true,
}];
const profile = {
...baseProfile,
locale: 'de-DE',
timezone: 'Europe/Berlin',
ver: '1.46',
products,
trip: true,
radar: true,
refreshJourneyUseOutReconL: true,
reachableFrom: true,
};
export {
profile,
};

View file

@ -1,17 +0,0 @@
# BLS profile for `hafas-client`
[*BLS AG*](https://en.wikipedia.org/wiki/BLS_AG) is the local transport provider of the [Canton of Bern](https://en.wikipedia.org/wiki/Canton_of_Bern). This profile adds *BLS* support to `hafas-client`.
## Usage
```js
import {createClient} from 'hafas-client'
import {profile as blsProfile} from 'hafas-client/p/bls/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
// create a client with BLS profile
const client = createClient(blsProfile, userAgent)
```
Check out the [code examples](example.js).

View file

@ -1,16 +0,0 @@
{
"auth": {
"type": "AID",
"aid": "dVg4TZbW8anjx9ztPwe2uk4LVRi9wO"
},
"client": {
"type": "WEB",
"id": "VBB",
"v": 10002,
"name": "webapp"
},
"endpoint": "https://bvg-apps-ext.hafas.de/bin/mgate.exe",
"ext": "BVG.1",
"ver": "1.72",
"defaultLanguage": "de"
}

View file

@ -1,55 +0,0 @@
import {inspect} from 'util'
import {createClient} from '../../index.js'
import {profile as bvgProfile} from './index.js'
// Pick a descriptive user agent! hafas-client won't work with this string.
const client = createClient(bvgProfile, 'hafas-client-example')
const berlinHbf = '900003201'
const charlottenburg = '900024101'
const kottbusserTor = '900013102'
const spichernstr = '900042101'
let data = await client.locations('Alexanderplatz', {results: 2})
// let data = await client.nearby({
// type: 'location',
// latitude: 52.5137344,
// longitude: 13.4744798
// }, {distance: 60})
// let data = await client.reachableFrom({
// type: 'location',
// address: '13353 Berlin-Wedding, Torfstr. 17',
// latitude: 52.541797,
// longitude: 13.350042
// }, {
// when: new Date('2018-08-27T10:00:00+0200'),
// maxDuration: 10
// })
// let data = await client.stop(spichernstr, {linesOfStops: true}) // Spichernstr
// let data = await client.departures(kottbusserTor, {duration: 1})
// let data = await client.arrivals(kottbusserTor, {duration: 10, linesOfStops: true})
// let data = await client.journeys(berlinHbf, charlottenburg, {
// results: 1,
// polylines: true,
// })
// {
// const [journey] = data.journeys
// const leg = journey.legs[0]
// data = await client.trip(leg.tripId, {polyline: true})
// }
// {
// const [journey] = data.journeys
// data = await client.refreshJourney(journey.refreshToken, {stopovers: true, remarks: true})
// }
// let data = await client.radar({
// north: 52.52411,
// west: 13.41002,
// south: 52.51942,
// east: 13.41709
// }, {results: 10})
console.log(inspect(data, {depth: null, colors: true}))

View file

@ -1,172 +0,0 @@
// todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
import {parseHook} from '../../lib/profile-hooks.js';
import {parseAndAddLocationDHID} from '../vbb/parse-loc-dhid.js';
import {parseLocation as _parseLocation} from '../../parse/location.js';
import {parseArrival as _parseArrival} from '../../parse/arrival.js';
import {parseDeparture as _parseDeparture} from '../../parse/departure.js';
import {parseStopover as _parseStopover} from '../../parse/stopover.js';
import {parseJourneyLeg as _parseJourneyLeg} from '../../parse/journey-leg.js';
const baseProfile = require('./base.json');
import {products} from './products.js';
// todo: there's also a referenced icon `{"res":"occup_fig_{low,mid}"}`
const addOccupancy = (item, occupancyCodes) => {
const remIdx = (item.remarks || [])
.findIndex(r => r.code && occupancyCodes.has(r.code));
if (remIdx < 0) {
return;
}
const rem = item.remarks[remIdx];
item.occupancy = occupancyCodes.get(rem.code);
item.remarks = [
...item.remarks.slice(0, remIdx),
...item.remarks.slice(remIdx + 1),
];
};
const stopoverOccupancyCodes = new Map([
['text.occup.loc.max.11', 'low'],
['text.occup.loc.max.12', 'medium'],
['text.occup.loc.max.13', 'high'],
]);
const journeyLegOccupancyCodes = new Map([
['text.occup.jny.max.11', 'low'],
['text.occup.jny.max.12', 'medium'],
['text.occup.jny.max.13', 'high'],
]);
const parseLocation = ({parsed}, l) => {
parseAndAddLocationDHID(parsed, l);
return parsed;
};
// todo: S45, S46?
const ringbahnClockwise = /^ringbahn s\s?41$/i;
const ringbahnAnticlockwise = /^ringbahn s\s?42$/i;
const parseDepartureRenameRingbahn = ({parsed}, dep) => {
if (parsed.line && parsed.line.product === 'suburban') {
const d = parsed.direction && parsed.direction.trim();
if (ringbahnClockwise.test(d)) {
parsed.direction = 'Ringbahn S41 ⟳';
} else if (ringbahnAnticlockwise.test(d)) {
parsed.direction = 'Ringbahn S42 ⟲';
}
}
return parsed;
};
const parseArrivalRenameRingbahn = ({parsed}, arr) => {
if (parsed.line && parsed.line.product === 'suburban') {
const p = parsed.provenance && parsed.provenance.trim();
if (ringbahnClockwise.test(p)) {
parsed.provenance = 'Ringbahn S41 ⟳';
} else if (ringbahnAnticlockwise.test(p)) {
parsed.provenance = 'Ringbahn S42 ⟲';
}
}
return parsed;
};
const parseArrDepWithOccupancy = ({parsed}, d) => {
addOccupancy(parsed, stopoverOccupancyCodes);
return parsed;
};
const parseStopoverWithOccupancy = ({parsed}, st, date) => {
addOccupancy(parsed, stopoverOccupancyCodes);
return parsed;
};
const parseJourneyLegWithBerlkönig = (ctx, leg, date) => {
if (leg.type === 'KISS') {
const icon = ctx.common.icons[leg.icoX];
if (icon && icon.type === 'prod_berl') {
const res = _parseJourneyLeg(ctx, {
...leg, type: 'WALK',
}, date);
delete res.walking;
const mcp = leg.dep.mcp || {};
const mcpData = mcp.mcpData || {};
// todo: mcp.lid
// todo: mcpData.occupancy, mcpData.type
// todo: journey.trfRes.bkgData
res.line = {
type: 'line',
id: null, // todo
// todo: fahrtNr?
name: mcpData.providerName,
public: true,
mode: 'taxi',
product: 'berlkoenig',
// todo: operator
};
return res;
}
}
return _parseJourneyLeg(ctx, leg, date);
};
const parseJourneyLegWithOccupancy = ({parsed}, leg, date) => {
if (leg.type === 'JNY') {
addOccupancy(parsed, journeyLegOccupancyCodes);
}
return parsed;
};
// use the Berlkönig ride sharing service?
// todo: https://github.com/alexander-albers/tripkit/issues/26#issuecomment-825437320
const requestJourneysWithBerlkoenig = ({opt}, query) => {
if ('numF' in query && opt.berlkoenig) {
// todo: check if this is still true
throw new Error('The `berlkoenig` and `results` options are mutually exclusive.');
}
query.jnyFltrL.push({type: 'GROUP', mode: 'INC', value: 'OEV'});
if (opt.berlkoenig) {
query.jnyFltrL.push({type: 'GROUP', mode: 'INC', value: 'BERLKOENIG'});
}
query.gisFltrL = [{meta: 'foot_speed_normal', type: 'M', mode: 'FB'}];
return query;
};
// todo: adapt/extend `vbb-parse-ticket` to support the BVG markup
const profile = {
...baseProfile,
locale: 'de-DE',
timezone: 'Europe/Berlin',
transformJourneysQuery: requestJourneysWithBerlkoenig,
products,
parseLocation: parseHook(_parseLocation, parseLocation),
parseArrival: parseHook(
parseHook(_parseArrival, parseArrivalRenameRingbahn),
parseArrDepWithOccupancy,
),
parseDeparture: parseHook(
parseHook(_parseDeparture, parseDepartureRenameRingbahn),
parseArrDepWithOccupancy,
),
parseStopover: parseHook(_parseStopover, parseStopoverWithOccupancy),
parseJourneyLeg: parseHook(
parseJourneyLegWithBerlkönig,
parseJourneyLegWithOccupancy,
),
refreshJourneyUseOutReconL: true,
trip: true,
radar: true,
refreshJourney: true,
reachableFrom: true,
};
export {
profile,
};

View file

@ -1,62 +0,0 @@
const products = [
{
id: 'suburban',
mode: 'train',
bitmasks: [1],
name: 'S-Bahn',
short: 'S',
default: true,
},
{
id: 'subway',
mode: 'train',
bitmasks: [2],
name: 'U-Bahn',
short: 'U',
default: true,
},
{
id: 'tram',
mode: 'train',
bitmasks: [4],
name: 'Tram',
short: 'T',
default: true,
},
{
id: 'bus',
mode: 'bus',
bitmasks: [8],
name: 'Bus',
short: 'B',
default: true,
},
{
id: 'ferry',
mode: 'watercraft',
bitmasks: [16],
name: 'Fähre',
short: 'F',
default: true,
},
{
id: 'express',
mode: 'train',
bitmasks: [32],
name: 'IC/ICE',
short: 'E',
default: true,
},
{
id: 'regional',
mode: 'train',
bitmasks: [64],
name: 'RB/RE',
short: 'R',
default: true,
},
];
export {
products,
};

View file

@ -1,45 +0,0 @@
# BVG profile for `hafas-client`
[*Verkehrsverbund Berlin-Brandenburg (BVG)*](https://en.wikipedia.org/wiki/Verkehrsverbund_Berlin-Brandenburg) is the major local transport provider in [Berlin](https://en.wikipedia.org/wiki/Berlin). This profile adds *BVG*-specific customizations to `hafas-client`.
## Usage
```js
import {createClient} from 'hafas-client'
import {profile as bvgProfile} from 'hafas-client/p/bvg/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
// create a client with BVG profile
const client = createClient(bvgProfile, userAgent)
```
## Customisations
- parses *BVG*-specific products (such as *X-Bus*)
- supports [BerlKönig `journey` legs](#berlkoenig)
- strips parts from station names that are unnecessary in the Berlin context
- parses line names to give more information (e.g. "Is it an express bus?")
- renames *Ringbahn* line names to contain `⟳` and `⟲`
### BerlKönig
BVG has recently announced [a ride-sharing service called *BerlKönig*](https://www.berlkoenig.de). Pass `berlkoenig: true` into `journeys()` to get special legs:
```js
{
mode: 'walking',
departure: // …
arrival: // …
origin: // …
destination: // …
line: {
type: 'line',
name: 'BerlKönig',
public: true,
mode: 'taxi',
product: 'berlkoenig'
}
}
```

View file

@ -1,15 +0,0 @@
{
"auth": {
"type": "AID",
"aid": "ALT2vl7LAFDFu2dz"
},
"client": {
"type": "IPH",
"id": "HAFAS",
"v": "4000000",
"name": "cflPROD-STORE"
},
"endpoint": "https://horaires.cfl.lu/bin/mgate.exe",
"ver": "1.43",
"defaultLanguage": "fr"
}

View file

@ -1,53 +0,0 @@
import {inspect} from 'util'
import {createClient} from '../../index.js'
import {profile as cflProfile} from './index.js'
// Pick a descriptive user agent! hafas-client won't work with this string.
const client = createClient(cflProfile, 'hafas-client-example')
const mersch = '9864348'
const bruxellesCentral = '8800003'
let data = await client.locations('mersch', {results: 3})
// let data = await client.nearby({
// type: 'location',
// latitude: 49.7523,
// longitude: 6.1103
// }, {distance: 500})
// let data = await client.reachableFrom({
// type: 'location',
// id: '980005067',
// address: '7557 Mersch, Rue Mies 1',
// latitude: 49.746044,
// longitude: 6.102228,
// }, {
// maxDuration: 30,
// })
// let data = await client.stop(mersch)
// let data = await client.departures(mersch, {duration: 5})
// let data = await client.arrivals(mersch, {duration: 10, linesOfStops: true})
// let data = await client.journeys(mersch, bruxellesCentral, {results: 1})
// {
// const [journey] = data.journeys
// const [leg] = journey.legs
// data = await client.trip(leg.tripId, {polyline: true})
// }
// {
// const [journey] = journeys
// data = await client.refreshJourney(journey.refreshToken, {
// stopovers: true,
// remarks: true,
// })
// }
// let data = await client.radar({
// north: 49.9,
// west: 6.11,
// south: 49.7,
// east: 6.13
// }, {results: 10})
console.log(inspect(data, {depth: null, colors: true}))

View file

@ -1,26 +0,0 @@
// todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
const baseProfile = require('./base.json');
import {products} from './products.js';
const profile = {
...baseProfile,
locale: 'de-LU',
timezone: 'Europe/Luxembourg',
defaultLanguage: 'de',
products: products,
refreshJourneyUseOutReconL: true,
trip: true,
radar: true,
reachableFrom: true,
remarksGetPolyline: false,
};
export {
profile,
};

View file

@ -1,47 +0,0 @@
const products = [
// todo: other bits
{
id: 'express-train',
mode: 'train',
bitmasks: [1, 2],
name: 'TGV, ICE, EuroCity',
short: 'TGV/ICE/EC',
default: true,
},
{
id: 'local-train',
mode: 'train',
bitmasks: [8, 16],
name: 'local trains',
short: 'local',
default: true,
},
{
id: 'tram',
mode: 'train',
bitmasks: [256],
name: 'tram',
short: 'tram',
default: true,
},
{
id: 'bus',
mode: 'bus',
bitmasks: [32],
name: 'bus',
short: 'bus',
default: true,
},
{
id: 'gondola',
mode: 'gondola',
bitmasks: [512],
name: 'Fun', // taken from the horaires.cfl.lu website
short: 'Fun', // abbreviation for funicular?
default: true,
},
];
export {
products,
};

View file

@ -1,20 +0,0 @@
# CFL profile for `hafas-client`
The [*Société Nationale des Chemins de Fer Luxembourgeois (CFL)*](https://en.wikipedia.org/wiki/Société_Nationale_des_Chemins_de_Fer_Luxembourgeois) is the national railway company of [Luxembourg](https://en.wikipedia.org/wiki/Luxembourg). This profile adds *CFL*-specific customisations to `hafas-client`.
## Usage
```js
import {createClient} from 'hafas-client'
import {profile as cflProfile} from 'hafas-client/p/cfl/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
// create a client with CFL profile
const client = createClient(cflProfile, userAgent)
```
## Customisations
- *CFL*-specific products (such as [*Standseilbahn_Pfaffenthal-Kirchberg*](https://de.wikipedia.org/wiki/Standseilbahn_Pfaffenthal-Kirchberg))

View file

@ -1,15 +0,0 @@
{
"auth": {
"type": "AID",
"aid": "ioslaskdcndrjcmlsd"
},
"client": {
"type": "IPH",
"id": "CMTA",
"v": "2",
"name": "CapMetro"
},
"endpoint": "https://capmetro.hafas.cloud/bin/mgate.exe",
"ver": "1.40",
"defaultLanguage": "en"
}

View file

@ -1,53 +0,0 @@
import {inspect} from 'util'
import {createClient} from '../../index.js'
import {profile as cmtaProfile} from './index.js'
// Pick a descriptive user agent! hafas-client won't work with this string.
const client = createClient(cmtaProfile, 'hafas-client-example')
const broadieOaks = '000002370'
const domain = '000005919'
let data = await client.locations('Westgate', {results: 2})
// let data = await client.nearby({
// type: 'location',
// latitude: 30.266222,
// longitude: -97.746058
// }, {distance: 60})
// let data = await client.reachableFrom({
// type: 'location',
// address: '604 W 9TH ST, Austin, TX 78701',
// latitude: 30.272910,
// longitude: -97.747883
// }, {
// when: new Date('2018-08-27T10:00:00+0200'),
// maxDuration: 15
// })
// let data = await client.stop('000005534') // Downtown light rail station
// let data = await client.departures(broadieOaks, {duration: 1})
// let data = await client.arrivals(broadieOaks, {duration: 10, linesOfStops: true})
// let data = await client.journeys(broadieOaks, domain, {results: 1, polylines: true})
// {
// const [journey] = data.journeys
// data = await client.refreshJourney(journey.refreshToken, {
// stopovers: true,
// remarks: true,
// })
// }
// {
// const [journey] = data.journeys
// const leg = journey.legs[0]
// data = await client.trip(leg.tripId, {polyline: true})
// }
// let data = await client.radar({
// north: 30.240877,
// west: -97.804588,
// south: 30.225378,
// east: -97.786692
// }, {results: 10})
console.log(inspect(data, {depth: null, colors: true}))

View file

@ -1,26 +0,0 @@
// todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
const baseProfile = require('./base.json');
import {products} from './products.js';
const profile = {
...baseProfile,
locale: 'en-US',
timezone: 'America/Chicago',
products,
refreshJourneyUseOutReconL: true,
trip: true,
radar: true,
refreshJourney: true,
reachableFrom: true,
remarks: true, // `.svcResL[0].res.msgL[]` is missing though 🤔
};
export {
profile,
};

View file

@ -1,30 +0,0 @@
const products = [
{
id: 'bus',
mode: 'bus',
bitmasks: [32],
name: 'MetroBus',
short: 'B',
default: true,
},
{
id: 'rapid',
mode: 'bus',
bitmasks: [4096],
name: 'MetroRapid',
short: 'R',
default: true,
},
{
id: 'rail',
mode: 'train',
bitmasks: [8],
name: 'MetroRail',
short: 'M',
default: true,
},
];
export {
products,
};

View file

@ -1,20 +0,0 @@
# CMTA profile for `hafas-client`
[*Capital Metropolitan Transportation Authority (CMTA)* or *CapMetro*](https://en.wikipedia.org/wiki/Capital_Metropolitan_Transportation_Authority) is a public transportation provider serving [Austin, Texas](https://en.wikipedia.org/wiki/Austin,_Texas) metropolitan area. This profile adds *CMTA*-specific customizations to `hafas-client`.
## Usage
```js
import {createClient} from 'hafas-client'
import {profile as cmtaProfile} from 'hafas-client/p/cmta/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
// create a client with CMTA profile
const client = createClient(cmtaProfile, userAgent)
```
## Customisations
- parses *CMTA*-specific products (such as *MetroRapid* and *MetroRail*)

View file

@ -1,14 +0,0 @@
{
"auth": {
"type": "AID",
"aid": "XNFGL2aSkxfDeK8N4waOZnsdJ"
},
"client": {
"type": "WEB",
"id": "DART",
"name": "webapp"
},
"endpoint": "https://dart.hafas.de/bin/mgate.exe",
"ver": "1.35",
"defaultLanguage": "en"
}

View file

@ -1,57 +0,0 @@
import {inspect} from 'util'
import {createClient} from '../../index.js'
import {profile as dartProfile} from './index.js'
// Pick a descriptive user agent! hafas-client won't work with this string.
const client = createClient(dartProfile, 'hafas-client example')
const mlkJrPkwyAdamsAveDsm2055 = '100002702'
const se5thStEHackleyAveDsm2294 = '100004972'
let data = await client.locations('adams ave', {results: 3})
// let data = await client.nearby({
// type: 'location',
// id: '980010311',
// address: 'Austraße 37, 6700 Bludenz',
// latitude: 41.6056,
// longitude: -93.5916,
// }, {distance: 1000})
// let data = await client.reachableFrom({
// type: 'location',
// latitude: 41.613584,
// longitude: -93.881803,
// address: 'Laurel St, Waukee, 50263',
// }, {
// maxDuration: 20,
// })
// let data = await client.stop(mlkJrPkwyAdamsAveDsm2055, {linesOfStops: true})
// let data = await client.departures(mlkJrPkwyAdamsAveDsm2055, {duration: 10})
// let data = await client.arrivals(mlkJrPkwyAdamsAveDsm2055, {linesOfStops: true})
// let data = await client.journeys(mlkJrPkwyAdamsAveDsm2055, se5thStEHackleyAveDsm2294, {
// results: 1,
// stopovers: true,
// })
// {
// const [journey] = data.journeys
// data = await client.refreshJourney(journey.refreshToken, {
// stopovers: true,
// remarks: true,
// })
// }
// {
// const [journey] = data.journeys
// const leg = journey.legs.find(l => !!l.line)
// data = await client.trip(leg.tripId, {polyline: true})
// }
// let data = await client.radar({
// north: 41.6266,
// west: -93.7299,
// south: 41.5503,
// east: -93.5699,
// })
console.log(inspect(data, {depth: null, colors: true}))

View file

@ -1,32 +0,0 @@
// todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
const baseProfile = require('./base.json');
const products = [{
id: 'bus',
mode: 'bus',
bitmasks: [32],
name: 'Bus',
short: 'Bus',
default: true,
}];
const profile = {
...baseProfile,
locale: 'en-US',
timezone: 'America/Chicago',
products,
refreshJourneyUseOutReconL: true,
trip: true,
reachableFrom: true,
radar: true,
};
export {
profile,
};

View file

@ -1,19 +0,0 @@
# DART profile for `hafas-client`
[*Des Moines Area Rapid Transit (DART)*](https://en.wikipedia.org/wiki/Des_Moines_Area_Regional_Transit) is the local transport provider of [Des Moines](https://en.wikipedia.org/wiki/Des_Moines_metropolitan_area), Iowa, USA. This profile adds *DART* support to `hafas-client`.
*Note:* This profile *does not* support [*Dallas Area Rapid Transit (DART)*](https://de.wikipedia.org/wiki/Verkehrsverbund_Vorarlberg) in [DallasFort Worth](https://en.wikipedia.org/wiki/DallasFort_Worth_metroplex), Texas, USA.
## Usage
```js
import {createClient} from 'hafas-client'
import {profile as dartProfile} from 'hafas-client/p/dart/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
// create a client with DART profile
const client = createClient(dartProfile, userAgent)
```
Check out the [code examples](example.js).

View file

@ -1,16 +0,0 @@
{
"auth": {
"type": "AID",
"aid": "OGBAqytjHhCvr0J4"
},
"client": {
"type": "AND",
"id": "DB-REGIO",
"v": 100021,
"name": "DB Busradar NRW"
},
"endpoint": "https://db-regio.hafas.de/bin/hci/mgate.exe",
"ext": "DB.REGIO.1",
"ver": "1.24",
"defaultLanguage": "de"
}

View file

@ -1,44 +0,0 @@
import {inspect} from 'util'
import {createClient} from '../../index.js'
import {profile as dbbusradarnrwProfile} from './index.js'
// Pick a descriptive user agent! hafas-client won't work with this string.
const client = createClient(dbbusradarnrwProfile, 'hafas-client-example')
const hagenBauhaus = '3307002'
const schwerteBahnhof = '3357026'
let data = await client.locations('Hagen Vorhalle')
// let data = await client.nearby({
// type: 'location',
// latitude: 51.38,
// longitude: 7.45
// }, {results: 1})
// let data = await client.stop(hagenBauhaus) // Hagen Bauhaus
// let data = await client.departures(hagenBauhaus, {duration: 60})
// let data = await client.arrivals(hagenBauhaus, {duration: 30, linesOfStops: true})
// let data = await client.journeys(hagenBauhaus, schwerteBahnhof, {results: 1})
// {
// const [journey] = data.journeys
// data = await client.refreshJourney(journey.refreshToken, {
// stopovers: true,
// remarks: true,
// })
// }
// {
// const [journey] = data.journeys
// const leg = journey.legs[0]
// data = await client.trip(leg.tripId, {polyline: true})
// }
// let data = await client.radar({
// north: 51.5,
// west: 7.2,
// south: 51.2,
// east: 7.8
// }, {results: 10})
console.log(inspect(data, {depth: null, colors: true}))

View file

@ -1,98 +0,0 @@
// todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
const baseProfile = require('./base.json');
// DB Busradar NRW app does not allow selecting specific modes of transport to filter results,
// so the bitmasks had to be determined by querying some stations and looking at the results..
const products = [
{
id: 'national-express',
mode: 'train',
bitmasks: [1],
name: 'InterCityExpress',
short: 'ICE',
default: true,
},
{
id: 'national',
mode: 'train',
bitmasks: [2],
name: 'InterCity & EuroCity',
short: 'IC/EC',
default: true,
},
// todo: not always true when a station has RE stopping at it
// maybe something else?
{
id: 'regional-express',
mode: 'train',
bitmasks: [4],
name: 'Regionalexpress',
short: 'RE',
default: true,
},
// todo: also used for replacement service incl. S-Bahn replacement
{
id: 'regional',
mode: 'train',
bitmasks: [8],
name: 'Regionalzug',
short: 'RB/RE',
default: true,
},
{
id: 'suburban',
mode: 'train',
bitmasks: [16],
name: 'S-Bahn',
short: 'S',
default: true,
},
{
id: 'bus',
mode: 'bus',
bitmasks: [32],
name: 'Bus',
short: 'Bus',
default: true,
},
{
id: 'ferry',
mode: 'watercraft',
bitmasks: [64],
name: 'Ferry',
short: 'F',
default: true,
},
// todo: are `128` & `256` unused?
{
id: 'taxi',
mode: 'taxi',
bitmasks: [512],
name: 'AnrufSammelTaxi',
short: 'AST',
default: true,
},
];
const profile = {
...baseProfile,
locale: 'de-DE',
timezone: 'Europe/Berlin',
products: products,
refreshJourneyUseOutReconL: true,
journeysOutFrwd: false,
trip: true,
radar: true,
remarks: true, // `.svcResL[0].res.msgL[]` is missing though 🤔
lines: false, // `.svcResL[0].res.lineL[]` is missing 🤔
};
export {
profile,
};

View file

@ -1,21 +0,0 @@
# DB Busradar NRW profile for `hafas-client`
[*DB Busradar NRW*](https://www.bahn.de/westfalenbus/view/fahrplan/busradar.shtml) is a mobile application used in [North Rhine-Westphalia](https://en.wikipedia.org/wiki/North_Rhine-Westphalia).
It shows realtime locations and arrival/departure information for vehicles operated by bus companies which are part of [DB Regio Bus](https://www.dbregio.de/db_regio/view/wir/bus.shtml) in NRW, namely:
- [BVR Busverkehr Rheinland GmbH](https://www.rheinlandbus.de/) (DB Rheinlandbus)
- [WB Westfalen Bus GmbH](https://www.westfalenbus.de/) (DB Westfalenbus)
- [BVO Busverkehr Ostwestfalen GmbH](https://www.ostwestfalen-lippe-bus.de) (DB Ostwestfalen-Lippe-Bus)
This profile adapts `hafas-client` to the HAFAS endpoint used by the application.
## Usage
```js
import {createClient} from 'hafas-client'
import {profile as dbbusradarnrwProfile} from 'hafas-client/p/db-busradar-nrw/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
// create a client with DB Busradar NRW profile
const client = createClient(dbbusradarnrwProfile, userAgent)
```

View file

@ -13,6 +13,14 @@ const ageGroup = {
}, },
}; };
const ageGroupLabel = {
'B': 'KLEINKIND',
'K': 'FAMILIENKIND',
'Y': 'JUGENDLICHER',
'E': 'ERWACHSENER',
'S': 'SENIOR',
};
const ageGroupFromAge = (age) => { const ageGroupFromAge = (age) => {
const {upperBoundOf} = ageGroup; const {upperBoundOf} = ageGroup;
if (age < upperBoundOf.BABY) { if (age < upperBoundOf.BABY) {
@ -35,5 +43,6 @@ const ageGroupFromAge = (age) => {
export { export {
ageGroup, ageGroup,
ageGroupLabel,
ageGroupFromAge, ageGroupFromAge,
}; };

View file

@ -1,17 +1,4 @@
{ {
"auth": { "journeysEndpoint": "https://int.bahn.de/web/api/angebote/fahrplan",
"type": "AID",
"aid": "n91dB8Z77MLdoR0K"
},
"salt": "6264493855566A34304B356676787766",
"client": {
"type": "AND",
"id": "DB",
"v": 21120000,
"name": "DB Navigator"
},
"endpoint": "https://reiseauskunft.bahn.de/bin/mgate.exe",
"ext": "DB.R21.12.a",
"ver": "1.34",
"defaultLanguage": "en" "defaultLanguage": "en"
} }

View file

@ -14,30 +14,18 @@ import {parseJourneyLeg as _parseJourneyLeg} from '../../parse/journey-leg.js';
import {parseLine as _parseLine} from '../../parse/line.js'; import {parseLine as _parseLine} from '../../parse/line.js';
import {parseArrival as _parseArrival} from '../../parse/arrival.js'; import {parseArrival as _parseArrival} from '../../parse/arrival.js';
import {parseDeparture as _parseDeparture} from '../../parse/departure.js'; import {parseDeparture as _parseDeparture} from '../../parse/departure.js';
import {parseHint as _parseHint} from '../../parse/hint.js';
import {parseLocation as _parseLocation} from '../../parse/location.js'; import {parseLocation as _parseLocation} from '../../parse/location.js';
import {formatStation as _formatStation} from '../../format/station.js'; import {formatStation as _formatStation} from '../../format/station.js';
import {parseDateTime} from '../../parse/date-time.js';
import {bike} from '../../format/filters.js'; import {bike} from '../../format/filters.js';
const baseProfile = require('./base.json'); const baseProfile = require('./base.json');
import {products} from './products.js'; import {products} from './products.js';
import {formatLoyaltyCard} from './loyalty-cards.js'; import {formatLoyaltyCard} from './loyalty-cards.js';
import {ageGroup, ageGroupFromAge} from './ageGroup.js'; import {ageGroup, ageGroupFromAge, ageGroupLabel} from './ageGroup.js';
import {routingModes} from './routing-modes.js'; import {routingModes} from './routing-modes.js';
const transformReqBody = (ctx, body) => { const transformReqBody = (ctx, body) => {
const req = body.svcReqL[0] || {};
// see https://pastebin.com/qZ9WS3Cx
const rtMode = 'routingMode' in ctx.opt
? ctx.opt.routingMode
: routingModes.REALTIME;
req.cfg = {
...req.cfg,
rtMode,
};
return body; return body;
}; };
@ -162,21 +150,18 @@ loadFactors[2] = 'high';
loadFactors[3] = 'very-high'; loadFactors[3] = 'very-high';
loadFactors[4] = 'exceptionally-high'; loadFactors[4] = 'exceptionally-high';
const parseLoadFactor = (opt, tcocL, tcocX) => { const parseLoadFactor = (opt, auslastung) => {
const cls = opt.firstClass const cls = opt.firstClass
? 'FIRST' ? 'KLASSE_1'
: 'SECOND'; : 'KLASSE_2';
const load = tcocX.map(i => tcocL[i]) const load = auslastung?.find(a => a.klasse === cls)?.stufe;
.find(lf => lf.c === cls);
return load && loadFactors[load.r] || null; return load && loadFactors[load.r] || null;
}; };
const parseArrOrDepWithLoadFactor = ({parsed, res, opt}, d) => { const parseArrOrDepWithLoadFactor = ({parsed, res, opt}, d) => {
if (d.stbStop.dTrnCmpSX && Array.isArray(d.stbStop.dTrnCmpSX.tcocX)) { const load = parseLoadFactor(opt, d);
const load = parseLoadFactor(opt, res.common.tcocL || [], d.stbStop.dTrnCmpSX.tcocX); if (load) {
if (load) { parsed.loadFactor = load;
parsed.loadFactor = load;
}
} }
return parsed; return parsed;
}; };
@ -193,35 +178,30 @@ Pass in just opt.age, and the age group will calculated automatically.`);
: opt.ageGroup; : opt.ageGroup;
const basicCtrfReq = { const basicCtrfReq = {
jnyCl: opt.firstClass === true ? 1 : 2, klasse: opt.firstClass === true ? 'KLASSE_1' : 'KLASSE_2',
// todo [breaking]: support multiple travelers // todo [breaking]: support multiple travelers
tvlrProf: [{ reisende: [{
type: tvlrAgeGroup || ageGroup.ADULT, typ: ageGroupLabel[tvlrAgeGroup || ageGroup.ADULT],
...'age' in opt anzahl: 1,
? {age: opt.age} alter: 'age' in opt
: {}, ? [opt.age]
redtnCard: opt.loyaltyCard : [],
/*ermaessigungen: opt.loyaltyCard TODO
? formatLoyaltyCard(opt.loyaltyCard) ? formatLoyaltyCard(opt.loyaltyCard)
: null, : null,*/
}], ermaessigungen: [
cType: 'PK', {
"art": "KEINE_ERMAESSIGUNG",
"klasse": "KLASSENLOS"
}
]
}]
}; };
if (refreshJourney && opt.tickets) {
// todo: what are these?
// basicCtrfReq.directESuiteCall = true
// If called with "Reconstruction"
// 'DB-PE' causes the response to contain the tariff information.
basicCtrfReq.rType = 'DB-PE';
}
return basicCtrfReq; return basicCtrfReq;
}; };
const transformJourneysQuery = ({opt}, query) => { const transformJourneysQuery = ({opt}, query) => {
const filters = query.jnyFltrL; query = Object.assign(query, trfReq(opt, false));
if (opt.bike) {
filters.push(bike);
}
query.trfReq = trfReq(opt, false);
return query; return query;
}; };
@ -322,24 +302,14 @@ const parseLineWithAdditionalName = ({parsed}, l) => {
// todo: conSubscr, showARSLink, useableTime // todo: conSubscr, showARSLink, useableTime
const mutateToAddPrice = (parsed, raw) => { const mutateToAddPrice = (parsed, raw) => {
parsed.price = null; parsed.price = null;
// todo: find cheapest, find discounts // TODO find all prices?
if ( if (raw.angebotsPreis?.betrag) {
raw.trfRes parsed.price = {
&& Array.isArray(raw.trfRes.fareSetL) amount: raw.angebotsPreis.betrag,
&& raw.trfRes.fareSetL[0] currency: raw.angebotsPreis.waehrung,
&& Array.isArray(raw.trfRes.fareSetL[0].fareL) hint: null,
&& raw.trfRes.fareSetL[0].fareL[0] };
) {
const tariff = raw.trfRes.fareSetL[0].fareL[0];
if (tariff.price && tariff.price.amount >= 0) { // wat
parsed.price = {
amount: tariff.price.amount / 100,
currency: 'EUR',
hint: null,
};
}
} }
return parsed; return parsed;
}; };
@ -402,17 +372,14 @@ const mutateToAddTickets = (parsed, opt, j) => {
const parseJourneyWithPriceAndTickets = ({parsed, opt}, raw) => { const parseJourneyWithPriceAndTickets = ({parsed, opt}, raw) => {
mutateToAddPrice(parsed, raw); mutateToAddPrice(parsed, raw);
mutateToAddTickets(parsed, opt, raw); //mutateToAddTickets(parsed, opt, raw); TODO
return parsed; return parsed;
}; };
const parseJourneyLegWithLoadFactor = ({parsed, res, opt}, raw) => { const parseJourneyLegWithLoadFactor = ({parsed, res, opt}, raw) => {
const tcocX = raw.jny && raw.jny.dTrnCmpSX && raw.jny.dTrnCmpSX.tcocX; const load = parseLoadFactor(opt, raw.auslastungsmeldungen);
if (Array.isArray(tcocX) && Array.isArray(res.common.tcocL)) { if (load) {
const load = parseLoadFactor(opt, res.common.tcocL, tcocX); parsed.loadFactor = load;
if (load) {
parsed.loadFactor = load;
}
} }
return parsed; return parsed;
}; };
@ -618,28 +585,12 @@ const codesByText = Object.assign(Object.create(null), {
'platform change': 'changed platform', // todo: use dash, German variant 'platform change': 'changed platform', // todo: use dash, German variant
}); });
const parseHintByCode = ({parsed}, raw) => { const parseHintByCode = (raw) => {
// plain-text hints used e.g. for stop metadata const hint = hintsByCode[raw.key.trim().toLowerCase()];
if (raw.type === 'K') { if (hint) {
return {type: 'hint', text: raw.txtN}; return Object.assign({text: raw.value}, hint);
} }
return null;
if (raw.type === 'A') {
const hint = hintsByCode[raw.code && raw.code.trim()
.toLowerCase()];
if (hint) {
return Object.assign({text: raw.txtN}, hint);
}
}
if (parsed && raw.txtN) {
const text = trim(raw.txtN.toLowerCase(), ' ()');
if (codesByText[text]) {
parsed.code = codesByText[text];
}
}
return parsed;
}; };
const isIBNR = /^\d{6,}$/; const isIBNR = /^\d{6,}$/;
@ -670,8 +621,9 @@ const profile = {
parseLine: parseHook(_parseLine, parseLineWithAdditionalName), parseLine: parseHook(_parseLine, parseLineWithAdditionalName),
parseArrival: parseHook(_parseArrival, parseArrOrDepWithLoadFactor), parseArrival: parseHook(_parseArrival, parseArrOrDepWithLoadFactor),
parseDeparture: parseHook(_parseDeparture, parseArrOrDepWithLoadFactor), parseDeparture: parseHook(_parseDeparture, parseArrOrDepWithLoadFactor),
parseHint: parseHook(_parseHint, parseHintByCode), parseDateTime,
parseLoadFactor,
parseHintByCode,
formatStation, formatStation,
generateUnreliableTicketUrls: false, generateUnreliableTicketUrls: false,

View file

@ -1,4 +1,3 @@
// todo: https://gist.github.com/anonymous/d3323a5d2d6e159ed42b12afd0380434#file-haf_products-properties-L1-L95
const products = [ const products = [
{ {
id: 'nationalExpress', id: 'nationalExpress',
@ -6,6 +5,7 @@ const products = [
bitmasks: [1], bitmasks: [1],
name: 'InterCityExpress', name: 'InterCityExpress',
short: 'ICE', short: 'ICE',
vendo: 'ICE',
default: true, default: true,
}, },
{ {
@ -14,14 +14,16 @@ const products = [
bitmasks: [2], bitmasks: [2],
name: 'InterCity & EuroCity', name: 'InterCity & EuroCity',
short: 'IC/EC', short: 'IC/EC',
vendo: 'EC_IC',
default: true, default: true,
}, },
{ {
id: 'regionalExpress', id: 'regionalExpress',
mode: 'train', mode: 'train',
bitmasks: [4], bitmasks: [4],
name: 'RegionalExpress & InterRegio', name: 'RegionalExpress & InterRegio', // FlixTrain??
short: 'RE/IR', short: 'RE/IR',
vendo: 'IR',
default: true, default: true,
}, },
{ {
@ -30,6 +32,7 @@ const products = [
bitmasks: [8], bitmasks: [8],
name: 'Regio', name: 'Regio',
short: 'RB', short: 'RB',
vendo: 'REGIONAL',
default: true, default: true,
}, },
{ {
@ -38,6 +41,7 @@ const products = [
bitmasks: [16], bitmasks: [16],
name: 'S-Bahn', name: 'S-Bahn',
short: 'S', short: 'S',
vendo: 'SBAHN',
default: true, default: true,
}, },
{ {
@ -46,6 +50,7 @@ const products = [
bitmasks: [32], bitmasks: [32],
name: 'Bus', name: 'Bus',
short: 'B', short: 'B',
vendo: 'BUS',
default: true, default: true,
}, },
{ {
@ -54,6 +59,7 @@ const products = [
bitmasks: [64], bitmasks: [64],
name: 'Ferry', name: 'Ferry',
short: 'F', short: 'F',
vendo: 'SCHIFF',
default: true, default: true,
}, },
{ {
@ -62,6 +68,7 @@ const products = [
bitmasks: [128], bitmasks: [128],
name: 'U-Bahn', name: 'U-Bahn',
short: 'U', short: 'U',
vendo: 'UBAHN',
default: true, default: true,
}, },
{ {
@ -70,6 +77,7 @@ const products = [
bitmasks: [256], bitmasks: [256],
name: 'Tram', name: 'Tram',
short: 'T', short: 'T',
vendo: 'TRAM',
default: true, default: true,
}, },
{ {
@ -78,6 +86,7 @@ const products = [
bitmasks: [512], bitmasks: [512],
name: 'Group Taxi', name: 'Group Taxi',
short: 'Taxi', short: 'Taxi',
vendo: 'ANRUFPFLICHTIG',
default: true, default: true,
}, },
]; ];

View file

@ -1,15 +0,0 @@
{
"auth": {
"type": "AID",
"aid": "nasa-apps"
},
"client": {
"type": "IPH",
"id": "NASA",
"v": "4000200",
"name": "nasaPROD"
},
"endpoint": "https://reiseauskunft.insa.de/bin/mgate.exe",
"ver": "1.44",
"defaultLanguage": "de"
}

View file

@ -1,48 +0,0 @@
import {inspect} from 'util'
import {createClient} from '../../index.js'
import {profile as insaProfile} from './index.js'
// Pick a descriptive user agent! hafas-client won't work with this string.
const client = createClient(insaProfile, 'hafas-client-example')
const magdeburgNeustadt = '008010226'
const magdeburgBuckau = '008013456'
const hellestr1 = {
type: 'location',
id: '980801263',
address: 'Magdeburg - Leipziger Straße, Hellestraße 1',
latitude: 52.116706, longitude: 11.621821
}
let data = await client.locations('Magdeburg Hbf', {results: 2})
// let data = await client.locations('Kunstmuseum Kloster Unser Lieben Frauen Magdeburg', {results: 2})
// let data = await client.nearby(hellestr1)
// let data = await client.reachableFrom(hellestr1, {maxDuration: 10})
// let data = await client.stop(magdeburgNeustadt)
// let data = await client.departures(magdeburgNeustadt, { duration: 5 })
// let data = await client.arrivals('8010226', {duration: 10, linesOfStops: true})
// let data = await client.journeys(magdeburgNeustadt, magdeburgBuckau, {results: 1})
// {
// const [journey] = data.journeys
// data = await client.refreshJourney(journey.refreshToken, {
// stopovers: true,
// remarks: true,
// })
// }
// {
// const [journey] = data.journeys
// const leg = journey.legs[0]
// data = await client.trip(leg.tripId, leg.line.name)
// }
// let data = await client.radar({
// north: 52.148364,
// west: 11.600826,
// south: 52.108486,
// east: 11.651451
// }, {results: 10})
console.log(inspect(data, {depth: null, colors: true}))

View file

@ -1,24 +0,0 @@
// todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
const baseProfile = require('./base.json');
import {products} from './products.js';
const profile = {
...baseProfile,
locale: 'de-DE',
timezone: 'Europe/Berlin',
products: products,
trip: true,
radar: true,
refreshJourneyUseOutReconL: true,
reachableFrom: true,
};
export {
profile,
};

View file

@ -1,62 +0,0 @@
const products = [
{
id: 'nationalExpress',
mode: 'train',
bitmasks: [1],
name: 'InterCityExpress',
short: 'ICE',
default: true,
},
{
id: 'national',
mode: 'train',
bitmasks: [2],
name: 'InterCity & EuroCity',
short: 'IC/EC',
default: true,
},
{
id: 'regional',
mode: 'train',
bitmasks: [8],
name: 'RegionalExpress & RegionalBahn',
short: 'RE/RB',
default: true,
},
{
id: 'suburban',
mode: 'train',
bitmasks: [16],
name: 'S-Bahn',
short: 'S',
default: true,
},
{
id: 'tram',
mode: 'train',
bitmasks: [32],
name: 'Tram',
short: 'T',
default: true,
},
{
id: 'bus',
mode: 'bus',
bitmasks: [64, 128],
name: 'Bus',
short: 'B',
default: true,
},
{
id: 'tourismTrain',
mode: 'train',
bitmasks: [256],
name: 'Tourism Train',
short: 'TT',
default: true,
},
];
export {
products,
};

View file

@ -1,20 +0,0 @@
# INSA profile for `hafas-client`
The [Nahverkehr Sachsen-Anhalt (NASA)](https://de.wikipedia.org/wiki/Nahverkehrsservice_Sachsen-Anhalt) offers [Informationssystem Nahverkehr Sachsen-Anhalt (INSA)](https://insa.de) to distribute their public transport data.
## Usage
```js
import {createClient} from 'hafas-client'
import {profile as insaProfile} from 'hafas-client/p/insa/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
// create a client with INSA profile
const client = createClient(insaProfile, userAgent)
```
## Customisations
- parses *INSA*-specific products (such as *Tourism Train*)

View file

@ -1,15 +0,0 @@
{
"auth": {
"type": "AID",
"aid": "GITvwi3BGOmTQ2a5"
},
"client": {
"type": "IPH",
"id": "INVG",
"v": "1040000",
"name": "invgPROD-APPSTORE-LIVE"
},
"endpoint": "https://fpa.invg.de/bin/mgate.exe",
"ver": "1.39",
"defaultLanguage": "de"
}

View file

@ -1,45 +0,0 @@
import {inspect} from 'util'
import {createClient} from '../../index.js'
import {profile as invgProfile} from './index.js'
// Pick a descriptive user agent! hafas-client won't work with this string.
const client = createClient(invgProfile, 'hafas-client-example')
const ingolstadtHbf = '8000183'
const audiParkplatz = '84999'
let data = await client.locations('tillystr 1', {results: 2})
// let data = await client.nearby({
// type: 'location',
// latitude: 48.74453,
// longitude: 11.43733
// }, {distance: 200})
// // todo: `reachableFrom` with `Ingolstadt, Tillystraße 1` 48.745769 | 11.432814
// let data = await client.stop(audiParkplatz)
// let data = await client.departures(ingolstadtHbf, {duration: 5})
// let data = await client.arrivals(ingolstadtHbf, {duration: 10, stationLines: true})
// let data = await client.journeys(ingolstadtHbf, audiParkplatz, {results: 1})
// {
// const [journey] = data.journeys
// data = await client.refreshJourney(journey.refreshToken, {
// stopovers: true,
// remarks: true,
// })
// }
// {
// const [journey] = data.journeys
// const leg = journey.legs[0]
// data = await client.trip(leg.tripId, {polyline: true})
// }
// let data = await client.radar({
// north: 48.74453,
// west: 11.42733,
// south: 48.73453,
// east: 11.43733
// }, {results: 10})
console.log(inspect(data, {depth: null, colors: true}))

View file

@ -1,24 +0,0 @@
// todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
const baseProfile = require('./base.json');
import {products} from './products.js';
const profile = {
...baseProfile,
locale: 'de-DE',
timezone: 'Europe/Berlin',
products,
refreshJourneyUseOutReconL: true,
trip: true,
radar: true,
refreshJourney: true,
};
export {
profile,
};

View file

@ -1,71 +0,0 @@
const products = [
// https://github.com/public-transport/hafas-client/issues/93#issuecomment-437868265
{
id: 'bus',
mode: 'bus',
bitmasks: [1, 16],
name: 'Bus',
short: 'Bus',
default: true, // the other `bus` has `false`
},
{
id: 'express-train',
mode: 'train',
bitmasks: [2],
name: 'High-speed train',
short: 'Train',
default: false,
},
{
id: 'regional-train',
mode: 'train',
bitmasks: [4],
name: 'Regional train',
short: 'Train',
default: false,
},
{
id: 'local-train',
mode: 'train',
bitmasks: [8],
name: 'Nahverkehrszug',
short: 'Zug',
default: true,
},
{
id: 'ferry',
mode: 'watercraft',
bitmasks: [32],
name: 'Ferry',
short: 'Ferry',
default: false,
},
{
id: 'subway',
mode: 'train',
bitmasks: [64],
name: 'Subway',
short: 'Subway',
default: false,
},
{
id: 'tram',
mode: 'train',
bitmasks: [128],
name: 'Tram',
short: 'Tram',
default: false,
},
{
id: 'on-demand',
mode: 'bus', // todo: correct?
bitmasks: [256],
name: 'On-demand traffic',
short: 'on demand',
default: false,
},
];
export {
products,
};

View file

@ -1,20 +0,0 @@
# INVG profile for `hafas-client`
[*Ingolstädter Verkehrsgesellschaft (INVG)*](https://de.wikipedia.org/wiki/Ingolstädter_Verkehrsgesellschaft) is a public transportation provider serving [Ingolstadt, Germany](https://en.wikipedia.org/wiki/Ingolstadt). This profile adds *INVG*-specific customizations to `hafas-client`.
## Usage
```js
import {createClient} from 'hafas-client'
import {profile as invgProfile} from 'hafas-client/p/invg/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
// create a client with INVG profile
const client = createClient(invgProfile, userAgent)
```
## Customisations
- parses *INVG*-specific products

View file

@ -1,15 +0,0 @@
{
"auth": {
"type": "AID",
"aid": "P9bplgVCGnozdgQE"
},
"client": {
"type": "IPA",
"id": "IRISHRAIL",
"v": "4000100",
"name": "IrishRailPROD-APPSTORE"
},
"endpoint": "https://journeyplanner.irishrail.ie/bin/mgate.exe",
"ver": "1.33",
"defaultLanguage": "en"
}

View file

@ -1,42 +0,0 @@
import {inspect} from 'util'
import {createClient} from '../../index.js'
import {profile as irishProfile} from './index.js'
// Pick a descriptive user agent! hafas-client won't work with this string.
const client = createClient(irishProfile, 'hafas-client example')
const dublin = '9909002'
const belfastCentral = '9990840'
let data = await client.locations('Dublin', {results: 2})
// let data = await client.locations('Hochschule Dublin', {
// poi: true,
// addressses: false,
// fuzzy: false,
// })
// let data = await client.nearby({
// type: 'location',
// latitude: 53.353,
// longitude: -6.247
// }, {distance: 200})
// let data = await client.stop(dublin) // Dublin
// let data = await client.departures(dublin, {duration: 5})
// let data = await client.arrivals(dublin, {duration: 10, linesOfStops: true})
// let data = await client.journeys(dublin, belfastCentral, {results: 1})
// {
// const [journey] = data.journeys
// const leg = journey.legs[0]
// data = await client.trip(leg.tripId, {polyline: true})
// }
// let data = await client.radar({
// north: 53.35,
// west: -6.245,
// south: 53.34,
// east: -6.244
// }, {results: 10})
console.log(inspect(data, {depth: null, colors: true}))

View file

@ -1,26 +0,0 @@
// todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
const baseProfile = require('./base.json');
import {products} from './products.js';
const profile = {
...baseProfile,
locale: 'en-IE',
timezone: 'Europe/Dublin',
defaultLanguage: 'ga',
salt: Buffer.from('i5s7m3q9z6b4k1c2', 'utf8'),
addMicMac: true,
products: products,
refreshJourney: false, // fails with `CGI_READ_FAILED`
trip: true,
radar: true,
};
export {
profile,
};

View file

@ -1,40 +0,0 @@
const products = [
{
id: 'national-train',
mode: 'train',
bitmasks: [2],
name: 'InterCity',
short: 'IC',
default: true,
},
// todo: 4
{
id: 'local-train',
mode: 'train',
bitmasks: [8],
name: 'Commuter',
short: 'Commuter',
default: true,
},
{
id: 'suburban',
mode: 'train',
bitmasks: [16],
name: 'Dublin Area Rapid Transit',
short: 'DART',
default: true,
},
// todo: 32
{
id: 'luas',
mode: 'train',
bitmasks: [64],
name: 'LUAS Tram',
short: 'LUAS',
default: true,
},
];
export {
products,
};

View file

@ -1,20 +0,0 @@
# Irish Rail profile for `hafas-client`
The [*Iarnród Éireann* (Irish Rail)](https://en.wikipedia.org/wiki/Iarnród_Éireann) is the national railway company of Ireland. This profile adds *Iarnród Éireann*-specific customizations to `hafas-client`.
## Usage
```js
import {createClient} from 'hafas-client'
import {profile as irishProfile} from 'hafas-client/p/irish-rail/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
// create a client with Irish Rail profile
const client = createClient(irishProfile, userAgent)
```
## Customisations
- parses *Irish Rail*-specific products (such as *LUAS* or *DART*)

View file

@ -1,13 +0,0 @@
{
"auth": {
"type": "AID",
"aid": "wf7mcf9bv3nv8g5f"
},
"client": {
"type": "WEB",
"id": "VAO",
"name": "webapp"
},
"endpoint": "https://fahrplan.ivb.at/bin/mgate.exe",
"defaultLanguage": "de"
}

View file

@ -1,90 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIG3zCCBcegAwIBAgIQA4jrkiZxv246m64Ihrcs5zANBgkqhkiG9w0BAQsFADBZ
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMTMwMQYDVQQDEypE
aWdpQ2VydCBHbG9iYWwgRzIgVExTIFJTQSBTSEEyNTYgMjAyMCBDQTEwHhcNMjMw
NDIwMDAwMDAwWhcNMjQwNTIwMjM1OTU5WjB1MQswCQYDVQQGEwJBVDEOMAwGA1UE
CBMFVGlyb2wxEjAQBgNVBAcTCUlubnNicnVjazEoMCYGA1UEChMfSW5uc2JydWNr
ZXIgS29tbXVuYWxiZXRyaWViZSBBRzEYMBYGA1UEAxMPZmFocnBsYW4uaXZiLmF0
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwYmSrXUIcYCIqpPLZiqx
Kt3cNYtrGGXRx78tiuGUrUwvaIVAWADUkmCw4lObUU9uPVwBrz6lBfbLRTBLV4vm
lji9QN6gefH1niBWSAZsNg7g+oJTscy+MyAriZ5AxEObInFsB1TDj/KoJSwmskMB
Ys2A0GDQF0vxtvUhIUDbS1cM5c0PKxMn2ZMCrlia4JVp/UvH4fY4b4GqV9ARGg9b
fC5+b+8sf11YgNMQtv/9ybUrKXbLUoW3IkgDBCd8pu8lSHK2J4Go/aJaqtQxlypr
BOzNeMJiakFcQRy39XYewn1QuQIp+ffwdbA7bPJf6ivQ0AgmAUmX08ejAYnPyTgm
tQIDAQABo4IDhTCCA4EwHwYDVR0jBBgwFoAUdIWAwGbH3zfez70pN6oDHb7tzRcw
HQYDVR0OBBYEFDNdNH+vHib5MyGaMvsr7sHCQNfVMBoGA1UdEQQTMBGCD2ZhaHJw
bGFuLml2Yi5hdDAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEG
CCsGAQUFBwMCMIGfBgNVHR8EgZcwgZQwSKBGoESGQmh0dHA6Ly9jcmwzLmRpZ2lj
ZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbEcyVExTUlNBU0hBMjU2MjAyMENBMS0xLmNy
bDBIoEagRIZCaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFs
RzJUTFNSU0FTSEEyNTYyMDIwQ0ExLTEuY3JsMD4GA1UdIAQ3MDUwMwYGZ4EMAQIC
MCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzCBhwYI
KwYBBQUHAQEEezB5MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5j
b20wUQYIKwYBBQUHMAKGRWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdp
Q2VydEdsb2JhbEcyVExTUlNBU0hBMjU2MjAyMENBMS0xLmNydDAJBgNVHRMEAjAA
MIIBewYKKwYBBAHWeQIEAgSCAWsEggFnAWUAdQDuzdBk1dsazsVct520zROiModG
fLzs3sNRSFlGcR+1mwAAAYefRP/9AAAEAwBGMEQCIFjeLHYIYiJPLIxFR15tBOR5
dDWoBqnN5yh4gWkFZWYrAiA4VD1tijI+LZA4vHRo+n3x06S7xrkFLj0CHAnYwTF2
FwB1AHPZnokbTJZ4oCB9R53mssYc0FFecRkqjGuAEHrBd3K1AAABh59FABUAAAQD
AEYwRAIgWYNY8mjOvJRwtISFw0h5difwB+GanCeY7vs7s1eIbP0CIBz+1RMDncjK
CBgaQ9A0r3VtIRSZGpZXYgtNLlwAm2QMAHUASLDja9qmRzQP5WoC+p0w6xxSActW
3SyB2bu/qznYhHMAAAGHn0T/2gAABAMARjBEAiBqWfo/fIUsxsbAxZ+nV3telcqV
UdI0VzsVdQyDOqjFxAIgco7BJJsS98sTeQa9MgTUaV5ehsHm1pHTO91d5SWU6cMw
DQYJKoZIhvcNAQELBQADggEBAFXJiVrfiED+qd1J8RUI6FG72VlOk8AEAW9aCxwI
Ik+9whPOLy7v6zoPLdIoWLSZSRao/RE79/Kj7z4IsaYS03SlwalcjDeHkatgnql2
WUEtnj4KuDVADqLbKCRc38pVb2BZ7n9tQDk20VLdSK+tDbxCPWiFQTLPDGLe8ugZ
pXzXHv53jhy+2Im+hgZ0QDlFz9quV5G/7MJLfnZUgRgVWdhcoU5vzzhHZUIr98Lr
K9gD/8kdEa/eF2Px7THbV1uBb+TbGLBrOGhymsU9UNs+Zo48uE7mpkSCNFOptPVY
kxhTLfkHhQr7DJyWO3VRSzCkZZNuo4CeU+tyD7wTNXfZb98=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIE9DCCA9ygAwIBAgIQCF+UwC2Fe+jMFP9T7aI+KjANBgkqhkiG9w0BAQsFADBh
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH
MjAeFw0yMDA5MjQwMDAwMDBaFw0zMDA5MjMyMzU5NTlaMFkxCzAJBgNVBAYTAlVT
MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxMzAxBgNVBAMTKkRpZ2lDZXJ0IEdsb2Jh
bCBHMiBUTFMgUlNBIFNIQTI1NiAyMDIwIENBMTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAMz3EGJPprtjb+2QUlbFbSd7ehJWivH0+dbn4Y+9lavyYEEV
cNsSAPonCrVXOFt9slGTcZUOakGUWzUb+nv6u8W+JDD+Vu/E832X4xT1FE3LpxDy
FuqrIvAxIhFhaZAmunjZlx/jfWardUSVc8is/+9dCopZQ+GssjoP80j812s3wWPc
3kbW20X+fSP9kOhRBx5Ro1/tSUZUfyyIxfQTnJcVPAPooTncaQwywa8WV0yUR0J8
osicfebUTVSvQpmowQTCd5zWSOTOEeAqgJnwQ3DPP3Zr0UxJqyRewg2C/Uaoq2yT
zGJSQnWS+Jr6Xl6ysGHlHx+5fwmY6D36g39HaaECAwEAAaOCAa4wggGqMB0GA1Ud
DgQWBBR0hYDAZsffN97PvSk3qgMdvu3NFzAfBgNVHSMEGDAWgBROIlQgGJXm427m
D/r6uRLtBhePOTAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwEG
CCsGAQUFBwMCMBIGA1UdEwEB/wQIMAYBAf8CAQAwdgYIKwYBBQUHAQEEajBoMCQG
CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQAYIKwYBBQUHMAKG
NGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RH
Mi5jcnQwewYDVR0fBHQwcjA3oDWgM4YxaHR0cDovL2NybDMuZGlnaWNlcnQuY29t
L0RpZ2lDZXJ0R2xvYmFsUm9vdEcyLmNybDA3oDWgM4YxaHR0cDovL2NybDQuZGln
aWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9vdEcyLmNybDAwBgNVHSAEKTAnMAcG
BWeBDAEBMAgGBmeBDAECATAIBgZngQwBAgIwCAYGZ4EMAQIDMA0GCSqGSIb3DQEB
CwUAA4IBAQB1i8A8W+//cFxrivUh76wx5kM9gK/XVakew44YbHnT96xC34+IxZ20
dfPJCP2K/lHz8p0gGgQ1zvi2QXmv/8yWXpTTmh1wLqIxi/ulzH9W3xc3l7/BjUOG
q4xmfrnti/EPjLXUVa9ciZ7gpyptsqNjMhg7y961n4OzEQGsIA2QlxK3KZw1tdeR
Du9Ab21cO72h2fviyy52QNI6uyy/FgvqvQNbTpg6Ku0FUAcVkzxzOZGUWkgOxtNK
Aa9mObm9QjQc2wgD80D8EuiuPKuK1ftyeWSm4w5VsTuVP61gM2eKrLanXPDtWlIb
1GHhJRLmB7WqlLLwKPZhJl5VHPgB63dx
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH
MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT
MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI
2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx
1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ
q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz
tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ
vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP
BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV
5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY
1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4
NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG
Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91
8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe
pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl
MrY=
-----END CERTIFICATE-----

View file

@ -1,45 +0,0 @@
import {inspect} from 'util'
import {createClient} from '../../index.js'
import {profile as ivbProfile} from './index.js'
// Pick a descriptive user agent! hafas-client won't work with this string.
const client = createClient(ivbProfile, 'hafas-client example')
const innsbruckGriesauweg = '476162400'
const völsWest = '476431800'
const lönsstr9 = {
type: 'location',
id: '980076175',
address: 'Lönsstraße 9, 6020 Innsbruck',
latitude: 47.262765,
longitude: 11.419851,
}
let data = await client.locations('griesauweg', {results: 3})
// let data = await client.nearby(lönsstr9, {distance: 1000})
// let data = await client.reachableFrom(lönsstr9, {
// maxDuration: 30,
// })
// let data = await client.stop(innsbruckGriesauweg, {linesOfStops: true})
// let data = await client.departures(innsbruckGriesauweg, {duration: 1})
// let data = await client.arrivals(innsbruckGriesauweg, {duration: 10, linesOfStops: true})
// let data = await client.journeys(innsbruckGriesauweg, völsWest, {
// results: 1, stopovers: true,
// })
// {
// const [journey] = data.journeys
// data = await client.refreshJourney(journey.refreshToken, {
// stopovers: true,
// remarks: true,
// })
// }
// {
// const [journey] = data.journeys
// const leg = journey.legs[0]
// data = await client.trip(leg.tripId, {polyline: true})
// }
console.log(inspect(data, {depth: null, colors: true}))

View file

@ -1,106 +0,0 @@
// todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
import {readFileSync} from 'fs';
import {Agent} from 'https';
const baseProfile = require('./base.json');
const products = [{
id: 'train-and-s-bahn',
mode: 'train',
bitmasks: [1, 2],
name: 'Bahn & S-Bahn',
short: 'Bahn & S-Bahn',
default: true,
}, {
id: 'u-bahn',
mode: 'train',
bitmasks: [4],
name: 'U-Bahn',
short: 'U-Bahn',
default: true,
}, {
id: 'tram',
mode: 'train',
bitmasks: [16],
name: 'Straßenbahn',
short: 'Straßenbahn',
default: true,
}, {
id: 'city-bus',
mode: 'bus',
bitmasks: [128],
name: 'Stadtbus',
short: 'Stadtbus',
default: true,
}, {
id: 'regional-bus',
mode: 'bus',
bitmasks: [64],
name: 'Regionalbus',
short: 'Regionalbus',
default: true,
}, {
id: 'long-distance-bus',
mode: 'bus',
bitmasks: [32],
name: 'Fernbus',
short: 'Fernbus',
default: true,
}, {
id: 'other-bus',
mode: 'bus',
bitmasks: [2048],
name: 'sonstige Busse',
short: 'sonstige Busse',
default: true,
}, {
id: 'aerial-lift',
mode: 'gondola',
bitmasks: [256],
name: 'Seil-/Zahnradbahn',
short: 'Seil-/Zahnradbahn',
default: true,
}, {
id: 'ferry',
mode: 'watercraft',
bitmasks: [512],
name: 'Schiff',
short: 'Schiff',
default: true,
}, {
id: 'on-call',
mode: 'taxi',
bitmasks: [1024],
name: 'Anrufsammeltaxi',
short: 'AST',
default: true,
}];
// `fahrplan.ivb.at:443` doesn't provide the necessary CA certificate chain for
// Node.js to trust the certificate, so we manually add it.
// todo: fix this properly, e.g. by letting them know
const ca = readFileSync(new URL('./digicert-tls-rsa-sha256-2020-ca1.crt.pem', import.meta.url).pathname);
const agent = new Agent({ca});
const transformReq = (ctx, req) => ({...req, agent});
const profile = {
...baseProfile,
ver: '1.32',
transformReq,
locale: 'at-DE',
timezone: 'Europe/Vienna',
products,
refreshJourneyUseOutReconL: true,
trip: true,
reachableFrom: true,
};
export {
profile,
};

View file

@ -1,17 +0,0 @@
# IVB profile for `hafas-client`
[*Innsbrucker Verkehrsbetriebe (IVB)*](https://de.wikipedia.org/wiki/Innsbrucker_Verkehrsbetriebe_und_Stubaitalbahn) is the local transport provider of [Innsbruck](https://en.wikipedia.org/wiki/Innsbruck). This profile adds *IVB* support to `hafas-client`.
## Usage
```js
import {createClient} from 'hafas-client'
import {profile as ivbProfile} from 'hafas-client/p/ivb/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
// create a client with IVB profile
const client = createClient(ivbProfile, userAgent)
```
Check out the [code examples](example.js).

View file

@ -1,15 +0,0 @@
{
"auth": {
"type": "AID",
"aid": "Rt6foY5zcTTRXMQs"
},
"client": {
"type": "WEB",
"id": "HAFAS",
"name": "webapp",
"l": "vs_webapp"
},
"endpoint": "https://auskunft.kvb.koeln/gate",
"defaultLanguage": "de",
"ver": "1.42"
}

View file

@ -1,48 +0,0 @@
import {inspect} from 'util'
import {createClient} from '../../index.js'
import {profile as kvbProfile} from './index.js'
// Pick a descriptive user agent! hafas-client won't work with this string.
const client = createClient(kvbProfile, 'hafas-client example')
const heumarkt = '900000001'
const poststr = '900000003'
let data = await client.locations('heumarkt', {results: 3})
// let data = await client.nearby({
// type: 'location',
// id: '991023531',
// address: 'An den Dominikanern 27',
// latitude: 50.942454, longitude: 6.954064,
// }, {distance: 500})
// let data = await client.reachableFrom({
// type: 'location',
// id: '991023531',
// address: 'An den Dominikanern 27',
// latitude: 50.942454, longitude: 6.954064,
// }, {
// maxDuration: 15,
// })
// let data = await client.stop(heumarkt, {linesOfStops: true})
// let data = await client.departures(heumarkt, {duration: 1})
// let data = await client.arrivals(heumarkt, {duration: 10, linesOfStops: true})
// let data = await client.journeys(heumarkt, poststr, {
// results: 1, stopovers: true,
// })
// {
// const [journey] = data.journeys
// data = await client.refreshJourney(journey.refreshToken, {
// stopovers: true,
// remarks: true,
// })
// }
// {
// const [journey] = data.journeys
// const leg = journey.legs[0]
// data = await client.trip(leg.tripId, {polyline: true})
// }
console.log(inspect(data, {depth: null, colors: true}))

View file

@ -1,66 +0,0 @@
// todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
const baseProfile = require('./base.json');
const products = [{
id: 'stadtbahn',
mode: 'train',
bitmasks: [2],
name: 'Stadtbahn',
short: 'Stadtbahn',
default: true,
}, {
id: 'bus',
mode: 'bus',
bitmasks: [8],
name: 'Bus',
short: 'Bus',
default: true,
}, {
id: 'taxibus',
mode: 'bus',
bitmasks: [256],
name: 'Taxibus',
short: 'Taxibus',
default: true,
}, {
id: 's-bahn',
mode: 'train',
bitmasks: [1],
name: 'S-Bahn',
short: 'S',
default: true,
}, {
id: 'regionalverkehr',
mode: 'train',
bitmasks: [16],
name: 'Regionalverkehr',
short: 'Regionalverkehr',
default: true,
}, {
id: 'fernverkehr',
mode: 'train',
bitmasks: [32],
name: 'Fernverkehr',
short: 'Fernverkehr',
default: true,
}];
const profile = {
...baseProfile,
locale: 'de-DE',
timezone: 'Europe/Berlin',
products,
refreshJourneyUseOutReconL: true,
trip: true,
reachableFrom: true,
};
export {
profile,
};

View file

@ -1,17 +0,0 @@
# KVB profile for `hafas-client`
[*Kölner Verkehrs-Betriebe (KVB)*](https://de.wikipedia.org/wiki/Kölner_Verkehrs-Betriebe) is the local transport provider of [Cologne](https://en.wikipedia.org/wiki/Cologne). This profile adds *KVB* support to `hafas-client`.
## Usage
```js
import {createClient} from 'hafas-client'
import {profile as kvbProfile} from 'hafas-client/p/kvb/index.js'
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
// create a client with KVB profile
const client = createClient(kvbProfile, userAgent)
```
Check out the [code examples](example.js).

View file

@ -1,15 +0,0 @@
{
"auth": {
"type": "AID",
"aid": "Kdf0LNRWYg5k3499"
},
"client": {
"type": "IPH",
"id": "DB-REGIO-NRW",
"v": "6000300",
"name": "NRW"
},
"endpoint": "https://nrw.hafas.de/bin/mgate.exe",
"ver": "1.34",
"defaultLanguage": "de"
}

View file

@ -1,57 +0,0 @@
import {inspect} from 'util'
import {createClient} from '../../index.js'
import {profile as mobilNrwProfile} from './index.js'
// Pick a descriptive user agent! hafas-client won't work with this string.
const client = createClient(mobilNrwProfile, 'hafas-client-example')
const soest = '8000076'
const aachenHbf = '8000001'
let data = await client.locations('soest', {results: 3})
// let data = await client.nearby({
// type: 'location',
// latitude: 51.4503,
// longitude: 6.6581,
// }, {distance: 1200})
// let data = await client.reachableFrom({
// type: 'location',
// id: '980301639',
// latitude: 51.387609,
// longitude: 6.684019,
// address: 'Duisburg, Am Mühlenberg 1',
// }, {
// maxDuration: 15,
// })
// let data = await client.stop(soest)
// let data = await client.departures(soest, {duration: 20})
// let data = await client.journeys(soest, aachenHbf, {
// results: 1,
// stopovers: true,
// })
// {
// const [journey] = data.journeys
// data = await client.refreshJourney(journey.refreshToken, {
// stopovers: true,
// remarks: true,
// })
// }
// {
// const [journey] = data.journeys
// const leg = journey.legs[0]
// data = await client.trip(leg.tripId, {polyline: true})
// }
// let data = await client.radar({
// north: 51.4358,
// west: 6.7625,
// south: 51.4214,
// east: 6.7900,
// }, {results: 10})
// let data = await client.remarks()
console.log(inspect(data, {depth: null, colors: true}))

View file

@ -1,25 +0,0 @@
// todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
const baseProfile = require('./base.json');
import {products} from './products.js';
const profile = {
...baseProfile,
locale: 'de-DE',
timezone: 'Europe/Berlin',
products,
trip: true,
radar: true,
reachableFrom: true,
refreshJourneyUseOutReconL: true,
remarks: true,
};
export {
profile,
};

Some files were not shown because too many files have changed in this diff Show more