mirror of
https://github.com/public-transport/db-vendo-client.git
synced 2025-02-23 15:19:35 +02:00
merge any-endpoint into master
This commit is contained in:
commit
079964ee40
54 changed files with 3545 additions and 356 deletions
|
@ -2,5 +2,5 @@ sudo: false
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- 'stable'
|
- 'stable'
|
||||||
- '7'
|
- '8'
|
||||||
- '6'
|
- '6'
|
||||||
|
|
163
docs/departures.md
Normal file
163
docs/departures.md
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
# `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
|
||||||
|
duration: 10 // show departures for the next n minutes
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response
|
||||||
|
|
||||||
|
*Note:* As stated in the [*Friendly Public Transport Format* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/tree/1.0.1), the `when` field includes the current delay. The `delay` field, if present, expresses how much the former differs from the schedule.
|
||||||
|
|
||||||
|
You may pass the `journeyId` field into [`journeyLeg(ref, lineName, [opt])`](journey-leg.md) to get details on the vehicle's journey.
|
||||||
|
|
||||||
|
As an example, we're going to use the [VBB profile](../p/vbb):
|
||||||
|
|
||||||
|
```js
|
||||||
|
const createClient = require('hafas-client')
|
||||||
|
const vbbProfile = require('hafas-client/p/vbb')
|
||||||
|
|
||||||
|
const client = createClient(vbbProfile)
|
||||||
|
|
||||||
|
// S Charlottenburg
|
||||||
|
client.journeys('900000024101', {duration: 3})
|
||||||
|
.then(console.log)
|
||||||
|
.catch(console.error)
|
||||||
|
```
|
||||||
|
|
||||||
|
The response may look like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
[ {
|
||||||
|
journeyId: '1|31431|28|86|17122017',
|
||||||
|
trip: 31431,
|
||||||
|
station: {
|
||||||
|
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.000+01:00',
|
||||||
|
delay: null
|
||||||
|
line: {
|
||||||
|
type: 'line',
|
||||||
|
id: '18299',
|
||||||
|
name: 'S9',
|
||||||
|
public: true,
|
||||||
|
mode: 'train',
|
||||||
|
product: 'suburban',
|
||||||
|
symbol: 'S',
|
||||||
|
nr: 9,
|
||||||
|
metro: false,
|
||||||
|
express: false,
|
||||||
|
night: false,
|
||||||
|
productCode: 0,
|
||||||
|
operator: {
|
||||||
|
type: 'operator',
|
||||||
|
id: 's-bahn-berlin-gmbh',
|
||||||
|
name: 'S-Bahn Berlin GmbH'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
direction: 'S Spandau'
|
||||||
|
}, {
|
||||||
|
journeyId: '1|30977|8|86|17122017',
|
||||||
|
trip: 30977,
|
||||||
|
station: { /* … */ },
|
||||||
|
when: null,
|
||||||
|
delay: null,
|
||||||
|
cancelled: true,
|
||||||
|
line: {
|
||||||
|
type: 'line',
|
||||||
|
id: '16441',
|
||||||
|
name: 'S5',
|
||||||
|
public: true,
|
||||||
|
mode: 'train',
|
||||||
|
product: 'suburban',
|
||||||
|
symbol: 'S',
|
||||||
|
nr: 5,
|
||||||
|
metro: false,
|
||||||
|
express: false,
|
||||||
|
night: false,
|
||||||
|
productCode: 0,
|
||||||
|
operator: { /* … */ }
|
||||||
|
},
|
||||||
|
direction: 'S Westkreuz'
|
||||||
|
}, {
|
||||||
|
journeyId: '1|28671|4|86|17122017',
|
||||||
|
trip: 28671,
|
||||||
|
station: {
|
||||||
|
type: 'station',
|
||||||
|
id: '900000024202',
|
||||||
|
name: 'U Wilmersdorfer Str.',
|
||||||
|
location: {
|
||||||
|
type: 'location',
|
||||||
|
latitude: 52.506415,
|
||||||
|
longitude: 13.306777
|
||||||
|
},
|
||||||
|
products: {
|
||||||
|
suburban: false,
|
||||||
|
subway: true,
|
||||||
|
tram: false,
|
||||||
|
bus: false,
|
||||||
|
ferry: false,
|
||||||
|
express: false,
|
||||||
|
regional: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
when: '2017-12-17T19:35:00.000+01:00',
|
||||||
|
delay: 0,
|
||||||
|
line: {
|
||||||
|
type: 'line',
|
||||||
|
id: '19494',
|
||||||
|
name: 'U7',
|
||||||
|
public: true,
|
||||||
|
mode: 'train',
|
||||||
|
product: 'subway',
|
||||||
|
symbol: 'U',
|
||||||
|
nr: 7,
|
||||||
|
metro: false,
|
||||||
|
express: false,
|
||||||
|
night: false,
|
||||||
|
productCode: 1,
|
||||||
|
operator: { /* … */ }
|
||||||
|
},
|
||||||
|
direction: 'U Rudow'
|
||||||
|
} ]
|
||||||
|
```
|
118
docs/journey-leg.md
Normal file
118
docs/journey-leg.md
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
# `journeyLeg(ref, lineName, [opt])`
|
||||||
|
|
||||||
|
This method can be used to refetch information about a leg of a journey. Note that it 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 a journey leg `id` like `'1|24983|22|86|18062017'`. `lineName` must be the name of the journey leg's `line.name`. You can get them like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const createClient = require('hafas-client')
|
||||||
|
const vbbProfile = require('hafas-client/p/vbb')
|
||||||
|
|
||||||
|
const client = createClient(vbbProfile)
|
||||||
|
|
||||||
|
// Hauptbahnhof to Heinrich-Heine-Str.
|
||||||
|
client.journeys('900000003201', '900000100008', {results: 1})
|
||||||
|
.then(([journey]) => {
|
||||||
|
const leg = journey.legs[0]
|
||||||
|
return client.journeyLeg(leg.id, leg.line.name)
|
||||||
|
})
|
||||||
|
.then(console.log)
|
||||||
|
.catch(console.error)
|
||||||
|
```
|
||||||
|
|
||||||
|
With `opt`, you can override the default options, which look like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
when: new Date(),
|
||||||
|
passedStations: true // return stations on the way?
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response
|
||||||
|
|
||||||
|
*Note:* As stated in the [*Friendly Public Transport Format* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/tree/1.0.1), 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
|
||||||
|
const createClient = require('hafas-client')
|
||||||
|
const vbbProfile = require('hafas-client/p/vbb')
|
||||||
|
|
||||||
|
const client = createClient(vbbProfile)
|
||||||
|
|
||||||
|
client.journeyLeg('1|31431|28|86|17122017', 'S9', {when: 1513534689273})
|
||||||
|
.then(console.log)
|
||||||
|
.catch(console.error)
|
||||||
|
```
|
||||||
|
|
||||||
|
The response looked like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
id: '1|31431|28|86|17122017',
|
||||||
|
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.000+01:00',
|
||||||
|
departurePlatform: '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:49:00.000+01:00',
|
||||||
|
arrivalPlatform: '2',
|
||||||
|
line: {
|
||||||
|
type: 'line',
|
||||||
|
id: '18299',
|
||||||
|
name: 'S9',
|
||||||
|
public: true,
|
||||||
|
mode: 'train',
|
||||||
|
product: 'suburban',
|
||||||
|
symbol: 'S',
|
||||||
|
nr: 9,
|
||||||
|
metro: false,
|
||||||
|
express: false,
|
||||||
|
night: false,
|
||||||
|
productCode: 0,
|
||||||
|
operator: {
|
||||||
|
type: 'operator',
|
||||||
|
id: 's-bahn-berlin-gmbh',
|
||||||
|
name: 'S-Bahn Berlin GmbH'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
direction: 'S Spandau',
|
||||||
|
passed: [ /* … */ ]
|
||||||
|
}
|
||||||
|
```
|
244
docs/journeys.md
Normal file
244
docs/journeys.md
Normal file
|
@ -0,0 +1,244 @@
|
||||||
|
# `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',
|
||||||
|
name: 'foo restaurant',
|
||||||
|
latitude: 1.23,
|
||||||
|
longitude: 3.21
|
||||||
|
}
|
||||||
|
|
||||||
|
// an address, which is an FTPF `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
|
||||||
|
{
|
||||||
|
when: new Date(),
|
||||||
|
results: 5, // how many journeys?
|
||||||
|
via: null, // let journeys pass this station
|
||||||
|
passedStations: false, // return stations on the way?
|
||||||
|
transfers: 5, // maximum of 5 transfers
|
||||||
|
transferTime: 0, // minimum time for a single transfer in minutes
|
||||||
|
accessibility: 'none', // 'none', 'partial' or 'complete'
|
||||||
|
bike: false, // only bike-friendly journeys
|
||||||
|
products: {
|
||||||
|
suburban: true,
|
||||||
|
subway: true,
|
||||||
|
tram: true,
|
||||||
|
bus: true,
|
||||||
|
ferry: true,
|
||||||
|
express: true,
|
||||||
|
regional: true
|
||||||
|
},
|
||||||
|
tickets: false // return tickets? only available with some profiles
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response
|
||||||
|
|
||||||
|
*Note:* As stated in the [*Friendly Public Transport Format* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/tree/1.0.1), 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
|
||||||
|
const createClient = require('hafas-client')
|
||||||
|
const vbbProfile = require('hafas-client/p/vbb')
|
||||||
|
|
||||||
|
const client = createClient(vbbProfile)
|
||||||
|
|
||||||
|
// Hauptbahnhof to Heinrich-Heine-Str.
|
||||||
|
client.journeys('900000003201', '900000100008', {
|
||||||
|
results: 1,
|
||||||
|
passedStations: true
|
||||||
|
})
|
||||||
|
.then(console.log)
|
||||||
|
.catch(console.error)
|
||||||
|
```
|
||||||
|
|
||||||
|
The response may look like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
[ {
|
||||||
|
legs: [ {
|
||||||
|
id: '1|31041|35|86|17122017',
|
||||||
|
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: '2017-12-17T19:07:00.000+01:00',
|
||||||
|
departurePlatform: '16',
|
||||||
|
destination: {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
arrival: '2017-12-17T19:47:00.000+01:00',
|
||||||
|
arrivalPlatform: '8',
|
||||||
|
arrivalDelay: 30,
|
||||||
|
line: {
|
||||||
|
type: 'line',
|
||||||
|
id: '16845',
|
||||||
|
name: 'S7',
|
||||||
|
public: true,
|
||||||
|
mode: 'train',
|
||||||
|
product: 'suburban',
|
||||||
|
symbol: 'S',
|
||||||
|
nr: 7,
|
||||||
|
metro: false,
|
||||||
|
express: false,
|
||||||
|
night: false,
|
||||||
|
productCode: 0,
|
||||||
|
operator: {
|
||||||
|
type: 'operator',
|
||||||
|
id: 's-bahn-berlin-gmbh',
|
||||||
|
name: 'S-Bahn Berlin GmbH'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
direction: 'S Potsdam Hauptbahnhof',
|
||||||
|
passed: [ {
|
||||||
|
station: {
|
||||||
|
type: 'station',
|
||||||
|
id: '900000003201',
|
||||||
|
name: 'S+U Berlin Hauptbahnhof',
|
||||||
|
location: { /* … */ },
|
||||||
|
products: { /* … */ }
|
||||||
|
},
|
||||||
|
arrival: null,
|
||||||
|
departure: null,
|
||||||
|
cancelled: true
|
||||||
|
}, {
|
||||||
|
station: {
|
||||||
|
type: 'station',
|
||||||
|
id: '900000003102',
|
||||||
|
name: 'S Bellevue',
|
||||||
|
location: { /* … */ },
|
||||||
|
products: { /* … */ }
|
||||||
|
},
|
||||||
|
arrival: '2017-12-17T19:09:00.000+01:00',
|
||||||
|
departure: '2017-12-17T19:09:00.000+01:00'
|
||||||
|
}, /* … */ {
|
||||||
|
station: {
|
||||||
|
type: 'station',
|
||||||
|
id: '900000024101',
|
||||||
|
name: 'S Charlottenburg',
|
||||||
|
location: { /* … */ },
|
||||||
|
products: { /* … */ }
|
||||||
|
},
|
||||||
|
arrival: '2017-12-17T19:17:00.000+01:00',
|
||||||
|
departure: '2017-12-17T19:17:00.000+01:00'
|
||||||
|
} ]
|
||||||
|
} ],
|
||||||
|
origin: {
|
||||||
|
type: 'station',
|
||||||
|
id: '900000003201',
|
||||||
|
name: 'S+U Berlin Hauptbahnhof',
|
||||||
|
location: { /* … */ },
|
||||||
|
products: { /* … */ }
|
||||||
|
},
|
||||||
|
departure: '2017-12-17T19:07:00.000+01:00',
|
||||||
|
destination: {
|
||||||
|
type: 'station',
|
||||||
|
id: '900000024101',
|
||||||
|
name: 'S Charlottenburg',
|
||||||
|
location: { /* … */ },
|
||||||
|
products: { /* … */ }
|
||||||
|
},
|
||||||
|
arrival: '2017-12-17T19:47:00.000+01:00',
|
||||||
|
arrivalDelay: 30
|
||||||
|
} ]
|
||||||
|
```
|
||||||
|
|
||||||
|
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`.
|
66
docs/locations.md
Normal file
66
docs/locations.md
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
# `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: 10 // how many search results?
|
||||||
|
, stations: true
|
||||||
|
, addresses: true
|
||||||
|
, poi: true // points of interest
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response
|
||||||
|
|
||||||
|
As an example, we're going to use the [VBB profile](../p/vbb):
|
||||||
|
|
||||||
|
```js
|
||||||
|
const createClient = require('hafas-client')
|
||||||
|
const vbbProfile = require('hafas-client/p/vbb')
|
||||||
|
|
||||||
|
const client = createClient(vbbProfile)
|
||||||
|
|
||||||
|
client.locations('Alexanderplatz', {results: 3})
|
||||||
|
.then(console.log)
|
||||||
|
.catch(console.error)
|
||||||
|
```
|
||||||
|
|
||||||
|
The response may look like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
[ {
|
||||||
|
type: 'station',
|
||||||
|
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',
|
||||||
|
name: 'Berlin, Holiday Inn Centre Alexanderplatz****',
|
||||||
|
id: '900980709',
|
||||||
|
latitude: 52.523549,
|
||||||
|
longitude: 13.418441
|
||||||
|
}, { // point of interest
|
||||||
|
type: 'location',
|
||||||
|
name: 'Berlin, Hotel Agon am Alexanderplatz',
|
||||||
|
id: '900980176',
|
||||||
|
latitude: 52.524556,
|
||||||
|
longitude: 13.420266
|
||||||
|
} ]
|
||||||
|
```
|
81
docs/nearby.md
Normal file
81
docs/nearby.md
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
# `nearby(location, [opt])`
|
||||||
|
|
||||||
|
This method can be used to find stations 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/1.0.1/spec/readme.md#location-objects).
|
||||||
|
|
||||||
|
With `opt`, you can override the default options, which look like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
distance: null, // maximum walking distance in meters
|
||||||
|
poi: false, // return points of interest?
|
||||||
|
stations: true, // return stations?
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response
|
||||||
|
|
||||||
|
As an example, we're going to use the [VBB profile](../p/vbb):
|
||||||
|
|
||||||
|
```js
|
||||||
|
const createClient = require('hafas-client')
|
||||||
|
const vbbProfile = require('hafas-client/p/vbb')
|
||||||
|
|
||||||
|
const client = createClient(vbbProfile)
|
||||||
|
|
||||||
|
client.nearby({
|
||||||
|
type: 'location',
|
||||||
|
latitude: 52.5137344,
|
||||||
|
longitude: 13.4744798
|
||||||
|
}, {distance: 400})
|
||||||
|
.then(console.log)
|
||||||
|
.catch(console.error)
|
||||||
|
```
|
||||||
|
|
||||||
|
The response may look like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
[ {
|
||||||
|
type: 'station',
|
||||||
|
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: 'station',
|
||||||
|
id: '900000120540',
|
||||||
|
name: 'Scharnweberstr./Weichselstr.',
|
||||||
|
location: {
|
||||||
|
type: 'location',
|
||||||
|
latitude: 52.512339,
|
||||||
|
longitude: 13.470174
|
||||||
|
},
|
||||||
|
products: { /* … */ },
|
||||||
|
distance: 330
|
||||||
|
}, {
|
||||||
|
type: 'station',
|
||||||
|
id: '900000160544',
|
||||||
|
name: 'Rathaus Lichtenberg',
|
||||||
|
location: {
|
||||||
|
type: 'location',
|
||||||
|
latitude: 52.515908,
|
||||||
|
longitude: 13.479073
|
||||||
|
},
|
||||||
|
products: { /* … */ },
|
||||||
|
distance: 394
|
||||||
|
} ]
|
||||||
|
```
|
158
docs/radar.md
Normal file
158
docs/radar.md
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
# `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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response
|
||||||
|
|
||||||
|
*Note:* As stated in the [*Friendly Public Transport Format* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/tree/1.0.1), 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
|
||||||
|
const createClient = require('hafas-client')
|
||||||
|
const vbbProfile = require('hafas-client/p/vbb')
|
||||||
|
|
||||||
|
const client = createClient(vbbProfile)
|
||||||
|
|
||||||
|
client.radar(52.52411, 13.41002, 52.51942, 13.41709, {results: 5})
|
||||||
|
.then(console.log)
|
||||||
|
.catch(console.error)
|
||||||
|
```
|
||||||
|
|
||||||
|
The response may look like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
[ {
|
||||||
|
location: {
|
||||||
|
type: 'location',
|
||||||
|
latitude: 52.521508,
|
||||||
|
longitude: 13.411267
|
||||||
|
},
|
||||||
|
line: {
|
||||||
|
type: 'line',
|
||||||
|
id: 's9',
|
||||||
|
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,
|
||||||
|
nextStops: [ {
|
||||||
|
station: {
|
||||||
|
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: null,
|
||||||
|
arrivalDelay: null,
|
||||||
|
departure: '2017-12-17T19:16:00.000+01:00',
|
||||||
|
departureDelay: null
|
||||||
|
} /* … */ ],
|
||||||
|
frames: [ {
|
||||||
|
origin: {
|
||||||
|
type: 'station',
|
||||||
|
id: '900000100003',
|
||||||
|
name: 'S+U Alexanderplatz',
|
||||||
|
location: { /* … */ },
|
||||||
|
products: { /* … */ }
|
||||||
|
},
|
||||||
|
destination: {
|
||||||
|
type: 'station',
|
||||||
|
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',
|
||||||
|
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,
|
||||||
|
nextStops: [ {
|
||||||
|
station: { /* S+U Alexanderplatz/Dircksenstr. */ },
|
||||||
|
arrival: null,
|
||||||
|
arrivalDelay: null,
|
||||||
|
departure: '2017-12-17T19:52:00.000+01:00',
|
||||||
|
departureDelay: null
|
||||||
|
}, {
|
||||||
|
station: { /* Memhardstr. */ },
|
||||||
|
arrival: '2017-12-17T19:54:00.000+01:00',
|
||||||
|
arrivalDelay: null,
|
||||||
|
departure: '2017-12-17T19:54:00.000+01:00',
|
||||||
|
departureDelay: null
|
||||||
|
}, /* … */ ],
|
||||||
|
frames: [ {
|
||||||
|
origin: { /* S+U Alexanderplatz/Dircksenstr. */ },
|
||||||
|
destination: { /* Memhardstr. */ },
|
||||||
|
t: 0
|
||||||
|
}, /* … */ {
|
||||||
|
origin: { /* Memhardstr. */ },
|
||||||
|
destination: { /* Mollstr./Prenzlauer Allee */ },
|
||||||
|
t: 30000
|
||||||
|
} ]
|
||||||
|
}, /* … */ ]
|
||||||
|
```
|
20
format/address.js
Normal file
20
format/address.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const formatCoord = require('./coord')
|
||||||
|
|
||||||
|
const formatAddress = (a) => {
|
||||||
|
if (a.type !== 'location' || !a.latitude || !a.longitude || !a.address) {
|
||||||
|
throw new Error('invalid address')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'A',
|
||||||
|
name: a.address,
|
||||||
|
crd: {
|
||||||
|
x: formatCoord(a.longitude),
|
||||||
|
y: formatCoord(a.latitude)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = formatAddress
|
5
format/coord.js
Normal file
5
format/coord.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const formatCoord = x => Math.round(x * 1000000)
|
||||||
|
|
||||||
|
module.exports = formatCoord
|
12
format/date.js
Normal file
12
format/date.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const {DateTime} = require('luxon')
|
||||||
|
|
||||||
|
const formatDate = (profile, when) => {
|
||||||
|
return DateTime.fromMillis(+when, {
|
||||||
|
locale: profile.locale,
|
||||||
|
zone: profile.timezone
|
||||||
|
}).toFormat('yyyyMMdd')
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = formatDate
|
11
format/filters.js
Normal file
11
format/filters.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const bike = {type: 'BC', mode: 'INC'}
|
||||||
|
|
||||||
|
const accessibility = {
|
||||||
|
none: {type: 'META', mode: 'INC', meta: 'notBarrierfree'},
|
||||||
|
partial: {type: 'META', mode: 'INC', meta: 'limitedBarrierfree'},
|
||||||
|
complete: {type: 'META', mode: 'INC', meta: 'completeBarrierfree'}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {bike, accessibility}
|
13
format/index.js
Normal file
13
format/index.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
date: require('./date'),
|
||||||
|
time: require('./time'),
|
||||||
|
filters: require('./filters'),
|
||||||
|
station: require('./station'),
|
||||||
|
address: require('./address'),
|
||||||
|
poi: require('./poi'),
|
||||||
|
location: require('./location'),
|
||||||
|
locationFilter: require('./location-filter'),
|
||||||
|
rectangle: require('./rectangle')
|
||||||
|
}
|
8
format/location-filter.js
Normal file
8
format/location-filter.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const formatLocationFilter = (stations, addresses, poi) => {
|
||||||
|
if (stations && addresses && poi) return 'ALL'
|
||||||
|
return (stations ? 'S' : '') + (addresses ? 'A' : '') + (poi ? 'P' : '')
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = formatLocationFilter
|
14
format/location.js
Normal file
14
format/location.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const formatLocation = (profile, l) => {
|
||||||
|
if ('string' === typeof l) return profile.formatStation(l)
|
||||||
|
if ('object' === typeof l && !Array.isArray(l)) {
|
||||||
|
if (l.type === 'station') return profile.formatStation(l.id)
|
||||||
|
if ('string' === typeof l.id) return profile.formatPoi(l)
|
||||||
|
if ('string' === typeof l.address) return profile.formatAddress(l)
|
||||||
|
throw new Error('invalid location type: ' + l.type)
|
||||||
|
}
|
||||||
|
throw new Error('valid station, address or poi required.')
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = formatLocation
|
21
format/poi.js
Normal file
21
format/poi.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const formatCoord = require('./coord')
|
||||||
|
|
||||||
|
const formatPoi = (p) => {
|
||||||
|
if (p.type !== 'location' || !p.latitude || !p.longitude || !p.id || !p.name) {
|
||||||
|
throw new Error('invalid POI')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'P',
|
||||||
|
name: p.name,
|
||||||
|
lid: 'L=' + p.id,
|
||||||
|
crd: {
|
||||||
|
x: formatCoord(p.longitude),
|
||||||
|
y: formatCoord(p.latitude)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = formatPoi
|
14
format/products-bitmask.js
Normal file
14
format/products-bitmask.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const createFormatBitmask = (modes) => {
|
||||||
|
const formatBitmask = (products) => {
|
||||||
|
let bitmask = 0
|
||||||
|
for (let product in products) {
|
||||||
|
if (products[product] === true) bitmask += modes[product].bitmask
|
||||||
|
}
|
||||||
|
return bitmask
|
||||||
|
}
|
||||||
|
return formatBitmask
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = createFormatBitmask
|
16
format/rectangle.js
Normal file
16
format/rectangle.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const formatRectangle = (profile, north, west, south, east) => {
|
||||||
|
return {
|
||||||
|
llCrd: {
|
||||||
|
x: profile.formatCoord(west),
|
||||||
|
y: profile.formatCoord(south)
|
||||||
|
},
|
||||||
|
urCrd: {
|
||||||
|
x: profile.formatCoord(east),
|
||||||
|
y: profile.formatCoord(north)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = formatRectangle
|
5
format/station.js
Normal file
5
format/station.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const formatStation = id => ({type: 'S', lid: 'L=' + id})
|
||||||
|
|
||||||
|
module.exports = formatStation
|
12
format/time.js
Normal file
12
format/time.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const {DateTime} = require('luxon')
|
||||||
|
|
||||||
|
const formatTime = (profile, when) => {
|
||||||
|
return DateTime.fromMillis(+when, {
|
||||||
|
locale: profile.locale,
|
||||||
|
zone: profile.timezone
|
||||||
|
}).toFormat('HHmmss')
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = formatTime
|
304
index.js
304
index.js
|
@ -1,74 +1,258 @@
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const Promise = require('pinkie-promise')
|
const minBy = require('lodash/minBy')
|
||||||
const {fetch} = require('fetch-ponyfill')({Promise})
|
const maxBy = require('lodash/maxBy')
|
||||||
const {stringify} = require('query-string')
|
|
||||||
|
|
||||||
const parse = require('./parse')
|
const validateProfile = require('./lib/validate-profile')
|
||||||
|
const defaultProfile = require('./lib/default-profile')
|
||||||
|
const request = require('./lib/request')
|
||||||
|
|
||||||
|
const createClient = (profile) => {
|
||||||
|
profile = Object.assign({}, defaultProfile, profile)
|
||||||
|
validateProfile(profile)
|
||||||
|
|
||||||
|
const departures = (station, opt = {}) => {
|
||||||
|
if ('object' === typeof station) station = profile.formatStation(station.id)
|
||||||
|
else if ('string' === typeof station) station = profile.formatStation(station)
|
||||||
|
else throw new Error('station must be an object or a string.')
|
||||||
|
|
||||||
const id = (x) => x
|
opt = Object.assign({
|
||||||
const defaults = {
|
direction: null, // only show departures heading to this station
|
||||||
onBody: id,
|
duration: 10 // show departures for the next n minutes
|
||||||
onReq: id,
|
}, opt)
|
||||||
onLocation: parse.location,
|
opt.when = opt.when || new Date()
|
||||||
onLine: parse.line,
|
const products = profile.formatProducts(opt.products || {})
|
||||||
onRemark: parse.remark,
|
|
||||||
onOperator: parse.operator
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const dir = opt.direction ? profile.formatStation(opt.direction) : null
|
||||||
|
return request(profile, {
|
||||||
const hafasError = (err) => {
|
meth: 'StationBoard',
|
||||||
err.isHafasError = true
|
req: {
|
||||||
return err
|
type: 'DEP',
|
||||||
}
|
date: profile.formatDate(profile, opt.when),
|
||||||
|
time: profile.formatTime(profile, opt.when),
|
||||||
const createRequest = (opt) => {
|
stbLoc: station,
|
||||||
opt = Object.assign({}, defaults, opt)
|
dirLoc: dir,
|
||||||
|
jnyFltrL: [products],
|
||||||
const request = (data) => {
|
dur: opt.duration,
|
||||||
const body = opt.onBody({lang: 'en', svcReqL: [data]})
|
getPasslist: false
|
||||||
const req = opt.onReq({
|
|
||||||
method: 'post',
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept-Encoding': 'gzip, deflate',
|
|
||||||
'user-agent': 'https://github.com/derhuerst/hafas-client'
|
|
||||||
},
|
|
||||||
query: null
|
|
||||||
})
|
|
||||||
const url = opt.endpoint + (req.query ? '?' + stringify(req.query) : '')
|
|
||||||
|
|
||||||
return fetch(url, req)
|
|
||||||
.then((res) => {
|
|
||||||
if (!res.ok) {
|
|
||||||
const err = new Error(res.statusText)
|
|
||||||
err.statusCode = res.status
|
|
||||||
throw hafasError(err)
|
|
||||||
}
|
}
|
||||||
return res.json()
|
|
||||||
})
|
})
|
||||||
.then((b) => {
|
.then((d) => {
|
||||||
if (b.err) throw hafasError(new Error(b.err))
|
if (!Array.isArray(d.jnyL)) return [] // todo: throw err?
|
||||||
if (!b.svcResL || !b.svcResL[0]) throw new Error('invalid response')
|
const parse = profile.parseDeparture(profile, d.locations, d.lines, d.remarks)
|
||||||
if (b.svcResL[0].err !== 'OK') {
|
return d.jnyL.map(parse)
|
||||||
throw hafasError(new Error(b.svcResL[0].errTxt))
|
.sort((a, b) => new Date(a.when) - new Date(b.when))
|
||||||
}
|
|
||||||
const d = b.svcResL[0].res
|
|
||||||
const c = d.common || {}
|
|
||||||
|
|
||||||
if (Array.isArray(c.locL)) d.locations = c.locL.map(opt.onLocation)
|
|
||||||
if (Array.isArray(c.prodL)) d.lines = c.prodL.map(opt.onLine)
|
|
||||||
if (Array.isArray(c.remL)) d.remarks = c.remL.map(opt.onRemark)
|
|
||||||
if (Array.isArray(c.opL)) d.operators = c.opL.map(opt.onOperator)
|
|
||||||
return d
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return request
|
const journeys = (from, to, opt = {}) => {
|
||||||
|
from = profile.formatLocation(profile, from)
|
||||||
|
to = profile.formatLocation(profile, to)
|
||||||
|
|
||||||
|
opt = Object.assign({
|
||||||
|
results: 5, // how many journeys?
|
||||||
|
via: null, // let journeys pass this station?
|
||||||
|
passedStations: false, // return stations on the way?
|
||||||
|
transfers: 5, // maximum of 5 transfers
|
||||||
|
transferTime: 0, // minimum time for a single transfer in minutes
|
||||||
|
// todo: does this work with every endpoint?
|
||||||
|
accessibility: 'none', // 'none', 'partial' or 'complete'
|
||||||
|
bike: false, // only bike-friendly journeys
|
||||||
|
tickets: false, // return tickets?
|
||||||
|
}, opt)
|
||||||
|
if (opt.via) opt.via = profile.formatLocation(profile, opt.via)
|
||||||
|
opt.when = opt.when || new Date()
|
||||||
|
|
||||||
|
const filters = [
|
||||||
|
profile.formatProducts(opt.products || {})
|
||||||
|
]
|
||||||
|
if (
|
||||||
|
opt.accessibility &&
|
||||||
|
profile.filters &&
|
||||||
|
profile.filters.accessibility &&
|
||||||
|
profile.filters.accessibility[opt.accessibility]
|
||||||
|
) {
|
||||||
|
filters.push(profile.filters.accessibility[opt.accessibility])
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = profile.transformJourneysQuery({
|
||||||
|
outDate: profile.formatDate(profile, opt.when),
|
||||||
|
outTime: profile.formatTime(profile, opt.when),
|
||||||
|
numF: opt.results,
|
||||||
|
getPasslist: !!opt.passedStations,
|
||||||
|
maxChg: opt.transfers,
|
||||||
|
minChgTime: opt.transferTime,
|
||||||
|
depLocL: [from],
|
||||||
|
viaLocL: opt.via ? [opt.via] : null,
|
||||||
|
arrLocL: [to],
|
||||||
|
jnyFltrL: filters,
|
||||||
|
getTariff: !!opt.tickets,
|
||||||
|
|
||||||
|
// todo: what is req.gisFltrL?
|
||||||
|
getPT: true, // todo: what is this?
|
||||||
|
outFrwd: true, // todo: what is this?
|
||||||
|
getIV: false, // todo: walk & bike as alternatives?
|
||||||
|
getPolyline: false // todo: shape for displaying on a map?
|
||||||
|
}, opt)
|
||||||
|
|
||||||
|
return request(profile, {
|
||||||
|
cfg: {polyEnc: 'GPA'},
|
||||||
|
meth: 'TripSearch',
|
||||||
|
req: query
|
||||||
|
})
|
||||||
|
.then((d) => {
|
||||||
|
if (!Array.isArray(d.outConL)) return []
|
||||||
|
const parse = profile.parseJourney(profile, d.locations, d.lines, d.remarks)
|
||||||
|
return d.outConL.map(parse)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const locations = (query, opt = {}) => {
|
||||||
|
if ('string' !== typeof query) throw new Error('query must be a string.')
|
||||||
|
opt = Object.assign({
|
||||||
|
fuzzy: true, // find only exact matches?
|
||||||
|
results: 10, // how many search results?
|
||||||
|
stations: true,
|
||||||
|
addresses: true,
|
||||||
|
poi: true // points of interest
|
||||||
|
}, opt)
|
||||||
|
|
||||||
|
const f = profile.formatLocationFilter(opt.stations, opt.addresses, opt.poi)
|
||||||
|
return request(profile, {
|
||||||
|
cfg: {polyEnc: 'GPA'},
|
||||||
|
meth: 'LocMatch',
|
||||||
|
req: {input: {
|
||||||
|
loc: {
|
||||||
|
type: f,
|
||||||
|
name: opt.fuzzy ? query + '?' : query
|
||||||
|
},
|
||||||
|
maxLoc: opt.results,
|
||||||
|
field: 'S' // todo: what is this?
|
||||||
|
}}
|
||||||
|
})
|
||||||
|
.then((d) => {
|
||||||
|
if (!d.match || !Array.isArray(d.match.locL)) return []
|
||||||
|
const parse = profile.parseLocation
|
||||||
|
return d.match.locL.map(loc => parse(profile, loc))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const nearby = (location, opt = {}) => {
|
||||||
|
if ('object' !== typeof location || Array.isArray(location)) {
|
||||||
|
throw new Error('location must be an object.')
|
||||||
|
} else if (location.type !== 'location') {
|
||||||
|
throw new Error('invalid location object.')
|
||||||
|
} else if ('number' !== typeof location.latitude) {
|
||||||
|
throw new Error('location.latitude must be a number.')
|
||||||
|
} else if ('number' !== typeof location.longitude) {
|
||||||
|
throw new Error('location.longitude must be a number.')
|
||||||
|
}
|
||||||
|
|
||||||
|
opt = Object.assign({
|
||||||
|
results: 8, // maximum number of results
|
||||||
|
distance: null, // maximum walking distance in meters
|
||||||
|
poi: false, // return points of interest?
|
||||||
|
stations: true, // return stations?
|
||||||
|
}, opt)
|
||||||
|
|
||||||
|
return request(profile, {
|
||||||
|
cfg: {polyEnc: 'GPA'},
|
||||||
|
meth: 'LocGeoPos',
|
||||||
|
req: {
|
||||||
|
ring: {
|
||||||
|
cCrd: {
|
||||||
|
x: profile.formatCoord(location.longitude),
|
||||||
|
y: profile.formatCoord(location.latitude)
|
||||||
|
},
|
||||||
|
maxDist: opt.distance || -1,
|
||||||
|
minDist: 0
|
||||||
|
},
|
||||||
|
getPOIs: !!opt.poi,
|
||||||
|
getStops: !!opt.stations,
|
||||||
|
maxLoc: opt.results
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((d) => {
|
||||||
|
if (!Array.isArray(d.locL)) return []
|
||||||
|
const parse = profile.parseNearby
|
||||||
|
return d.locL.map(loc => parse(profile, loc))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const journeyLeg = (ref, lineName, opt = {}) => {
|
||||||
|
opt = Object.assign({
|
||||||
|
passedStations: true // return stations on the way?
|
||||||
|
}, opt)
|
||||||
|
opt.when = opt.when || new Date()
|
||||||
|
|
||||||
|
return request(profile, {
|
||||||
|
cfg: {polyEnc: 'GPA'},
|
||||||
|
meth: 'JourneyDetails',
|
||||||
|
req: {
|
||||||
|
jid: ref,
|
||||||
|
name: lineName,
|
||||||
|
date: profile.formatDate(profile, opt.when)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((d) => {
|
||||||
|
const parse = profile.parseJourneyLeg(profile, d.locations, d.lines, d.remarks)
|
||||||
|
|
||||||
|
const leg = { // pretend the leg is contained in a journey
|
||||||
|
type: 'JNY',
|
||||||
|
dep: minBy(d.journey.stopL, 'idx'),
|
||||||
|
arr: maxBy(d.journey.stopL, 'idx'),
|
||||||
|
jny: d.journey
|
||||||
|
}
|
||||||
|
return parse(d.journey, leg, !!opt.passedStations)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const radar = (north, west, south, east, opt) => {
|
||||||
|
if ('number' !== typeof north) throw new Error('north must be a number.')
|
||||||
|
if ('number' !== typeof west) throw new Error('west must be a number.')
|
||||||
|
if ('number' !== typeof south) throw new Error('south must be a number.')
|
||||||
|
if ('number' !== typeof east) throw new Error('east must be a number.')
|
||||||
|
|
||||||
|
opt = Object.assign({
|
||||||
|
results: 256, // maximum number of vehicles
|
||||||
|
duration: 30, // compute frames for the next n seconds
|
||||||
|
frames: 3, // nr of frames to compute
|
||||||
|
products: null // optionally an object of booleans
|
||||||
|
}, opt || {})
|
||||||
|
opt.when = opt.when || new Date()
|
||||||
|
|
||||||
|
const durationPerStep = opt.duration / Math.max(opt.frames, 1) * 1000
|
||||||
|
return request(profile, {
|
||||||
|
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(durationPerStep),
|
||||||
|
ageOfReport: true, // todo: what is this?
|
||||||
|
jnyFltrL: [
|
||||||
|
profile.formatProducts(opt.products || {})
|
||||||
|
],
|
||||||
|
trainPosMode: 'CALC' // todo: what is this? what about realtime?
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((d) => {
|
||||||
|
if (!Array.isArray(d.jnyL)) return []
|
||||||
|
|
||||||
|
const parse = profile.parseMovement(profile, d.locations, d.lines, d.remarks)
|
||||||
|
return d.jnyL.map(parse)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = {departures, journeys, locations, nearby}
|
||||||
|
if (profile.journeyLeg) client.journeyLeg = journeyLeg
|
||||||
|
if (profile.radar) client.radar = radar
|
||||||
|
Object.defineProperty(client, 'profile', {value: profile})
|
||||||
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = createRequest
|
module.exports = createClient
|
||||||
|
|
62
lib/default-profile.js
Normal file
62
lib/default-profile.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const parseDateTime = require('../parse/date-time')
|
||||||
|
const parseDeparture = require('../parse/departure')
|
||||||
|
const parseJourneyLeg = require('../parse/journey-leg')
|
||||||
|
const parseJourney = require('../parse/journey')
|
||||||
|
const parseLine = require('../parse/line')
|
||||||
|
const parseLocation = require('../parse/location')
|
||||||
|
const parseMovement = require('../parse/movement')
|
||||||
|
const parseNearby = require('../parse/nearby')
|
||||||
|
const parseOperator = require('../parse/operator')
|
||||||
|
const parseRemark = require('../parse/remark')
|
||||||
|
const parseStopover = require('../parse/stopover')
|
||||||
|
|
||||||
|
const formatAddress = require('../format/address')
|
||||||
|
const formatCoord = require('../format/coord')
|
||||||
|
const formatDate = require('../format/date')
|
||||||
|
const formatLocationFilter = require('../format/location-filter')
|
||||||
|
const formatPoi = require('../format/poi')
|
||||||
|
const formatStation = require('../format/station')
|
||||||
|
const formatTime = require('../format/time')
|
||||||
|
const formatLocation = require('../format/location')
|
||||||
|
const formatRectangle = require('../format/rectangle')
|
||||||
|
const filters = require('../format/filters')
|
||||||
|
|
||||||
|
const id = x => x
|
||||||
|
|
||||||
|
const defaultProfile = {
|
||||||
|
transformReqBody: id,
|
||||||
|
transformReq: id,
|
||||||
|
|
||||||
|
transformJourneysQuery: id,
|
||||||
|
|
||||||
|
parseDateTime,
|
||||||
|
parseDeparture,
|
||||||
|
parseJourneyLeg,
|
||||||
|
parseJourney,
|
||||||
|
parseLine,
|
||||||
|
parseStationName: id,
|
||||||
|
parseLocation,
|
||||||
|
parseMovement,
|
||||||
|
parseNearby,
|
||||||
|
parseOperator,
|
||||||
|
parseRemark,
|
||||||
|
parseStopover,
|
||||||
|
|
||||||
|
formatAddress,
|
||||||
|
formatCoord,
|
||||||
|
formatDate,
|
||||||
|
formatLocationFilter,
|
||||||
|
formatPoi,
|
||||||
|
formatStation,
|
||||||
|
formatTime,
|
||||||
|
formatLocation,
|
||||||
|
formatRectangle,
|
||||||
|
filters,
|
||||||
|
|
||||||
|
journeyLeg: false,
|
||||||
|
radar: false
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = defaultProfile
|
62
lib/request.js
Normal file
62
lib/request.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const Promise = require('pinkie-promise')
|
||||||
|
const {fetch} = require('fetch-ponyfill')({Promise})
|
||||||
|
const {stringify} = require('query-string')
|
||||||
|
|
||||||
|
const hafasError = (err) => {
|
||||||
|
err.isHafasError = true
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = (profile, data) => {
|
||||||
|
const body = profile.transformReqBody({lang: 'en', svcReqL: [data]})
|
||||||
|
const req = profile.transformReq({
|
||||||
|
method: 'post',
|
||||||
|
// todo: CORS? referrer policy?
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept-Encoding': 'gzip, deflate',
|
||||||
|
'user-agent': 'https://github.com/derhuerst/hafas-client'
|
||||||
|
},
|
||||||
|
query: null
|
||||||
|
})
|
||||||
|
const url = profile.endpoint + (req.query ? '?' + stringify(req.query) : '')
|
||||||
|
|
||||||
|
return fetch(url, req)
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = new Error(res.statusText)
|
||||||
|
err.statusCode = res.status
|
||||||
|
throw hafasError(err)
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
})
|
||||||
|
.then((b) => {
|
||||||
|
if (b.err) throw hafasError(new Error(b.err))
|
||||||
|
if (!b.svcResL || !b.svcResL[0]) throw new Error('invalid response')
|
||||||
|
if (b.svcResL[0].err !== 'OK') {
|
||||||
|
throw hafasError(new Error(b.svcResL[0].errTxt))
|
||||||
|
}
|
||||||
|
const d = b.svcResL[0].res
|
||||||
|
const c = d.common || {}
|
||||||
|
|
||||||
|
if (Array.isArray(c.locL)) {
|
||||||
|
d.locations = c.locL.map(loc => profile.parseLocation(profile, loc))
|
||||||
|
}
|
||||||
|
if (Array.isArray(c.remL)) {
|
||||||
|
d.remarks = c.remL.map(rem => profile.parseRemark(profile, rem))
|
||||||
|
}
|
||||||
|
if (Array.isArray(c.opL)) {
|
||||||
|
d.operators = c.opL.map(op => profile.parseOperator(profile, op))
|
||||||
|
}
|
||||||
|
if (Array.isArray(c.prodL)) {
|
||||||
|
const parse = profile.parseLine(profile, d.operators)
|
||||||
|
d.lines = c.prodL.map(parse)
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = request
|
48
lib/validate-profile.js
Normal file
48
lib/validate-profile.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const types = {
|
||||||
|
locale: 'string',
|
||||||
|
timezone: 'string',
|
||||||
|
transformReq: 'function',
|
||||||
|
transformReqBody: 'function',
|
||||||
|
transformJourneysQuery: 'function',
|
||||||
|
|
||||||
|
products: 'object',
|
||||||
|
|
||||||
|
parseDateTime: 'function',
|
||||||
|
parseDeparture: 'function',
|
||||||
|
parseJourneyLeg: 'function',
|
||||||
|
parseJourney: 'function',
|
||||||
|
parseLine: 'function',
|
||||||
|
parseStationName: 'function',
|
||||||
|
parseLocation: 'function',
|
||||||
|
parseMovement: 'function',
|
||||||
|
parseNearby: 'function',
|
||||||
|
parseOperator: 'function',
|
||||||
|
parseRemark: 'function',
|
||||||
|
parseStopover: 'function',
|
||||||
|
|
||||||
|
formatAddress: 'function',
|
||||||
|
formatCoord: 'function',
|
||||||
|
formatDate: 'function',
|
||||||
|
formatLocationFilter: 'function',
|
||||||
|
formatPoi: 'function',
|
||||||
|
formatStation: 'function',
|
||||||
|
formatTime: 'function',
|
||||||
|
formatLocation: 'function',
|
||||||
|
formatRectangle: 'function'
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateProfile = (profile) => {
|
||||||
|
for (let key of Object.keys(types)) {
|
||||||
|
const type = types[key]
|
||||||
|
if (type !== typeof profile[key]) {
|
||||||
|
throw new Error(`profile.${key} must be a ${type}.`)
|
||||||
|
}
|
||||||
|
if (type === 'object' && profile[key] === null) {
|
||||||
|
throw new Error(`profile.${key} must not be null.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = validateProfile
|
17
p/db/example.js
Normal file
17
p/db/example.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const createClient = require('../../')
|
||||||
|
const dbProfile = require('.')
|
||||||
|
|
||||||
|
const client = createClient(dbProfile)
|
||||||
|
|
||||||
|
// Berlin Jungfernheide to München Hbf
|
||||||
|
client.journeys('8011167', '8000261', {results: 1, tickets: true})
|
||||||
|
// client.departures('8011167', {duration: 1})
|
||||||
|
// client.locations('Berlin Jungfernheide')
|
||||||
|
// client.locations('Atze Musiktheater', {poi: true, addressses: false, fuzzy: false})
|
||||||
|
// client.nearby(52.4751309, 13.3656537, {results: 1})
|
||||||
|
|
||||||
|
.then((data) => {
|
||||||
|
console.log(require('util').inspect(data, {depth: null}))
|
||||||
|
}, console.error)
|
160
p/db/index.js
Normal file
160
p/db/index.js
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const crypto = require('crypto')
|
||||||
|
|
||||||
|
const _createParseLine = require('../../parse/line')
|
||||||
|
const _createParseJourney = require('../../parse/journey')
|
||||||
|
const _formatStation = require('../../format/station')
|
||||||
|
const createParseBitmask = require('../../parse/products-bitmask')
|
||||||
|
const createFormatBitmask = require('../../format/products-bitmask')
|
||||||
|
const {bike} = require('../../format/filters')
|
||||||
|
|
||||||
|
const modes = require('./modes')
|
||||||
|
const formatLoyaltyCard = require('./loyalty-cards').format
|
||||||
|
|
||||||
|
const formatBitmask = createFormatBitmask(modes)
|
||||||
|
|
||||||
|
const transformReqBody = (body) => {
|
||||||
|
body.client = {id: 'DB', v: '16040000', type: 'IPH', name: 'DB Navigator'}
|
||||||
|
body.ext = 'DB.R15.12.a'
|
||||||
|
body.ver = '1.15'
|
||||||
|
body.auth = {type: 'AID', aid: 'n91dB8Z77MLdoR0K'}
|
||||||
|
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
const salt = 'bdI8UVj40K5fvxwf'
|
||||||
|
const transformReq = (req) => {
|
||||||
|
const hash = crypto.createHash('md5')
|
||||||
|
hash.update(req.body + salt)
|
||||||
|
|
||||||
|
if (!req.query) req.query = {}
|
||||||
|
req.query.checksum = hash.digest('hex')
|
||||||
|
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformJourneysQuery = (query, opt) => {
|
||||||
|
const filters = query.jnyFltrL
|
||||||
|
if (opt.bike) filters.push(bike)
|
||||||
|
|
||||||
|
query.trfReq = {
|
||||||
|
jnyCl: opt.firstClass === true ? 1 : 2,
|
||||||
|
tvlrProf: [{
|
||||||
|
type: 'E',
|
||||||
|
redtnCard: opt.loyaltyCard
|
||||||
|
? formatLoyaltyCard(opt.loyaltyCard)
|
||||||
|
: null
|
||||||
|
}],
|
||||||
|
cType: 'PK'
|
||||||
|
}
|
||||||
|
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
const createParseLine = (profile, operators) => {
|
||||||
|
const parseLine = _createParseLine(profile, operators)
|
||||||
|
|
||||||
|
const parseLineWithMode = (l) => {
|
||||||
|
const res = parseLine(l)
|
||||||
|
|
||||||
|
res.mode = res.product = null
|
||||||
|
if ('class' in res) {
|
||||||
|
const data = modes.bitmasks[parseInt(res.class)]
|
||||||
|
if (data) {
|
||||||
|
res.mode = data.mode
|
||||||
|
res.product = data.product
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
return parseLineWithMode
|
||||||
|
}
|
||||||
|
|
||||||
|
const createParseJourney = (profile, stations, lines, remarks) => {
|
||||||
|
const parseJourney = _createParseJourney(profile, stations, lines, remarks)
|
||||||
|
|
||||||
|
// todo: j.sotRating, j.conSubscr, j.isSotCon, j.showARSLink, k.sotCtxt
|
||||||
|
// todo: j.conSubscr, j.showARSLink, j.useableTime
|
||||||
|
const parseJourneyWithPrice = (j) => {
|
||||||
|
const res = parseJourney(j)
|
||||||
|
|
||||||
|
// todo: find cheapest, find discounts
|
||||||
|
// todo: write a parser like vbb-parse-ticket
|
||||||
|
// [ {
|
||||||
|
// prc: 15000,
|
||||||
|
// isFromPrice: true,
|
||||||
|
// isBookable: true,
|
||||||
|
// isUpsell: false,
|
||||||
|
// targetCtx: 'D',
|
||||||
|
// buttonText: 'To offer selection'
|
||||||
|
// } ]
|
||||||
|
res.price = {amount: null, hint: 'No pricing information available.'}
|
||||||
|
if (
|
||||||
|
j.trfRes &&
|
||||||
|
Array.isArray(j.trfRes.fareSetL) &&
|
||||||
|
j.trfRes.fareSetL[0] &&
|
||||||
|
Array.isArray(j.trfRes.fareSetL[0].fareL) &&
|
||||||
|
j.trfRes.fareSetL[0].fareL[0]
|
||||||
|
) {
|
||||||
|
const tariff = j.trfRes.fareSetL[0].fareL[0]
|
||||||
|
if (tariff.prc >= 0) { // wat
|
||||||
|
res.price = {amount: tariff.prc / 100, hint: null}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseJourneyWithPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
const isIBNR = /^\d{6,}$/
|
||||||
|
const formatStation = (id) => {
|
||||||
|
if (!isIBNR.test(id)) throw new Error('station ID must be an IBNR.')
|
||||||
|
return _formatStation(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultProducts = {
|
||||||
|
suburban: true,
|
||||||
|
subway: true,
|
||||||
|
tram: true,
|
||||||
|
bus: true,
|
||||||
|
ferry: true,
|
||||||
|
national: true,
|
||||||
|
nationalExp: true,
|
||||||
|
regional: true,
|
||||||
|
regionalExp: true
|
||||||
|
}
|
||||||
|
const formatProducts = (products) => {
|
||||||
|
products = Object.assign(Object.create(null), defaultProducts, products)
|
||||||
|
return {
|
||||||
|
type: 'PROD',
|
||||||
|
mode: 'INC',
|
||||||
|
value: formatBitmask(products) + ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: find option for absolute number of results
|
||||||
|
|
||||||
|
const dbProfile = {
|
||||||
|
locale: 'de-DE',
|
||||||
|
timezone: 'Europe/Berlin',
|
||||||
|
endpoint: 'https://reiseauskunft.bahn.de/bin/mgate.exe',
|
||||||
|
transformReqBody,
|
||||||
|
transformReq,
|
||||||
|
transformJourneysQuery,
|
||||||
|
|
||||||
|
products: modes.allProducts,
|
||||||
|
|
||||||
|
// todo: parseLocation
|
||||||
|
parseLine: createParseLine,
|
||||||
|
parseProducts: createParseBitmask(modes.bitmasks),
|
||||||
|
parseJourney: createParseJourney,
|
||||||
|
|
||||||
|
formatStation,
|
||||||
|
formatProducts
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = dbProfile
|
30
p/db/loyalty-cards.js
Normal file
30
p/db/loyalty-cards.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const c = {
|
||||||
|
NONE: Symbol('no loyaly card'),
|
||||||
|
BAHNCARD: Symbol('Bahncard'),
|
||||||
|
VORTEILSCARD: Symbol('VorteilsCard'),
|
||||||
|
HALBTAXABO: Symbol('HalbtaxAbo'),
|
||||||
|
VOORDEELURENABO: Symbol('Voordeelurenabo'),
|
||||||
|
SHCARD: Symbol('SH-Card'),
|
||||||
|
GENERALABONNEMENT: Symbol('General-Abonnement')
|
||||||
|
}
|
||||||
|
|
||||||
|
// see https://gist.github.com/juliuste/202bb04f450a79f8fa12a2ec3abcd72d
|
||||||
|
const formatLoyaltyCard = (data) => {
|
||||||
|
if (data.type === c.BAHNCARD) {
|
||||||
|
if (data.discount === 25) return c.class === 1 ? 1 : 2
|
||||||
|
if (data.discount === 50) return c.class === 1 ? 3 : 4
|
||||||
|
}
|
||||||
|
if (data.type === c.VORTEILSCARD) return 9
|
||||||
|
if (data.type === c.HALBTAXABO) return data.railplus ? 10 : 11
|
||||||
|
if (data.type === c.VOORDEELURENABO) return data.railplus ? 12 : 13
|
||||||
|
if (data.type === c.SHCARD) return 14
|
||||||
|
if (data.type === c.GENERALABONNEMENT) return 15
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: c,
|
||||||
|
format: formatLoyaltyCard
|
||||||
|
}
|
108
p/db/modes.js
Normal file
108
p/db/modes.js
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
// todo: https://gist.github.com/anonymous/d3323a5d2d6e159ed42b12afd0380434#file-haf_products-properties-L1-L95
|
||||||
|
const m = {
|
||||||
|
nationalExp: {
|
||||||
|
bitmask: 1,
|
||||||
|
name: 'InterCityExpress',
|
||||||
|
short: 'ICE',
|
||||||
|
mode: 'train',
|
||||||
|
product: 'nationalExp'
|
||||||
|
},
|
||||||
|
national: {
|
||||||
|
bitmask: 2,
|
||||||
|
name: 'InterCity & EuroCity',
|
||||||
|
short: 'IC/EC',
|
||||||
|
mode: 'train',
|
||||||
|
product: 'national'
|
||||||
|
},
|
||||||
|
regionalExp: {
|
||||||
|
bitmask: 4,
|
||||||
|
name: 'RegionalExpress & InterRegio',
|
||||||
|
short: 'RE/IR',
|
||||||
|
mode: 'train',
|
||||||
|
product: 'regionalExp'
|
||||||
|
},
|
||||||
|
regional: {
|
||||||
|
bitmask: 8,
|
||||||
|
name: 'Regio',
|
||||||
|
short: 'RB',
|
||||||
|
mode: 'train',
|
||||||
|
product: 'regional'
|
||||||
|
},
|
||||||
|
suburban: {
|
||||||
|
bitmask: 16,
|
||||||
|
name: 'S-Bahn',
|
||||||
|
short: 'S',
|
||||||
|
mode: 'train',
|
||||||
|
product: 'suburban'
|
||||||
|
},
|
||||||
|
bus: {
|
||||||
|
bitmask: 32,
|
||||||
|
name: 'Bus',
|
||||||
|
short: 'B',
|
||||||
|
mode: 'bus',
|
||||||
|
product: 'bus'
|
||||||
|
},
|
||||||
|
ferry: {
|
||||||
|
bitmask: 64,
|
||||||
|
name: 'Ferry',
|
||||||
|
short: 'F',
|
||||||
|
mode: 'watercraft',
|
||||||
|
product: 'ferry'
|
||||||
|
},
|
||||||
|
subway: {
|
||||||
|
bitmask: 128,
|
||||||
|
name: 'U-Bahn',
|
||||||
|
short: 'U',
|
||||||
|
mode: 'train',
|
||||||
|
product: 'subway'
|
||||||
|
},
|
||||||
|
tram: {
|
||||||
|
bitmask: 256,
|
||||||
|
name: 'Tram',
|
||||||
|
short: 'T',
|
||||||
|
mode: 'tram',
|
||||||
|
product: 'tram'
|
||||||
|
},
|
||||||
|
taxi: {
|
||||||
|
bitmask: 512,
|
||||||
|
name: 'Group Taxi',
|
||||||
|
short: 'Taxi',
|
||||||
|
mode: null, // todo
|
||||||
|
product: 'taxi'
|
||||||
|
},
|
||||||
|
unknown: {
|
||||||
|
bitmask: 0,
|
||||||
|
name: 'unknown',
|
||||||
|
short: '?',
|
||||||
|
product: 'unknown'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.bitmasks = []
|
||||||
|
m.bitmasks[1] = m.nationalExp
|
||||||
|
m.bitmasks[2] = m.national
|
||||||
|
m.bitmasks[4] = m.regionalExp
|
||||||
|
m.bitmasks[8] = m.regional
|
||||||
|
m.bitmasks[16] = m.suburban
|
||||||
|
m.bitmasks[32] = m.bus
|
||||||
|
m.bitmasks[64] = m.ferry
|
||||||
|
m.bitmasks[128] = m.subway
|
||||||
|
m.bitmasks[256] = m.tram
|
||||||
|
m.bitmasks[512] = m.taxi
|
||||||
|
|
||||||
|
m.allProducts = [
|
||||||
|
m.nationalExp,
|
||||||
|
m.national,
|
||||||
|
m.regionalExp,
|
||||||
|
m.regional,
|
||||||
|
m.suburban,
|
||||||
|
m.bus,
|
||||||
|
m.ferry,
|
||||||
|
m.subway,
|
||||||
|
m.tram,
|
||||||
|
m.taxi
|
||||||
|
]
|
||||||
|
|
||||||
|
module.exports = m
|
21
p/db/readme.md
Normal file
21
p/db/readme.md
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# DB profile for `hafas-client`
|
||||||
|
|
||||||
|
[*Deutsche Bahn (DB)*](https://en.wikipedia.org/wiki/Deutsche_Bahn) is the largest German long-distance public transport company. This profile adds *DB*-specific customizations to `hafas-client`. Consider using [`db-hafas`](https://github.com/derhuerst/db-hafas#db-hafas), to always get the customized client right away.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```js
|
||||||
|
const createClient = require('hafas-client')
|
||||||
|
const dbProfile = require('hafas-client/p/db')
|
||||||
|
|
||||||
|
// create a client with DB profile
|
||||||
|
const client = createClient(dbProfile)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Customisations
|
||||||
|
|
||||||
|
- supports 1st and 2nd class with `journey()`
|
||||||
|
- supports [their loyalty cards](https://en.wikipedia.org/wiki/Deutsche_Bahn#Tickets) with `journey()`
|
||||||
|
- parses *DB*-specific products (such as *InterCity-Express*)
|
||||||
|
- exposes the cheapest ticket price for a `journey`
|
15
p/readme.md
Normal file
15
p/readme.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# Profiles
|
||||||
|
|
||||||
|
This directory contains specific customisations for each endpoint, called *profiles*. They **parse data from the API differently, add additional information, or enable non-default methods** (such as [`journeyLeg`](../docs/journey-leg.md)) if they are supported.
|
||||||
|
|
||||||
|
Each profile has it's own directory. It will be passed into `hafas-client` and is expected to be in a certain structure:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const createClient = require('hafas-client')
|
||||||
|
const someProfile = require('hafas-client/p/some-profile')
|
||||||
|
|
||||||
|
// create a client with the profile
|
||||||
|
const client = createClient(dbProfile)
|
||||||
|
|
||||||
|
// use it to query data…
|
||||||
|
```
|
18
p/vbb/example.js
Normal file
18
p/vbb/example.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const createClient = require('../..')
|
||||||
|
const vbbProfile = require('.')
|
||||||
|
|
||||||
|
const client = createClient(vbbProfile)
|
||||||
|
|
||||||
|
// Hauptbahnhof to Charlottenburg
|
||||||
|
client.journeys('900000003201', '900000024101', {results: 1})
|
||||||
|
// client.departures('900000013102', {duration: 1})
|
||||||
|
// client.locations('Alexanderplatz', {results: 2})
|
||||||
|
// client.nearby(52.5137344, 13.4744798, {distance: 60})
|
||||||
|
// client.radar(52.52411, 13.41002, 52.51942, 13.41709, {results: 10})
|
||||||
|
|
||||||
|
.then((data) => {
|
||||||
|
console.log(require('util').inspect(data, {depth: null}))
|
||||||
|
})
|
||||||
|
.catch(console.error)
|
190
p/vbb/index.js
Normal file
190
p/vbb/index.js
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const shorten = require('vbb-short-station-name')
|
||||||
|
const {to12Digit, to9Digit} = require('vbb-translate-ids')
|
||||||
|
const parseLineName = require('vbb-parse-line')
|
||||||
|
const parseTicket = require('vbb-parse-ticket')
|
||||||
|
const getStations = require('vbb-stations')
|
||||||
|
|
||||||
|
const _createParseLine = require('../../parse/line')
|
||||||
|
const _parseLocation = require('../../parse/location')
|
||||||
|
const _createParseJourney = require('../../parse/journey')
|
||||||
|
const _createParseStopover = require('../../parse/stopover')
|
||||||
|
const _createParseDeparture = require('../../parse/departure')
|
||||||
|
const _formatStation = require('../../format/station')
|
||||||
|
const createParseBitmask = require('../../parse/products-bitmask')
|
||||||
|
const createFormatBitmask = require('../../format/products-bitmask')
|
||||||
|
|
||||||
|
const modes = require('./modes')
|
||||||
|
|
||||||
|
const formatBitmask = createFormatBitmask(modes)
|
||||||
|
|
||||||
|
const transformReqBody = (body) => {
|
||||||
|
body.client = {type: 'IPA', id: 'VBB', name: 'vbbPROD', v: '4010300'}
|
||||||
|
body.ext = 'VBB.1'
|
||||||
|
body.ver = '1.11' // todo: 1.16 with `mic` and `mac` query params
|
||||||
|
body.auth = {type: 'AID', aid: 'hafas-vbb-apps'}
|
||||||
|
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
const createParseLine = (profile, operators) => {
|
||||||
|
const parseLine = _createParseLine(profile, operators)
|
||||||
|
|
||||||
|
const parseLineWithMode = (l) => {
|
||||||
|
const res = parseLine(l)
|
||||||
|
|
||||||
|
res.mode = res.product = null
|
||||||
|
if ('class' in res) {
|
||||||
|
const data = modes.bitmasks[parseInt(res.class)]
|
||||||
|
if (data) {
|
||||||
|
res.mode = data.mode
|
||||||
|
res.product = data.product
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const details = parseLineName(l.name)
|
||||||
|
res.symbol = details.symbol
|
||||||
|
res.nr = details.nr
|
||||||
|
res.metro = details.metro
|
||||||
|
res.express = details.express
|
||||||
|
res.night = details.night
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
return parseLineWithMode
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseLocation = (profile, l) => {
|
||||||
|
const res = _parseLocation(profile, l)
|
||||||
|
|
||||||
|
if (res.type === 'station') {
|
||||||
|
res.name = shorten(res.name)
|
||||||
|
res.id = to12Digit(res.id)
|
||||||
|
if (!res.location.latitude || !res.location.longitude) {
|
||||||
|
const [s] = getStations(res.id)
|
||||||
|
if (s) Object.assign(res.location, s.coordinates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
const createParseJourney = (profile, stations, lines, remarks) => {
|
||||||
|
const parseJourney = _createParseJourney(profile, stations, lines, remarks)
|
||||||
|
|
||||||
|
const parseJourneyWithTickets = (j) => {
|
||||||
|
const res = parseJourney(j)
|
||||||
|
|
||||||
|
if (
|
||||||
|
j.trfRes &&
|
||||||
|
Array.isArray(j.trfRes.fareSetL) &&
|
||||||
|
j.trfRes.fareSetL[0] &&
|
||||||
|
Array.isArray(j.trfRes.fareSetL[0].fareL)
|
||||||
|
) {
|
||||||
|
res.tickets = []
|
||||||
|
const sets = j.trfRes.fareSetL[0].fareL
|
||||||
|
for (let s of sets) {
|
||||||
|
if (!Array.isArray(s.ticketL) || s.ticketL.length === 0) continue
|
||||||
|
for (let t of s.ticketL) {
|
||||||
|
const ticket = parseTicket(t)
|
||||||
|
ticket.name = s.name + ' – ' + ticket.name
|
||||||
|
res.tickets.push(ticket)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseJourneyWithTickets
|
||||||
|
}
|
||||||
|
|
||||||
|
const createParseStopover = (profile, stations, lines, remarks, connection) => {
|
||||||
|
const parseStopover = _createParseStopover(profile, stations, lines, remarks, connection)
|
||||||
|
|
||||||
|
const parseStopoverWithShorten = (st) => {
|
||||||
|
const res = parseStopover(st)
|
||||||
|
if (res.station && res.station.name) {
|
||||||
|
res.station.name = shorten(res.station.name)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseStopoverWithShorten
|
||||||
|
}
|
||||||
|
|
||||||
|
const createParseDeparture = (profile, stations, lines, remarks) => {
|
||||||
|
const parseDeparture = _createParseDeparture(profile, stations, lines, remarks)
|
||||||
|
|
||||||
|
const ringbahnClockwise = /^ringbahn s\s?41$/i
|
||||||
|
const ringbahnAnticlockwise = /^ringbahn s\s?42$/i
|
||||||
|
const parseDepartureRenameRingbahn = (j) => {
|
||||||
|
const res = parseDeparture(j)
|
||||||
|
|
||||||
|
if (res.line && res.line.product === 'suburban') {
|
||||||
|
const d = res.direction && res.direction.trim()
|
||||||
|
if (ringbahnClockwise.test(d)) res.direction = 'Ringbahn S41 ⟳'
|
||||||
|
else if (ringbahnAnticlockwise.test(d)) res.direction = 'Ringbahn S42 ⟲'
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseDepartureRenameRingbahn
|
||||||
|
}
|
||||||
|
|
||||||
|
const validIBNR = /^\d+$/
|
||||||
|
const formatStation = (id) => {
|
||||||
|
if ('string' !== typeof id) throw new Error('station ID must be a string.')
|
||||||
|
const l = id.length
|
||||||
|
if ((l !== 7 && l !== 9 && l !== 12) || !validIBNR.test(id)) {
|
||||||
|
throw new Error('station ID must be a valid IBNR.')
|
||||||
|
}
|
||||||
|
// The VBB has some 7-digit stations. We don't convert them to 12 digits,
|
||||||
|
// because it only recognizes in the 7-digit format. see derhuerst/vbb-hafas#22
|
||||||
|
if (l !== 7) id = to9Digit(id)
|
||||||
|
return _formatStation(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultProducts = {
|
||||||
|
suburban: true,
|
||||||
|
subway: true,
|
||||||
|
tram: true,
|
||||||
|
bus: true,
|
||||||
|
ferry: true,
|
||||||
|
express: true,
|
||||||
|
regional: true
|
||||||
|
}
|
||||||
|
const formatProducts = (products) => {
|
||||||
|
products = Object.assign(Object.create(null), defaultProducts, products)
|
||||||
|
return {
|
||||||
|
type: 'PROD',
|
||||||
|
mode: 'INC',
|
||||||
|
value: formatBitmask(products) + ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const vbbProfile = {
|
||||||
|
locale: 'de-DE',
|
||||||
|
timezone: 'Europe/Berlin',
|
||||||
|
endpoint: 'https://fahrinfo.vbb.de/bin/mgate.exe',
|
||||||
|
transformReqBody,
|
||||||
|
|
||||||
|
products: modes.allProducts,
|
||||||
|
|
||||||
|
parseStationName: shorten,
|
||||||
|
parseLocation,
|
||||||
|
parseLine: createParseLine,
|
||||||
|
parseProducts: createParseBitmask(modes.bitmasks),
|
||||||
|
parseJourney: createParseJourney,
|
||||||
|
parseDeparture: createParseDeparture,
|
||||||
|
parseStopover: createParseStopover,
|
||||||
|
|
||||||
|
formatStation,
|
||||||
|
formatProducts,
|
||||||
|
|
||||||
|
journeyLeg: true,
|
||||||
|
radar: true
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = vbbProfile
|
112
p/vbb/modes.js
Normal file
112
p/vbb/modes.js
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
// todo: remove useless keys
|
||||||
|
const m = {
|
||||||
|
suburban: {
|
||||||
|
category: 0,
|
||||||
|
bitmask: 1,
|
||||||
|
name: 'S-Bahn',
|
||||||
|
mode: 'train',
|
||||||
|
short: 'S',
|
||||||
|
product: 'suburban'
|
||||||
|
},
|
||||||
|
|
||||||
|
subway: {
|
||||||
|
category: 1,
|
||||||
|
bitmask: 2,
|
||||||
|
name: 'U-Bahn',
|
||||||
|
mode: 'train',
|
||||||
|
short: 'U',
|
||||||
|
product: 'subway'
|
||||||
|
},
|
||||||
|
|
||||||
|
tram: {
|
||||||
|
category: 2,
|
||||||
|
bitmask: 4,
|
||||||
|
name: 'Tram',
|
||||||
|
mode: 'train',
|
||||||
|
short: 'T',
|
||||||
|
product: 'tram'
|
||||||
|
},
|
||||||
|
|
||||||
|
bus: {
|
||||||
|
category: 3,
|
||||||
|
bitmask: 8,
|
||||||
|
name: 'Bus',
|
||||||
|
mode: 'bus',
|
||||||
|
short: 'B',
|
||||||
|
product: 'bus'
|
||||||
|
},
|
||||||
|
|
||||||
|
ferry: {
|
||||||
|
category: 4,
|
||||||
|
bitmask: 16,
|
||||||
|
name: 'Fähre',
|
||||||
|
mode: 'watercraft',
|
||||||
|
short: 'F',
|
||||||
|
product: 'ferry'
|
||||||
|
},
|
||||||
|
|
||||||
|
express: {
|
||||||
|
category: 5,
|
||||||
|
bitmask: 32,
|
||||||
|
name: 'IC/ICE',
|
||||||
|
mode: 'train',
|
||||||
|
short: 'E',
|
||||||
|
product: 'express'
|
||||||
|
},
|
||||||
|
|
||||||
|
regional: {
|
||||||
|
category: 6,
|
||||||
|
bitmask: 64,
|
||||||
|
name: 'RB/RE',
|
||||||
|
mode: 'train',
|
||||||
|
short: 'R',
|
||||||
|
product: 'regional'
|
||||||
|
},
|
||||||
|
|
||||||
|
unknown: {
|
||||||
|
category: null,
|
||||||
|
bitmask: 0,
|
||||||
|
name: 'unknown',
|
||||||
|
mode: null,
|
||||||
|
short: '?',
|
||||||
|
product: 'unknown'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.bitmasks = []
|
||||||
|
m.bitmasks[1] = m.suburban
|
||||||
|
m.bitmasks[2] = m.subway
|
||||||
|
m.bitmasks[4] = m.tram
|
||||||
|
m.bitmasks[8] = m.bus
|
||||||
|
m.bitmasks[16] = m.ferry
|
||||||
|
m.bitmasks[32] = m.express
|
||||||
|
m.bitmasks[64] = m.regional
|
||||||
|
|
||||||
|
m.categories = [
|
||||||
|
m.suburban,
|
||||||
|
m.subway,
|
||||||
|
m.tram,
|
||||||
|
m.bus,
|
||||||
|
m.ferry,
|
||||||
|
m.express,
|
||||||
|
m.regional,
|
||||||
|
m.unknown
|
||||||
|
]
|
||||||
|
|
||||||
|
m.allProducts = [
|
||||||
|
m.suburban,
|
||||||
|
m.subway,
|
||||||
|
m.tram,
|
||||||
|
m.bus,
|
||||||
|
m.ferry,
|
||||||
|
m.express,
|
||||||
|
m.regional
|
||||||
|
]
|
||||||
|
|
||||||
|
// m.parseCategory = (category) => {
|
||||||
|
// return m.categories[parseInt(category)] || m.unknown
|
||||||
|
// }
|
||||||
|
|
||||||
|
module.exports = m
|
22
p/vbb/readme.md
Normal file
22
p/vbb/readme.md
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# VBB profile for `hafas-client`
|
||||||
|
|
||||||
|
[*Verkehrsverbund Berlin-Brandenburg (VBB)*](https://en.wikipedia.org/wiki/Verkehrsverbund_Berlin-Brandenburg) is a group of public transport companies, running the public transport network in [Berlin](https://en.wikipedia.org/wiki/Berlin). This profile adds *VBB*-specific customizations to `hafas-client`. Consider using [`vbb-hafas`](https://github.com/derhuerst/vbb-hafas#vbb-hafas), to always get the customized client right away.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```js
|
||||||
|
const createClient = require('hafas-client')
|
||||||
|
const vbbProfile = require('hafas-client/p/vbb')
|
||||||
|
|
||||||
|
// create a client with VBB profile
|
||||||
|
const client = createClient(vbbProfile)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Customisations
|
||||||
|
|
||||||
|
- parses *VBB*-specific products (such as *X-Bus*)
|
||||||
|
- 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?")
|
||||||
|
- parses *VBB*-specific tickets
|
||||||
|
- renames *Ringbahn* line names to contain `⟳` and `⟲`
|
38
package.json
38
package.json
|
@ -1,12 +1,16 @@
|
||||||
{
|
{
|
||||||
"name": "hafas-client",
|
"name": "hafas-client",
|
||||||
"description": "JavaScript client for HAFAS mobile APIs.",
|
"description": "JavaScript client for HAFAS mobile APIs.",
|
||||||
"version": "1.3.1",
|
"description": "JavaScript client for HAFAS public transport APIs.",
|
||||||
|
"version": "2.0.0",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"index.js",
|
||||||
"parse.js",
|
"lib",
|
||||||
"stringify.js"
|
"parse",
|
||||||
|
"format",
|
||||||
|
"p",
|
||||||
|
"docs"
|
||||||
],
|
],
|
||||||
"author": "Jannis R <mail@jannisr.de>",
|
"author": "Jannis R <mail@jannisr.de>",
|
||||||
"homepage": "https://github.com/derhuerst/hafas-client",
|
"homepage": "https://github.com/derhuerst/hafas-client",
|
||||||
|
@ -17,21 +21,39 @@
|
||||||
"hafas",
|
"hafas",
|
||||||
"public",
|
"public",
|
||||||
"transport",
|
"transport",
|
||||||
|
"transit",
|
||||||
"api",
|
"api",
|
||||||
"mgate"
|
"http"
|
||||||
],
|
],
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fetch-ponyfill": "^4.1.0",
|
"fetch-ponyfill": "^4.1.0",
|
||||||
"moment-timezone": "^0.5.13",
|
"lodash": "^4.17.4",
|
||||||
|
"luxon": "^0.2.7",
|
||||||
"pinkie-promise": "^2.0.1",
|
"pinkie-promise": "^2.0.1",
|
||||||
"query-string": "^5.0.0",
|
"query-string": "^5.0.0",
|
||||||
"slugg": "^1.2.0"
|
"slugg": "^1.2.0",
|
||||||
|
"vbb-parse-line": "^0.2.5",
|
||||||
|
"vbb-parse-ticket": "^0.2.1",
|
||||||
|
"vbb-short-station-name": "^0.4.0",
|
||||||
|
"vbb-stations": "^5.9.0",
|
||||||
|
"vbb-translate-ids": "^3.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"co": "^4.6.0",
|
||||||
|
"db-stations": "^1.25.0",
|
||||||
|
"is-coordinates": "^2.0.2",
|
||||||
|
"is-roughly-equal": "^0.1.0",
|
||||||
|
"tap-spec": "^4.1.1",
|
||||||
|
"tape": "^4.8.0",
|
||||||
|
"tape-promise": "^2.0.1",
|
||||||
|
"validate-fptf": "^1.0.2",
|
||||||
|
"vbb-stations-autocomplete": "^2.11.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node -e \"require('.')\"",
|
"test": "node test/index.js",
|
||||||
"prepublishOnly": "npm test"
|
"prepublishOnly": "npm test | tap-spec"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
237
parse.js
237
parse.js
|
@ -1,237 +0,0 @@
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const moment = require('moment-timezone')
|
|
||||||
const slugg = require('slugg')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const dateTime = (tz, date, time) => {
|
|
||||||
let offset = 0 // in days
|
|
||||||
if (time.length > 6) {
|
|
||||||
offset = +time.slice(0, -6)
|
|
||||||
time = time.slice(-6)
|
|
||||||
}
|
|
||||||
return moment.tz(date + 'T' + time, tz)
|
|
||||||
.add(offset, 'days')
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const types = {P: 'poi', S: 'station', A: 'address'}
|
|
||||||
// todo: what is s.rRefL?
|
|
||||||
const location = (l) => {
|
|
||||||
const type = types[l.type] || 'unknown'
|
|
||||||
const result = {
|
|
||||||
type,
|
|
||||||
name: l.name,
|
|
||||||
coordinates: l.crd ? {
|
|
||||||
latitude: l.crd.y / 1000000,
|
|
||||||
longitude: l.crd.x / 1000000
|
|
||||||
} : null
|
|
||||||
}
|
|
||||||
if (type === 'poi' || type === 'station') result.id = l.extId
|
|
||||||
if ('pCls' in l) result.products = l.pCls
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// todo: what is p.number vs p.line?
|
|
||||||
// todo: what is p.icoX?
|
|
||||||
// todo: what is p.oprX?
|
|
||||||
const line = (p) => {
|
|
||||||
if (!p) return null
|
|
||||||
const result = {type: 'line', name: p.line || p.name}
|
|
||||||
if (p.cls) result.class = p.cls
|
|
||||||
if (p.prodCtx) {
|
|
||||||
result.productCode = +p.prodCtx.catCode
|
|
||||||
result.productName = p.prodCtx.catOutS
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const remark = (r) => null // todo
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const operator = (a) => ({
|
|
||||||
type: 'operator',
|
|
||||||
id: slugg(a.name),
|
|
||||||
name: a.name
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// s = stations, ln = lines, r = remarks, c = connection
|
|
||||||
const stopover = (tz, s, ln, r, c) => (st) => {
|
|
||||||
const result = {station: s[parseInt(st.locX)]}
|
|
||||||
if (st.aTimeR || st.aTimeS) {
|
|
||||||
result.arrival = dateTime(tz, c.date, st.aTimeR || st.aTimeS).format()
|
|
||||||
}
|
|
||||||
if (st.dTimeR || st.dTimeS) {
|
|
||||||
result.departure = dateTime(tz, c.date, st.dTimeR || st.dTimeS).format()
|
|
||||||
}
|
|
||||||
if (st.aCncl && st.dCncl) {
|
|
||||||
result.cancelled = true
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: finish parseRemark first
|
|
||||||
// s = stations, ln = lines, r = remarks, c = connection
|
|
||||||
const applyRemark = (s, ln, r, c) => (rm) => null
|
|
||||||
|
|
||||||
// todo: pt.sDays
|
|
||||||
// todo: pt.dep.dProgType, pt.arr.dProgType
|
|
||||||
// todo: what is pt.jny.dirFlg?
|
|
||||||
// todo: how does pt.freq work?
|
|
||||||
// tz = timezone, s = stations, ln = lines, r = remarks, c = connection
|
|
||||||
const part = (tz, s, ln, r, c) => (pt) => {
|
|
||||||
const result = {
|
|
||||||
origin: Object.assign({}, s[parseInt(pt.dep.locX)])
|
|
||||||
, destination: Object.assign({}, s[parseInt(pt.arr.locX)])
|
|
||||||
, departure: dateTime(tz, c.date, pt.dep.dTimeR || pt.dep.dTimeS).format()
|
|
||||||
, arrival: dateTime(tz, c.date, pt.arr.aTimeR || pt.arr.aTimeS).format()
|
|
||||||
}
|
|
||||||
if (pt.dep.dTimeR && pt.dep.dTimeS) {
|
|
||||||
const realtime = dateTime(tz, c.date, pt.dep.dTimeR)
|
|
||||||
const planned = dateTime(tz, c.date, pt.dep.dTimeS)
|
|
||||||
result.delay = Math.round((realtime - planned) / 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pt.type === 'WALK') result.mode = 'walking'
|
|
||||||
else if (pt.type === 'JNY') {
|
|
||||||
result.id = pt.jny.jid
|
|
||||||
result.line = ln[parseInt(pt.jny.prodX)]
|
|
||||||
result.direction = pt.jny.dirTxt // todo: parse this
|
|
||||||
|
|
||||||
if (pt.dep.dPlatfS) result.departurePlatform = pt.dep.dPlatfS
|
|
||||||
if (pt.arr.aPlatfS) result.arrivalPlatform = pt.arr.aPlatfS
|
|
||||||
|
|
||||||
if (pt.jny.stopL) result.passed = pt.jny.stopL.map(stopover(tz, s, ln, r, c))
|
|
||||||
if (Array.isArray(pt.jny.remL))
|
|
||||||
pt.jny.remL.forEach(applyRemark(s, ln, r, c))
|
|
||||||
|
|
||||||
if (pt.jny.freq && pt.jny.freq.jnyL)
|
|
||||||
result.alternatives = pt.jny.freq.jnyL
|
|
||||||
.filter((a) => a.stopL[0].locX === pt.dep.locX)
|
|
||||||
.map((a) => ({
|
|
||||||
line: ln[parseInt(a.prodX)],
|
|
||||||
when: dateTime(tz, c.date, a.stopL[0].dTimeS).format()
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: follow public-transport/friendly-public-transport-format#27 here
|
|
||||||
if (pt.dep.dCncl && pt.arr.aCncl) {
|
|
||||||
result.cancelled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: c.sDays
|
|
||||||
// todo: c.dep.dProgType, c.arr.dProgType
|
|
||||||
// todo: c.conSubscr
|
|
||||||
// todo: c.trfRes x vbb-parse-ticket
|
|
||||||
// todo: use computed information from part
|
|
||||||
// s = stations, ln = lines, r = remarks, p = parsePart
|
|
||||||
const journey = (tz, s, ln, r, p = part) => (c) => {
|
|
||||||
const parts = c.secL.map(p(tz, s, ln, r, c))
|
|
||||||
return {
|
|
||||||
parts
|
|
||||||
, origin: parts[0].origin
|
|
||||||
, destination: parts[parts.length - 1].destination
|
|
||||||
, departure: parts[0].departure
|
|
||||||
, arrival: parts[parts.length - 1].arrival
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// todos from derhuerst/hafas-client#2
|
|
||||||
// - stdStop.dPlatfS, stdStop.dPlatfR
|
|
||||||
// todo: what is d.jny.dirFlg?
|
|
||||||
// todo: d.stbStop.dProgType
|
|
||||||
// tz = timezone, s = stations, ln = lines, r = remarks
|
|
||||||
const departure = (tz, s, ln, r) => (d) => {
|
|
||||||
const result = {
|
|
||||||
ref: d.jid
|
|
||||||
, station: s[parseInt(d.stbStop.locX)]
|
|
||||||
, when: dateTime(tz, d.date, d.stbStop.dTimeR || d.stbStop.dTimeS).format()
|
|
||||||
, direction: d.dirTxt
|
|
||||||
, line: ln[parseInt(d.prodX)]
|
|
||||||
, remarks: d.remL ? d.remL.map((rm) => r[parseInt(rm.remX)]) : null
|
|
||||||
, trip: +d.jid.split('|')[1]
|
|
||||||
}
|
|
||||||
if (d.stbStop.dTimeR && d.stbStop.dTimeS) {
|
|
||||||
const realtime = dateTime(tz, d.date, d.stbStop.dTimeR)
|
|
||||||
const planned = dateTime(tz, d.date, d.stbStop.dTimeS)
|
|
||||||
result.delay = Math.round((realtime - planned) / 1000)
|
|
||||||
} else result.delay = null
|
|
||||||
|
|
||||||
// todo: follow public-transport/friendly-public-transport-format#27 here
|
|
||||||
if (d.stbStop.dCncl) {
|
|
||||||
result.cancelled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: remarks
|
|
||||||
// todo: lines
|
|
||||||
// todo: what is s.pCls?
|
|
||||||
// todo: what is s.wt?
|
|
||||||
// todo: what is s.dur?
|
|
||||||
const nearby = (n) => {
|
|
||||||
const result = location(n)
|
|
||||||
result.distance = n.dist
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: what is m.dirGeo? maybe the speed?
|
|
||||||
// todo: what is m.stopL?
|
|
||||||
// todo: what is m.proc? wut?
|
|
||||||
// todo: what is m.pos?
|
|
||||||
// todo: what is m.ani.dirGeo[n]? maybe the speed?
|
|
||||||
// todo: what is m.ani.proc[n]? wut?
|
|
||||||
// todo: how does m.ani.poly work?
|
|
||||||
// tz = timezone, l = locations, ln = lines, r = remarks
|
|
||||||
const movement = (tz, l, ln, r) => (m) => {
|
|
||||||
const result = {
|
|
||||||
direction: m.dirTxt
|
|
||||||
, line: ln[m.prodX]
|
|
||||||
, coordinates: m.pos ? {
|
|
||||||
latitude: m.pos.y / 1000000,
|
|
||||||
longitude: m.pos.x / 1000000
|
|
||||||
} : null
|
|
||||||
, nextStops: m.stopL.map((s) => ({
|
|
||||||
station: l[s.locX]
|
|
||||||
, departure: s.dTimeR || s.dTimeS
|
|
||||||
? dateTime(tz, m.date, s.dTimeR || s.dTimeS).format()
|
|
||||||
: null
|
|
||||||
, arrival: s.aTimeR || s.aTimeS
|
|
||||||
? dateTime(tz, m.date, s.aTimeR || s.aTimeS).format()
|
|
||||||
: null
|
|
||||||
}))
|
|
||||||
, frames: []
|
|
||||||
}
|
|
||||||
if (m.ani && Array.isArray(m.ani.mSec))
|
|
||||||
for (let i = 0; i < m.ani.mSec.length; i++)
|
|
||||||
result.frames.push({
|
|
||||||
origin: l[m.ani.fLocX[i]],
|
|
||||||
destination: l[m.ani.tLocX[i]],
|
|
||||||
t: m.ani.mSec[i]
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
dateTime,
|
|
||||||
location, line, remark, operator,
|
|
||||||
stopover, applyRemark, part, journey,
|
|
||||||
departure,
|
|
||||||
nearby,
|
|
||||||
movement
|
|
||||||
}
|
|
27
parse/date-time.js
Normal file
27
parse/date-time.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const {DateTime} = require('luxon')
|
||||||
|
|
||||||
|
const validDate = /^(\d{4})-(\d{2})-(\d{2})$/
|
||||||
|
|
||||||
|
const parseDateTime = (profile, date, time) => {
|
||||||
|
const pDate = [date.substr(-8, 4), date.substr(-4, 2), date.substr(-2, 2)]
|
||||||
|
if (!pDate[0] || !pDate[1] || !pDate[2]) {
|
||||||
|
throw new Error('invalid date format: ' + date)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pTime = [time.substr(-6, 2), time.substr(-4, 2), time.substr(-2, 2)]
|
||||||
|
if (!pTime[0] || !pTime[1] || !pTime[2]) {
|
||||||
|
throw new Error('invalid time format: ' + time)
|
||||||
|
}
|
||||||
|
|
||||||
|
const offset = time.length > 6 ? parseInt(time.slice(0, -6)) : 0
|
||||||
|
|
||||||
|
const dt = DateTime.fromISO(pDate.join('-') + 'T' + pTime.join(':'), {
|
||||||
|
locale: profile.locale,
|
||||||
|
zone: profile.timezone
|
||||||
|
})
|
||||||
|
return offset > 0 ? dt.plus({days: offset}) : dt
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = parseDateTime
|
44
parse/departure.js
Normal file
44
parse/departure.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
// todos from derhuerst/hafas-client#2
|
||||||
|
// - stdStop.dPlatfS, stdStop.dPlatfR
|
||||||
|
// todo: what is d.jny.dirFlg?
|
||||||
|
// todo: d.stbStop.dProgType
|
||||||
|
// todo: d.freq, d.freq.jnyL, see https://github.com/derhuerst/hafas-client/blob/9203ed1481f08baacca41ac5e3c19bf022f01b0b/parse.js#L115
|
||||||
|
|
||||||
|
const createParseDeparture = (profile, stations, lines, remarks) => {
|
||||||
|
const findRemark = rm => remarks[parseInt(rm.remX)] || null
|
||||||
|
|
||||||
|
const parseDeparture = (d) => {
|
||||||
|
const when = profile.parseDateTime(profile, d.date, d.stbStop.dTimeR || d.stbStop.dTimeS)
|
||||||
|
const res = {
|
||||||
|
journeyId: d.jid,
|
||||||
|
station: stations[parseInt(d.stbStop.locX)] || null,
|
||||||
|
when: when.toISO(),
|
||||||
|
direction: profile.parseStationName(d.dirTxt),
|
||||||
|
line: lines[parseInt(d.prodX)] || null,
|
||||||
|
remarks: d.remL ? d.remL.map(findRemark) : [],
|
||||||
|
trip: +d.jid.split('|')[1] // todo: this seems brittle
|
||||||
|
}
|
||||||
|
// todo: res.trip from rawLine.prodCtx.num
|
||||||
|
|
||||||
|
if (d.stbStop.dTimeR && d.stbStop.dTimeS) {
|
||||||
|
const realtime = profile.parseDateTime(profile, d.date, d.stbStop.dTimeR)
|
||||||
|
const planned = profile.parseDateTime(profile, d.date, d.stbStop.dTimeS)
|
||||||
|
res.delay = Math.round((realtime - planned) / 1000)
|
||||||
|
} else res.delay = null
|
||||||
|
|
||||||
|
// todo: follow public-transport/friendly-public-transport-format#27 here
|
||||||
|
// see also derhuerst/vbb-rest#19
|
||||||
|
if (d.stbStop.aCncl || d.stbStop.dCncl) {
|
||||||
|
res.cancelled = true
|
||||||
|
res.when = res.delay = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseDeparture
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = createParseDeparture
|
14
parse/index.js
Normal file
14
parse/index.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
dateTime: require('./date-time'),
|
||||||
|
location: require('./location'),
|
||||||
|
line: require('./line'),
|
||||||
|
remark: require('./remark'),
|
||||||
|
operator: require('./operator'),
|
||||||
|
stopover: require('./stopover'),
|
||||||
|
journeyLeg: require('./journey-leg'),
|
||||||
|
journey: require('./journey'),
|
||||||
|
nearby: require('./nearby'),
|
||||||
|
movement: require('./movement')
|
||||||
|
}
|
85
parse/journey-leg.js
Normal file
85
parse/journey-leg.js
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const parseDateTime = require('./date-time')
|
||||||
|
|
||||||
|
const clone = obj => Object.assign({}, obj)
|
||||||
|
|
||||||
|
const createParseJourneyLeg = (profile, stations, lines, remarks) => {
|
||||||
|
// todo: finish parse/remark.js first
|
||||||
|
const applyRemark = (j, rm) => {}
|
||||||
|
|
||||||
|
// todo: pt.sDays
|
||||||
|
// todo: pt.dep.dProgType, pt.arr.dProgType
|
||||||
|
// todo: what is pt.jny.dirFlg?
|
||||||
|
// todo: how does pt.freq work?
|
||||||
|
// todo: what is pt.himL?
|
||||||
|
const parseJourneyLeg = (j, pt, passed = true) => { // j = journey, pt = part
|
||||||
|
const dep = profile.parseDateTime(profile, j.date, pt.dep.dTimeR || pt.dep.dTimeS)
|
||||||
|
const arr = profile.parseDateTime(profile, j.date, pt.arr.aTimeR || pt.arr.aTimeS)
|
||||||
|
const res = {
|
||||||
|
origin: clone(stations[parseInt(pt.dep.locX)]) || null,
|
||||||
|
destination: clone(stations[parseInt(pt.arr.locX)]),
|
||||||
|
departure: dep.toISO(),
|
||||||
|
arrival: arr.toISO()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pt.dep.dTimeR && pt.dep.dTimeS) {
|
||||||
|
const realtime = profile.parseDateTime(profile, j.date, pt.dep.dTimeR)
|
||||||
|
const planned = profile.parseDateTime(profile, j.date, pt.dep.dTimeS)
|
||||||
|
res.delay = Math.round((realtime - planned) / 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pt.type === 'WALK') {
|
||||||
|
res.mode = 'walking'
|
||||||
|
res.public = true
|
||||||
|
} else if (pt.type === 'JNY') {
|
||||||
|
// todo: pull `public` value from `profile.products`
|
||||||
|
res.id = pt.jny.jid
|
||||||
|
res.line = lines[parseInt(pt.jny.prodX)] || null
|
||||||
|
res.direction = profile.parseStationName(pt.jny.dirTxt)
|
||||||
|
|
||||||
|
if (pt.dep.dPlatfS) res.departurePlatform = pt.dep.dPlatfS
|
||||||
|
if (pt.arr.aPlatfS) res.arrivalPlatform = pt.arr.aPlatfS
|
||||||
|
|
||||||
|
if (passed && pt.jny.stopL) {
|
||||||
|
const parse = profile.parseStopover(profile, stations, lines, remarks, j)
|
||||||
|
res.passed = pt.jny.stopL.map(parse)
|
||||||
|
}
|
||||||
|
if (Array.isArray(pt.jny.remL)) {
|
||||||
|
for (let remark of pt.jny.remL) applyRemark(j, remark)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pt.jny.freq && pt.jny.freq.jnyL) {
|
||||||
|
const parseAlternative = (a) => {
|
||||||
|
const t = a.stopL[0].dTimeS || a.stopL[0].dTimeR
|
||||||
|
const when = profile.parseDateTime(profile, j.date, t)
|
||||||
|
return {
|
||||||
|
line: lines[parseInt(a.prodX)] || null,
|
||||||
|
when: when.toISO()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.alternatives = pt.jny.freq.jnyL
|
||||||
|
.filter(a => a.stopL[0].locX === pt.dep.locX)
|
||||||
|
.map(parseAlternative)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: follow public-transport/friendly-public-transport-format#27 here
|
||||||
|
// see also derhuerst/vbb-rest#19
|
||||||
|
if (pt.arr.aCncl) {
|
||||||
|
res.cancelled = true
|
||||||
|
res.arrival = res.arrivalPlatform = null
|
||||||
|
}
|
||||||
|
if (pt.dep.dCncl) {
|
||||||
|
res.cancelled = true
|
||||||
|
res.departure = res.departurePlatform = null
|
||||||
|
res.delay = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseJourneyLeg
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = createParseJourneyLeg
|
34
parse/journey.js
Normal file
34
parse/journey.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const createParseJourneyLeg = require('./journey-leg')
|
||||||
|
|
||||||
|
const clone = obj => Object.assign({}, obj)
|
||||||
|
|
||||||
|
const createParseJourney = (profile, stations, lines, remarks) => {
|
||||||
|
const parseLeg = createParseJourneyLeg(profile, stations, lines, remarks)
|
||||||
|
|
||||||
|
// todo: c.sDays
|
||||||
|
// todo: c.dep.dProgType, c.arr.dProgType
|
||||||
|
// todo: c.conSubscr
|
||||||
|
// todo: c.trfRes x vbb-parse-ticket
|
||||||
|
const parseJourney = (j) => {
|
||||||
|
const legs = j.secL.map(leg => parseLeg(j, leg))
|
||||||
|
const res = {
|
||||||
|
legs,
|
||||||
|
origin: legs[0].origin,
|
||||||
|
destination: legs[legs.length - 1].destination,
|
||||||
|
departure: legs[0].departure,
|
||||||
|
arrival: legs[legs.length - 1].arrival
|
||||||
|
}
|
||||||
|
if (legs.some(p => p.cancelled)) {
|
||||||
|
res.cancelled = true
|
||||||
|
res.departure = res.arrival = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseJourney
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = createParseJourney
|
39
parse/line.js
Normal file
39
parse/line.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const slugg = require('slugg')
|
||||||
|
|
||||||
|
// todo: are p.number and p.line ever different?
|
||||||
|
const createParseLine = (profile, operators) => {
|
||||||
|
const parseLine = (p) => {
|
||||||
|
if (!p) return null // todo: handle this upstream
|
||||||
|
const res = {
|
||||||
|
type: 'line',
|
||||||
|
id: null,
|
||||||
|
name: p.line || p.name,
|
||||||
|
public: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't get a proper line id from the API, so we use the trip nr here.
|
||||||
|
// todo: find a better way
|
||||||
|
if (p.prodCtx && p.prodCtx.num) res.id = p.prodCtx.num
|
||||||
|
// This is terrible, but FPTF demands an ID. Let's pray for VBB to expose an ID.
|
||||||
|
else if (p.line) res.id = slugg(p.line.trim())
|
||||||
|
else if (p.name) res.id = slugg(p.name.trim())
|
||||||
|
|
||||||
|
if (p.cls) res.class = p.cls
|
||||||
|
if (p.prodCtx && p.prodCtx.catCode !== undefined) {
|
||||||
|
res.productCode = +p.prodCtx.catCode
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: parse mode, remove from profiles
|
||||||
|
|
||||||
|
if ('number' === typeof p.oprX) {
|
||||||
|
res.operator = operators[p.oprX] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
return parseLine
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = createParseLine
|
34
parse/location.js
Normal file
34
parse/location.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const POI = 'P'
|
||||||
|
const STATION = 'S'
|
||||||
|
const ADDRESS = 'A'
|
||||||
|
|
||||||
|
// todo: what is s.rRefL?
|
||||||
|
// todo: is passing in profile necessary?
|
||||||
|
const parseLocation = (profile, l) => {
|
||||||
|
const res = {type: 'location'}
|
||||||
|
if (l.crd) {
|
||||||
|
res.latitude = l.crd.y / 1000000
|
||||||
|
res.longitude = l.crd.x / 1000000
|
||||||
|
}
|
||||||
|
|
||||||
|
if (l.type === STATION) {
|
||||||
|
const station = {
|
||||||
|
type: 'station',
|
||||||
|
id: l.extId,
|
||||||
|
name: l.name,
|
||||||
|
location: res
|
||||||
|
}
|
||||||
|
if ('pCls' in l) station.products = profile.parseProducts(l.pCls)
|
||||||
|
return station
|
||||||
|
}
|
||||||
|
|
||||||
|
if (l.type === ADDRESS) res.address = l.name
|
||||||
|
else res.name = l.name
|
||||||
|
if (l.type === POI) res.id = l.extId
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = parseLocation
|
67
parse/movement.js
Normal file
67
parse/movement.js
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const createParseMovement = (profile, locations, lines, remarks) => {
|
||||||
|
// todo: what is m.dirGeo? maybe the speed?
|
||||||
|
// todo: what is m.stopL?
|
||||||
|
// todo: what is m.proc? wut?
|
||||||
|
// todo: what is m.pos?
|
||||||
|
// todo: what is m.ani.dirGeo[n]? maybe the speed?
|
||||||
|
// todo: what is m.ani.proc[n]? wut?
|
||||||
|
// todo: how does m.ani.poly work?
|
||||||
|
const parseMovement = (m) => {
|
||||||
|
const parseNextStop = (s) => {
|
||||||
|
const dep = s.dTimeR || s.dTimeS
|
||||||
|
? profile.parseDateTime(profile, m.date, s.dTimeR || s.dTimeS)
|
||||||
|
: null
|
||||||
|
const arr = s.aTimeR || s.aTimeS
|
||||||
|
? profile.parseDateTime(profile, m.date, s.aTimeR || s.aTimeS)
|
||||||
|
: null
|
||||||
|
|
||||||
|
const res = {
|
||||||
|
station: locations[s.locX],
|
||||||
|
departure: dep ? dep.toISO() : null,
|
||||||
|
arrival: arr ? arr.toISO() : null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m.dTimeR && m.dTimeS) {
|
||||||
|
const plannedDep = profile.parseDateTime(profile, m.date, s.dTimeS)
|
||||||
|
res.departureDelay = Math.round((dep - plannedDep) / 1000)
|
||||||
|
} else res.departureDelay = null
|
||||||
|
|
||||||
|
if (m.aTimeR && m.aTimeS) {
|
||||||
|
const plannedArr = profile.parseDateTime(profile, m.date, s.aTimeS)
|
||||||
|
res.arrivalDelay = Math.round((arr - plannedArr) / 1000)
|
||||||
|
} else res.arrivalDelay = null
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = {
|
||||||
|
direction: profile.parseStationName(m.dirTxt),
|
||||||
|
trip: m.jid && +m.jid.split('|')[1] || null, // todo: this seems brittle
|
||||||
|
line: lines[m.prodX] || null,
|
||||||
|
location: m.pos ? {
|
||||||
|
type: 'location',
|
||||||
|
latitude: m.pos.y / 1000000,
|
||||||
|
longitude: m.pos.x / 1000000
|
||||||
|
} : null,
|
||||||
|
nextStops: m.stopL.map(parseNextStop),
|
||||||
|
frames: []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m.ani && Array.isArray(m.ani.mSec)) {
|
||||||
|
for (let i = 0; i < m.ani.mSec.length; i++) {
|
||||||
|
res.frames.push({
|
||||||
|
origin: locations[m.ani.fLocX[i]] || null,
|
||||||
|
destination: locations[m.ani.tLocX[i]] || null,
|
||||||
|
t: m.ani.mSec[i]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
return parseMovement
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = createParseMovement
|
14
parse/nearby.js
Normal file
14
parse/nearby.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
// todo: remarks
|
||||||
|
// todo: lines
|
||||||
|
// todo: what is s.pCls?
|
||||||
|
// todo: what is s.wt?
|
||||||
|
// todo: what is s.dur?
|
||||||
|
const parseNearby = (profile, n) => {
|
||||||
|
const res = profile.parseLocation(profile, n)
|
||||||
|
res.distance = n.dist
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = parseNearby
|
14
parse/operator.js
Normal file
14
parse/operator.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const slugg = require('slugg')
|
||||||
|
|
||||||
|
// todo: is passing in profile necessary?
|
||||||
|
const parseOperator = (profile, a) => {
|
||||||
|
return {
|
||||||
|
type: 'operator',
|
||||||
|
id: slugg(a.name), // todo: find a more reliable way
|
||||||
|
name: a.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = parseOperator
|
16
parse/products-bitmask.js
Normal file
16
parse/products-bitmask.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const createParseBitmask = (bitmasks) => {
|
||||||
|
const parseBitmask = (bitmask) => {
|
||||||
|
const products = {}
|
||||||
|
let i = 1
|
||||||
|
do {
|
||||||
|
products[bitmasks[i].product] = !!(bitmask & i)
|
||||||
|
i *= 2
|
||||||
|
} while (bitmasks[i] && bitmasks[i].product)
|
||||||
|
return products
|
||||||
|
}
|
||||||
|
return parseBitmask
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = createParseBitmask
|
8
parse/remark.js
Normal file
8
parse/remark.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
// todo: is passing in profile necessary?
|
||||||
|
const parseRemark = (profile, r) => {
|
||||||
|
return null // todo
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = parseRemark
|
36
parse/stopover.js
Normal file
36
parse/stopover.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
// todo: arrivalDelay, departureDelay or only delay ?
|
||||||
|
// todo: arrivalPlatform, departurePlatform
|
||||||
|
const createParseStopover = (profile, stations, lines, remarks, connection) => {
|
||||||
|
const parseStopover = (st) => {
|
||||||
|
const res = {
|
||||||
|
station: stations[parseInt(st.locX)] || null
|
||||||
|
}
|
||||||
|
if (st.aTimeR || st.aTimeS) {
|
||||||
|
const arr = profile.parseDateTime(profile, connection.date, st.aTimeR || st.aTimeS)
|
||||||
|
res.arrival = arr.toISO()
|
||||||
|
}
|
||||||
|
if (st.dTimeR || st.dTimeS) {
|
||||||
|
const dep = profile.parseDateTime(profile, connection.date, st.dTimeR || st.dTimeS)
|
||||||
|
res.departure = dep.toISO()
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: follow public-transport/friendly-public-transport-format#27 here
|
||||||
|
// see also derhuerst/vbb-rest#19
|
||||||
|
if (st.aCncl) {
|
||||||
|
res.cancelled = true
|
||||||
|
res.arrival = null
|
||||||
|
}
|
||||||
|
if (st.dCncl) {
|
||||||
|
res.cancelled = true
|
||||||
|
res.departure = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseStopover
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = createParseStopover
|
164
readme.md
164
readme.md
|
@ -1,15 +1,25 @@
|
||||||
# hafas-client
|
# hafas-client
|
||||||
|
|
||||||
**A client for HAFAS mobile APIs**, providing the base for [vbb-hafas](https://github.com/derhuerst/vbb-hafas) and [db-hafas](https://github.com/derhuerst/db-hafas).
|
**A client for HAFAS public transport APIs**. Sort of like [public-transport-enabler](https://github.com/schildbach/public-transport-enabler), but with a smaller scope. It also [contains customisations](p) for the following transport networks:
|
||||||
|
|
||||||
|
HAFAS endpoint | wrapper library? | docs | example code | source code
|
||||||
|
---------------|------------------|------|---------|------------
|
||||||
|
[Deutsche Bahn](https://en.wikipedia.org/wiki/Deutsche_Bahn) | [`vbb-hafas`](https://github.com/derhuerst/vbb-hafas), which has additional features | [docs](p/db/readme.md) | [example code](p/db/example.js) | [src](p/db/index.js)
|
||||||
|
[Berlin & Brandenburg public transport](https://en.wikipedia.org/wiki/Verkehrsverbund_Berlin-Brandenburg) | [`db-hafas`](https://github.com/derhuerst/db-hafas), which has additional features | [docs](p/vbb/readme.md) | [example code](p/vbb/example.js) | [src](p/vbb/index.js)
|
||||||
|
|
||||||
[](https://www.npmjs.com/package/hafas-client)
|
[](https://www.npmjs.com/package/hafas-client)
|
||||||
[](https://travis-ci.org/derhuerst/hafas-client)
|
[](https://travis-ci.org/derhuerst/hafas-client)
|
||||||
[](https://david-dm.org/derhuerst/hafas-client)
|
|
||||||
[](https://david-dm.org/derhuerst/hafas-client#info=devDependencies)
|
|
||||||

|

|
||||||
[](https://gitter.im/derhuerst)
|
[](https://gitter.im/derhuerst)
|
||||||
|
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
There's [a company called HaCon](http://hacon.de) that sells [a public transport management system called HAFAS](https://de.wikipedia.org/wiki/HAFAS). It is [used by companies all over Europe](https://gist.github.com/derhuerst/2b7ed83bfa5f115125a5) to serve routing and departure information for apps. All those endpoints are similar, with the same terms and API routes, but have slightly different options, filters and sets of enabled features.
|
||||||
|
|
||||||
|
`hafas-client` contains all logic for communicating with these, as well as serialising from and parsing to [*Friendly Public Transport Format (FPTF)* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/blob/1.0.1/spec/readme.md). Endpoint-specific customisations (called *profiles* here) increase the quality of the returned data.
|
||||||
|
|
||||||
|
|
||||||
## Installing
|
## Installing
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
@ -17,9 +27,155 @@ npm install hafas-client
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
- [`journeys(from, to, [opt])`](docs/journeys.md) – get journeys between locations
|
||||||
|
- [`journeyLeg(ref, name, [opt])`](docs/journey-leg.md) – get details for a leg of a journey
|
||||||
|
- [`departures(station, [opt])`](docs/departures.md) – query the next departures at a station
|
||||||
|
- [`locations(query, [opt])`](docs/locations.md) – find stations, POIs and addresses
|
||||||
|
- [`nearby(location, [opt])`](docs/nearby.md) – show stations & POIs around
|
||||||
|
- [`radar(query, [opt])`](docs/radar.md) – find all vehicles currently in a certain area
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
See [vbb-hafas](https://github.com/derhuerst/vbb-hafas/blob/master/lib/request.js).
|
```js
|
||||||
|
const createClient = require('hafas-client')
|
||||||
|
const dbProfile = require('hafas-client/p/db')
|
||||||
|
|
||||||
|
// create a client with Deutsche Bahn profile
|
||||||
|
const client = createClient(dbProfile)
|
||||||
|
|
||||||
|
// Berlin Jungfernheide to München Hbf
|
||||||
|
client.journeys('8011167', '8000261', {results: 1})
|
||||||
|
.then(console.log)
|
||||||
|
.catch(console.error)
|
||||||
|
```
|
||||||
|
|
||||||
|
The returned [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/promise) will resolve with an array of one [*FPTF* `journey`](https://github.com/public-transport/friendly-public-transport-format/blob/1.0.1/spec/readme.md#journey).
|
||||||
|
|
||||||
|
```js
|
||||||
|
[ {
|
||||||
|
legs: [ {
|
||||||
|
id: '1|100067|48|81|17122017',
|
||||||
|
origin: {
|
||||||
|
type: 'station',
|
||||||
|
id: '8089100',
|
||||||
|
name: 'Berlin Jungfernheide (S)',
|
||||||
|
location: {
|
||||||
|
type: 'location',
|
||||||
|
latitude: 52.530291,
|
||||||
|
longitude: 13.299451
|
||||||
|
},
|
||||||
|
products: { /* … */ }
|
||||||
|
},
|
||||||
|
departure: '2017-12-17T17:05:00.000+01:00',
|
||||||
|
departurePlatform: '5',
|
||||||
|
destination: {
|
||||||
|
type: 'station',
|
||||||
|
id: '8089118',
|
||||||
|
name: 'Berlin Beusselstraße',
|
||||||
|
location: { /* … */ },
|
||||||
|
products: { /* … */ }
|
||||||
|
},
|
||||||
|
arrival: '2017-12-17T17:08:00.000+01:00',
|
||||||
|
arrivalPlatform: '1',
|
||||||
|
line: {
|
||||||
|
type: 'line',
|
||||||
|
id: '41172',
|
||||||
|
name: 'S 41',
|
||||||
|
public: true,
|
||||||
|
mode: 'train',
|
||||||
|
product: 'suburban',
|
||||||
|
class: 16,
|
||||||
|
productCode: 4,
|
||||||
|
operator: {
|
||||||
|
type: 'operator',
|
||||||
|
id: 's-bahn-berlin-gmbh',
|
||||||
|
name: 'S-Bahn Berlin GmbH'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
direction: 'Ringbahn ->'
|
||||||
|
}, /* … */ {
|
||||||
|
origin: {
|
||||||
|
type: 'station',
|
||||||
|
id: '730749',
|
||||||
|
name: 'Berlin Hauptbahnhof (S+U), Berlin',
|
||||||
|
location: {
|
||||||
|
type: 'location',
|
||||||
|
latitude: 52.526461,
|
||||||
|
longitude: 13.369378
|
||||||
|
},
|
||||||
|
products: { /* … */ }
|
||||||
|
},
|
||||||
|
departure: '2017-12-17T17:25:00.000+01:00',
|
||||||
|
destination: {
|
||||||
|
type: 'station',
|
||||||
|
id: '8098160',
|
||||||
|
name: 'Berlin Hbf (tief)',
|
||||||
|
location: { /* … */ },
|
||||||
|
products: { /* … */ }
|
||||||
|
},
|
||||||
|
arrival: '2017-12-17T17:33:00.000+01:00',
|
||||||
|
mode: 'walking',
|
||||||
|
public: true
|
||||||
|
}, {
|
||||||
|
id: '1|70906|0|81|17122017',
|
||||||
|
origin: {
|
||||||
|
type: 'station',
|
||||||
|
id: '8098160',
|
||||||
|
name: 'Berlin Hbf (tief)',
|
||||||
|
location: { /* … */ },
|
||||||
|
products: { /* … */ }
|
||||||
|
},
|
||||||
|
departure: '2017-12-17T17:37:00.000+01:00',
|
||||||
|
departurePlatform: '1',
|
||||||
|
destination: {
|
||||||
|
type: 'station',
|
||||||
|
id: '8000261',
|
||||||
|
name: 'München Hbf',
|
||||||
|
location: { /* … */ },
|
||||||
|
products: { /* … */ }
|
||||||
|
},
|
||||||
|
arrival: '2017-12-17T22:45:00.000+01:00',
|
||||||
|
arrivalPlatform: '13',
|
||||||
|
line: { /* … */ },
|
||||||
|
direction: 'München Hbf'
|
||||||
|
} ],
|
||||||
|
origin: {
|
||||||
|
type: 'station',
|
||||||
|
id: '8089100',
|
||||||
|
name: 'Berlin Jungfernheide (S)',
|
||||||
|
location: { /* … */ },
|
||||||
|
products: { /* … */ }
|
||||||
|
},
|
||||||
|
departure: '2017-12-17T17:05:00.000+01:00',
|
||||||
|
destination: {
|
||||||
|
type: 'station',
|
||||||
|
id: '8000261',
|
||||||
|
name: 'München Hbf',
|
||||||
|
location: { /* … */ },
|
||||||
|
products: { /* … */ }
|
||||||
|
},
|
||||||
|
arrival: '2017-12-17T22:45:00.000+01:00',
|
||||||
|
price: {
|
||||||
|
amount: null,
|
||||||
|
hint: 'No pricing information available.'
|
||||||
|
}
|
||||||
|
} ]
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [*Friendly Public Transport Format*](https://github.com/public-transport/friendly-public-transport-format#friendly-public-transport-format-fptf) – A format for APIs, libraries and datasets containing and working with public transport data.
|
||||||
|
- [`db-hafas`](https://github.com/derhuerst/db-hafas#db-hafas) – JavaScript client for the DB HAFAS API.
|
||||||
|
- [`vbb-hafas`](https://github.com/derhuerst/vbb-hafas#vbb-hafas) – JavaScript client for Berlin & Brandenburg public transport HAFAS API.
|
||||||
|
- [`hafas-departures-in-direction`](https://github.com/derhuerst/hafas-departures-in-direction#hafas-departures-in-direction) – Pass in a HAFAS client, get departures in a certain direction.
|
||||||
|
- [`hafas-collect-departures-at`](https://github.com/derhuerst/hafas-collect-departures-at#hafas-collect-departures-at) – Utility to collect departures, using any HAFAS client.
|
||||||
|
- [`hafas-rest-api`](https://github.com/derhuerst/hafas-rest-api#hafas-rest-api) – Expose a HAFAS client via an HTTP REST API.
|
||||||
|
- [List of european long-distance transport operators, available API endpoints, GTFS feeds and client modules.](https://github.com/public-transport/european-transport-operators)
|
||||||
|
- [Collection of european transport JavaScript modules.](https://github.com/public-transport/european-transport-modules)
|
||||||
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
46
stringify.js
46
stringify.js
|
@ -1,46 +0,0 @@
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const moment = require('moment-timezone')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const date = (tz, when) => moment(when).tz(tz).format('YYYYMMDD')
|
|
||||||
const time = (tz, when) => moment(when).tz(tz).format('HHmmss')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// filters
|
|
||||||
const bike = {type: 'BC', mode: 'INC'}
|
|
||||||
const accessibility = {
|
|
||||||
none: {type: 'META', mode: 'INC', meta: 'notBarrierfree'}
|
|
||||||
, partial: {type: 'META', mode: 'INC', meta: 'limitedBarrierfree'}
|
|
||||||
, complete: {type: 'META', mode: 'INC', meta: 'completeBarrierfree'}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const coord = (x) => Math.round(x * 1000000)
|
|
||||||
const station = (id) => ({type: 'S', lid: 'L=' + id})
|
|
||||||
const address = (latitude, longitude, name) => {
|
|
||||||
if (!latitude || !longitude || !name) throw new Error('invalid address.')
|
|
||||||
return {type: 'A', name, crd: {x: coord(longitude), y: coord(latitude)}}
|
|
||||||
}
|
|
||||||
const poi = (latitude, longitude, id, name) => {
|
|
||||||
if (!latitude || !longitude || !id || !name) throw new Error('invalid poi.')
|
|
||||||
return {type: 'P', name, lid: 'L=' + id, crd: {x: coord(longitude), y: coord(latitude)}}
|
|
||||||
}
|
|
||||||
|
|
||||||
const locationFilter = (stations, addresses, poi) => {
|
|
||||||
if (stations && addresses && poi) return 'ALL'
|
|
||||||
return (stations ? 'S' : '')
|
|
||||||
+ (addresses ? 'A' : '')
|
|
||||||
+ (poi ? 'P' : '')
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
date, time,
|
|
||||||
bike, accessibility,
|
|
||||||
coord, station, address, poi, locationFilter
|
|
||||||
}
|
|
285
test/db.js
Normal file
285
test/db.js
Normal file
|
@ -0,0 +1,285 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const getStations = require('db-stations').full
|
||||||
|
const tapePromise = require('tape-promise').default
|
||||||
|
const tape = require('tape')
|
||||||
|
const co = require('co')
|
||||||
|
const isRoughlyEqual = require('is-roughly-equal')
|
||||||
|
|
||||||
|
const createClient = require('..')
|
||||||
|
const dbProfile = require('../p/db')
|
||||||
|
const modes = require('../p/db/modes')
|
||||||
|
const {
|
||||||
|
assertValidStation,
|
||||||
|
assertValidPoi,
|
||||||
|
assertValidAddress,
|
||||||
|
assertValidLocation,
|
||||||
|
assertValidLine,
|
||||||
|
assertValidStopover,
|
||||||
|
when, isValidWhen
|
||||||
|
} = require('./util.js')
|
||||||
|
|
||||||
|
const assertValidStationProducts = (t, p) => {
|
||||||
|
t.ok(p)
|
||||||
|
t.equal(typeof p.nationalExp, 'boolean')
|
||||||
|
t.equal(typeof p.national, 'boolean')
|
||||||
|
t.equal(typeof p.regionalExp, 'boolean')
|
||||||
|
t.equal(typeof p.regional, 'boolean')
|
||||||
|
t.equal(typeof p.suburban, 'boolean')
|
||||||
|
t.equal(typeof p.bus, 'boolean')
|
||||||
|
t.equal(typeof p.ferry, 'boolean')
|
||||||
|
t.equal(typeof p.subway, 'boolean')
|
||||||
|
t.equal(typeof p.tram, 'boolean')
|
||||||
|
t.equal(typeof p.taxi, 'boolean')
|
||||||
|
}
|
||||||
|
|
||||||
|
const findStation = (id) => new Promise((yay, nay) => {
|
||||||
|
const stations = getStations()
|
||||||
|
stations
|
||||||
|
.once('error', nay)
|
||||||
|
.on('data', (s) => {
|
||||||
|
if (
|
||||||
|
s.id === id ||
|
||||||
|
(s.additionalIds && s.additionalIds.includes(id))
|
||||||
|
) {
|
||||||
|
yay(s)
|
||||||
|
stations.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.once('end', yay)
|
||||||
|
})
|
||||||
|
|
||||||
|
const isJungfernheide = (s) => {
|
||||||
|
return s.type === 'station' &&
|
||||||
|
(s.id === '008011167' || s.id === '8011167') &&
|
||||||
|
s.name === 'Berlin Jungfernheide' &&
|
||||||
|
s.location &&
|
||||||
|
isRoughlyEqual(s.location.latitude, 52.530408, .0005) &&
|
||||||
|
isRoughlyEqual(s.location.longitude, 13.299424, .0005)
|
||||||
|
}
|
||||||
|
|
||||||
|
const assertIsJungfernheide = (t, s) => {
|
||||||
|
t.equal(s.type, 'station')
|
||||||
|
t.ok(s.id === '008011167' || s.id === '8011167', 'id should be 8011167')
|
||||||
|
t.equal(s.name, 'Berlin Jungfernheide')
|
||||||
|
t.ok(s.location)
|
||||||
|
t.ok(isRoughlyEqual(s.location.latitude, 52.530408, .0005))
|
||||||
|
t.ok(isRoughlyEqual(s.location.longitude, 13.299424, .0005))
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: this doesnt seem to work
|
||||||
|
// todo: DRY with assertValidStationProducts
|
||||||
|
const assertValidProducts = (t, p) => {
|
||||||
|
for (let k of Object.keys(modes)) {
|
||||||
|
t.ok('boolean', typeof modes[k], 'mode ' + k + ' must be a boolean')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const assertValidPrice = (t, p) => {
|
||||||
|
t.ok(p)
|
||||||
|
if (p.amount !== null) {
|
||||||
|
t.equal(typeof p.amount, 'number')
|
||||||
|
t.ok(p.amount > 0)
|
||||||
|
}
|
||||||
|
if (p.hint !== null) {
|
||||||
|
t.equal(typeof p.hint, 'string')
|
||||||
|
t.ok(p.hint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const test = tapePromise(tape)
|
||||||
|
const client = createClient(dbProfile)
|
||||||
|
|
||||||
|
test('Berlin Jungfernheide to München Hbf', co.wrap(function* (t) {
|
||||||
|
const journeys = yield client.journeys('8011167', '8000261', {
|
||||||
|
when, passedStations: true
|
||||||
|
})
|
||||||
|
|
||||||
|
t.ok(Array.isArray(journeys))
|
||||||
|
t.ok(journeys.length > 0, 'no journeys')
|
||||||
|
for (let journey of journeys) {
|
||||||
|
assertValidStation(t, journey.origin)
|
||||||
|
assertValidStationProducts(t, journey.origin.products)
|
||||||
|
if (!(yield findStation(journey.origin.id))) {
|
||||||
|
console.error('unknown station', journey.origin.id, journey.origin.name)
|
||||||
|
}
|
||||||
|
if (journey.origin.products) {
|
||||||
|
assertValidProducts(t, journey.origin.products)
|
||||||
|
}
|
||||||
|
t.ok(isValidWhen(journey.departure))
|
||||||
|
|
||||||
|
assertValidStation(t, journey.destination)
|
||||||
|
assertValidStationProducts(t, journey.origin.products)
|
||||||
|
if (!(yield findStation(journey.origin.id))) {
|
||||||
|
console.error('unknown station', journey.destination.id, journey.destination.name)
|
||||||
|
}
|
||||||
|
if (journey.destination.products) {
|
||||||
|
assertValidProducts(t, journey.destination.products)
|
||||||
|
}
|
||||||
|
t.ok(isValidWhen(journey.arrival))
|
||||||
|
|
||||||
|
t.ok(Array.isArray(journey.legs))
|
||||||
|
t.ok(journey.legs.length > 0, 'no legs')
|
||||||
|
const leg = journey.legs[0]
|
||||||
|
|
||||||
|
assertValidStation(t, leg.origin)
|
||||||
|
assertValidStationProducts(t, leg.origin.products)
|
||||||
|
if (!(yield findStation(leg.origin.id))) {
|
||||||
|
console.error('unknown station', leg.origin.id, leg.origin.name)
|
||||||
|
}
|
||||||
|
t.ok(isValidWhen(leg.departure))
|
||||||
|
t.equal(typeof leg.departurePlatform, 'string')
|
||||||
|
|
||||||
|
assertValidStation(t, leg.destination)
|
||||||
|
assertValidStationProducts(t, leg.origin.products)
|
||||||
|
if (!(yield findStation(leg.destination.id))) {
|
||||||
|
console.error('unknown station', leg.destination.id, leg.destination.name)
|
||||||
|
}
|
||||||
|
t.ok(isValidWhen(leg.arrival))
|
||||||
|
t.equal(typeof leg.arrivalPlatform, 'string')
|
||||||
|
|
||||||
|
assertValidLine(t, leg.line)
|
||||||
|
|
||||||
|
t.ok(Array.isArray(leg.passed))
|
||||||
|
for (let stopover of leg.passed) assertValidStopover(t, stopover)
|
||||||
|
|
||||||
|
if (journey.price) assertValidPrice(t, journey.price)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.end()
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('Berlin Jungfernheide to Torfstraße 17', co.wrap(function* (t) {
|
||||||
|
const journeys = yield client.journeys('8011167', {
|
||||||
|
type: 'location', address: 'Torfstraße 17',
|
||||||
|
latitude: 52.5416823, longitude: 13.3491223
|
||||||
|
}, {when})
|
||||||
|
|
||||||
|
t.ok(Array.isArray(journeys))
|
||||||
|
t.ok(journeys.length >= 1, 'no journeys')
|
||||||
|
const journey = journeys[0]
|
||||||
|
const leg = journey.legs[journey.legs.length - 1]
|
||||||
|
|
||||||
|
assertValidStation(t, leg.origin)
|
||||||
|
assertValidStationProducts(t, leg.origin.products)
|
||||||
|
if (!(yield findStation(leg.origin.id))) {
|
||||||
|
console.error('unknown station', leg.origin.id, leg.origin.name)
|
||||||
|
}
|
||||||
|
if (leg.origin.products) assertValidProducts(t, leg.origin.products)
|
||||||
|
t.ok(isValidWhen(leg.departure))
|
||||||
|
t.ok(isValidWhen(leg.arrival))
|
||||||
|
|
||||||
|
const d = leg.destination
|
||||||
|
assertValidAddress(t, d)
|
||||||
|
t.equal(d.address, 'Torfstraße 17')
|
||||||
|
t.ok(isRoughlyEqual(.0001, d.latitude, 52.5416823))
|
||||||
|
t.ok(isRoughlyEqual(.0001, d.longitude, 13.3491223))
|
||||||
|
|
||||||
|
t.end()
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('Berlin Jungfernheide to ATZE Musiktheater', co.wrap(function* (t) {
|
||||||
|
const journeys = yield client.journeys('8011167', {
|
||||||
|
type: 'location', id: '991598902', name: 'ATZE Musiktheater',
|
||||||
|
latitude: 52.542417, longitude: 13.350437
|
||||||
|
}, {when})
|
||||||
|
|
||||||
|
t.ok(Array.isArray(journeys))
|
||||||
|
t.ok(journeys.length >= 1, 'no journeys')
|
||||||
|
const journey = journeys[0]
|
||||||
|
const leg = journey.legs[journey.legs.length - 1]
|
||||||
|
|
||||||
|
assertValidStation(t, leg.origin)
|
||||||
|
assertValidStationProducts(t, leg.origin.products)
|
||||||
|
if (!(yield findStation(leg.origin.id))) {
|
||||||
|
console.error('unknown station', leg.origin.id, leg.origin.name)
|
||||||
|
}
|
||||||
|
if (leg.origin.products) assertValidProducts(t, leg.origin.products)
|
||||||
|
t.ok(isValidWhen(leg.departure))
|
||||||
|
t.ok(isValidWhen(leg.arrival))
|
||||||
|
|
||||||
|
const d = leg.destination
|
||||||
|
assertValidPoi(t, d)
|
||||||
|
t.equal(d.name, 'ATZE Musiktheater')
|
||||||
|
t.ok(isRoughlyEqual(.0001, d.latitude, 52.542399))
|
||||||
|
t.ok(isRoughlyEqual(.0001, d.longitude, 13.350402))
|
||||||
|
|
||||||
|
t.end()
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('departures at Berlin Jungfernheide', co.wrap(function* (t) {
|
||||||
|
const deps = yield client.departures('8011167', {
|
||||||
|
duration: 5, when
|
||||||
|
})
|
||||||
|
|
||||||
|
t.ok(Array.isArray(deps))
|
||||||
|
for (let dep of deps) {
|
||||||
|
assertValidStation(t, dep.station)
|
||||||
|
assertValidStationProducts(t, dep.station.products)
|
||||||
|
if (!(yield findStation(dep.station.id))) {
|
||||||
|
console.error('unknown station', dep.station.id, dep.station.name)
|
||||||
|
}
|
||||||
|
if (dep.station.products) assertValidProducts(t, dep.station.products)
|
||||||
|
t.ok(isValidWhen(dep.when))
|
||||||
|
}
|
||||||
|
|
||||||
|
t.end()
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('departures with station object', co.wrap(function* (t) {
|
||||||
|
yield client.departures({
|
||||||
|
type: 'station',
|
||||||
|
id: '8011167',
|
||||||
|
name: 'Berlin Jungfernheide',
|
||||||
|
location: {
|
||||||
|
type: 'location',
|
||||||
|
latitude: 1.23,
|
||||||
|
longitude: 2.34
|
||||||
|
}
|
||||||
|
}, {when})
|
||||||
|
|
||||||
|
t.ok('did not fail')
|
||||||
|
t.end()
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('nearby Berlin Jungfernheide', co.wrap(function* (t) {
|
||||||
|
const nearby = yield client.nearby({
|
||||||
|
type: 'location',
|
||||||
|
latitude: 52.530273,
|
||||||
|
longitude: 13.299433
|
||||||
|
}, {
|
||||||
|
results: 2, distance: 400
|
||||||
|
})
|
||||||
|
|
||||||
|
t.ok(Array.isArray(nearby))
|
||||||
|
t.equal(nearby.length, 2)
|
||||||
|
|
||||||
|
assertIsJungfernheide(t, nearby[0])
|
||||||
|
t.ok(nearby[0].distance >= 0)
|
||||||
|
t.ok(nearby[0].distance <= 100)
|
||||||
|
|
||||||
|
for (let n of nearby) {
|
||||||
|
if (n.type === 'station') assertValidStation(t, n)
|
||||||
|
else assertValidLocation(t, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.end()
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('locations named Jungfernheide', co.wrap(function* (t) {
|
||||||
|
const locations = yield client.locations('Jungfernheide', {
|
||||||
|
results: 10
|
||||||
|
})
|
||||||
|
|
||||||
|
t.ok(Array.isArray(locations))
|
||||||
|
t.ok(locations.length > 0)
|
||||||
|
t.ok(locations.length <= 10)
|
||||||
|
|
||||||
|
for (let l of locations) {
|
||||||
|
if (l.type === 'station') assertValidStation(t, l)
|
||||||
|
else assertValidLocation(t, l)
|
||||||
|
}
|
||||||
|
t.ok(locations.some(isJungfernheide))
|
||||||
|
|
||||||
|
t.end()
|
||||||
|
}))
|
4
test/index.js
Normal file
4
test/index.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
require('./db')
|
||||||
|
require('./vbb')
|
156
test/util.js
Normal file
156
test/util.js
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const validateFptf = require('validate-fptf')
|
||||||
|
const isRoughlyEqual = require('is-roughly-equal')
|
||||||
|
const {DateTime} = require('luxon')
|
||||||
|
const isValidWGS84 = require('is-coordinates')
|
||||||
|
|
||||||
|
const validateFptfWith = (t, item, allowedTypes, name) => {
|
||||||
|
try {
|
||||||
|
validateFptf.recurse(allowedTypes, item, name)
|
||||||
|
} catch (err) {
|
||||||
|
t.ifError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const assertValidStation = (t, s, coordsOptional = false) => {
|
||||||
|
validateFptfWith(t, s, ['station'], 'station')
|
||||||
|
|
||||||
|
if (!coordsOptional || (s.location !== null && s.location !== undefined)) {
|
||||||
|
t.ok(s.location)
|
||||||
|
assertValidLocation(t, s.location, coordsOptional)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const assertValidPoi = (t, p) => {
|
||||||
|
assertValidLocation(t, p, true)
|
||||||
|
|
||||||
|
t.equal(typeof p.id, 'string')
|
||||||
|
t.equal(typeof p.name, 'string')
|
||||||
|
if (p.address !== null && p.address !== undefined) {
|
||||||
|
t.equal(typeof p.address, 'string')
|
||||||
|
t.ok(p.address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const assertValidAddress = (t, a) => {
|
||||||
|
assertValidLocation(t, a, true)
|
||||||
|
|
||||||
|
t.equal(typeof a.address, 'string')
|
||||||
|
}
|
||||||
|
|
||||||
|
const assertValidLocation = (t, l, coordsOptional = false) => {
|
||||||
|
t.equal(l.type, 'location')
|
||||||
|
if (l.name !== null && l.name !== undefined) {
|
||||||
|
t.equal(typeof l.name, 'string')
|
||||||
|
t.ok(l.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (l.address !== null && l.address !== undefined) {
|
||||||
|
t.equal(typeof l.address, 'string')
|
||||||
|
t.ok(l.address)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasLatitude = l.latitude !== null && l.latitude !== undefined
|
||||||
|
const hasLongitude = l.longitude !== null && l.longitude !== undefined
|
||||||
|
if (!coordsOptional && hasLatitude) t.equal(typeof l.latitude, 'number')
|
||||||
|
if (!coordsOptional && hasLongitude) t.equal(typeof l.longitude, 'number')
|
||||||
|
if ((hasLongitude && !hasLatitude) || (hasLatitude && !hasLongitude)) {
|
||||||
|
t.fail('should have both .latitude and .longitude')
|
||||||
|
}
|
||||||
|
if (hasLatitude && hasLongitude) isValidWGS84([l.longitude, l.latitude])
|
||||||
|
|
||||||
|
if (!coordsOptional && l.altitude !== null && l.altitude !== undefined) {
|
||||||
|
t.equal(typeof l.altitude, 'number')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validLineModes = [
|
||||||
|
'train', 'bus', 'watercraft', 'taxi', 'gondola', 'aircraft',
|
||||||
|
'car', 'bicycle', 'walking'
|
||||||
|
]
|
||||||
|
|
||||||
|
const assertValidLine = (t, l) => {
|
||||||
|
validateFptfWith(t, l, ['line'], 'line')
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidDateTime = (w) => {
|
||||||
|
return !Number.isNaN(+new Date(w))
|
||||||
|
}
|
||||||
|
|
||||||
|
const assertValidStopover = (t, s, coordsOptional = false) => {
|
||||||
|
if ('arrival' in s) t.ok(isValidDateTime(s.arrival))
|
||||||
|
if ('departure' in s) t.ok(isValidDateTime(s.departure))
|
||||||
|
if (s.arrivalDelay !== null && s.arrivalDelay !== undefined) {
|
||||||
|
t.equal(typeof s.arrivalDelay, 'number')
|
||||||
|
}
|
||||||
|
if (s.departureDelay !== null && s.departureDelay !== undefined) {
|
||||||
|
t.equal(typeof s.departureDelay, 'number')
|
||||||
|
}
|
||||||
|
if (!('arrival' in s) && !('departure' in s)) {
|
||||||
|
t.fail('stopover doesn\'t contain arrival or departure')
|
||||||
|
}
|
||||||
|
t.ok(s.station)
|
||||||
|
assertValidStation(t, s.station, coordsOptional)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hour = 60 * 60 * 1000
|
||||||
|
const week = 7 * 24 * hour
|
||||||
|
|
||||||
|
// next Monday 10 am
|
||||||
|
const when = DateTime.fromMillis(Date.now(), {
|
||||||
|
zone: 'Europe/Berlin',
|
||||||
|
locale: 'de-DE'
|
||||||
|
}).startOf('week').plus({weeks: 1, hours: 10}).toJSDate()
|
||||||
|
const isValidWhen = (w) => {
|
||||||
|
const ts = +new Date(w)
|
||||||
|
if (Number.isNaN(ts)) return false
|
||||||
|
return isRoughlyEqual(12 * hour, +when, ts)
|
||||||
|
}
|
||||||
|
|
||||||
|
const assertValidWhen = (t, w) => {
|
||||||
|
t.ok(isValidWhen(w), 'invalid when')
|
||||||
|
}
|
||||||
|
|
||||||
|
const assertValidTicket = (t, ti) => {
|
||||||
|
t.strictEqual(typeof ti.name, 'string')
|
||||||
|
t.ok(ti.name.length > 0)
|
||||||
|
if (ti.price !== null) {
|
||||||
|
t.strictEqual(typeof ti.price, 'number')
|
||||||
|
t.ok(ti.price > 0)
|
||||||
|
}
|
||||||
|
if (ti.amount !== null) {
|
||||||
|
t.strictEqual(typeof ti.amount, 'number')
|
||||||
|
t.ok(ti.amount > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('bike' in ti) t.strictEqual(typeof ti.bike, 'boolean')
|
||||||
|
if ('shortTrip' in ti) t.strictEqual(typeof ti.shortTrip, 'boolean')
|
||||||
|
if ('group' in ti) t.strictEqual(typeof ti.group, 'boolean')
|
||||||
|
if ('fullDay' in ti) t.strictEqual(typeof ti.fullDay, 'boolean')
|
||||||
|
|
||||||
|
if (ti.tariff !== null) {
|
||||||
|
t.strictEqual(typeof ti.tariff, 'string')
|
||||||
|
t.ok(ti.tariff.length > 0)
|
||||||
|
}
|
||||||
|
if (ti.coverage !== null) {
|
||||||
|
t.strictEqual(typeof ti.coverage, 'string')
|
||||||
|
t.ok(ti.coverage.length > 0)
|
||||||
|
}
|
||||||
|
if (ti.variant !== null) {
|
||||||
|
t.strictEqual(typeof ti.variant, 'string')
|
||||||
|
t.ok(ti.variant.length > 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
assertValidStation,
|
||||||
|
assertValidPoi,
|
||||||
|
assertValidAddress,
|
||||||
|
assertValidLocation,
|
||||||
|
assertValidLine,
|
||||||
|
isValidDateTime,
|
||||||
|
assertValidStopover,
|
||||||
|
hour, when, isValidWhen, assertValidWhen,
|
||||||
|
assertValidTicket
|
||||||
|
}
|
387
test/vbb.js
Normal file
387
test/vbb.js
Normal file
|
@ -0,0 +1,387 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const a = require('assert')
|
||||||
|
const isRoughlyEqual = require('is-roughly-equal')
|
||||||
|
const stations = require('vbb-stations-autocomplete')
|
||||||
|
const tapePromise = require('tape-promise').default
|
||||||
|
const tape = require('tape')
|
||||||
|
const co = require('co')
|
||||||
|
const shorten = require('vbb-short-station-name')
|
||||||
|
|
||||||
|
const createClient = require('..')
|
||||||
|
const vbbProfile = require('../p/vbb')
|
||||||
|
const {
|
||||||
|
assertValidStation: _assertValidStation,
|
||||||
|
assertValidPoi,
|
||||||
|
assertValidAddress,
|
||||||
|
assertValidLocation,
|
||||||
|
assertValidLine: _assertValidLine,
|
||||||
|
assertValidStopover,
|
||||||
|
hour, when,
|
||||||
|
assertValidWhen,
|
||||||
|
assertValidTicket
|
||||||
|
} = require('./util')
|
||||||
|
|
||||||
|
const assertValidStation = (t, s, coordsOptional = false) => {
|
||||||
|
_assertValidStation(t, s, coordsOptional)
|
||||||
|
t.equal(s.name, shorten(s.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
const assertValidStationProducts = (t, p) => {
|
||||||
|
t.ok(p)
|
||||||
|
t.equal(typeof p.suburban, 'boolean')
|
||||||
|
t.equal(typeof p.subway, 'boolean')
|
||||||
|
t.equal(typeof p.tram, 'boolean')
|
||||||
|
t.equal(typeof p.bus, 'boolean')
|
||||||
|
t.equal(typeof p.ferry, 'boolean')
|
||||||
|
t.equal(typeof p.express, 'boolean')
|
||||||
|
t.equal(typeof p.regional, 'boolean')
|
||||||
|
}
|
||||||
|
|
||||||
|
const assertValidLine = (t, l) => {
|
||||||
|
_assertValidLine(t, l)
|
||||||
|
if (l.symbol !== null) t.equal(typeof l.symbol, 'string')
|
||||||
|
if (l.nr !== null) t.equal(typeof l.nr, 'number')
|
||||||
|
if (l.metro !== null) t.equal(typeof l.metro, 'boolean')
|
||||||
|
if (l.express !== null) t.equal(typeof l.express, 'boolean')
|
||||||
|
if (l.night !== null) t.equal(typeof l.night, 'boolean')
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo
|
||||||
|
const findStation = (query) => stations(query, true, false)
|
||||||
|
|
||||||
|
const test = tapePromise(tape)
|
||||||
|
const client = createClient(vbbProfile)
|
||||||
|
|
||||||
|
const amrumerStr = '900000009101'
|
||||||
|
const spichernstr = '900000042101'
|
||||||
|
const bismarckstr = '900000024201'
|
||||||
|
|
||||||
|
test('journeys – station to station', co.wrap(function* (t) {
|
||||||
|
const journeys = yield client.journeys(spichernstr, amrumerStr, {
|
||||||
|
results: 3, when, passedStations: true
|
||||||
|
})
|
||||||
|
|
||||||
|
t.ok(Array.isArray(journeys))
|
||||||
|
t.strictEqual(journeys.length, 3)
|
||||||
|
|
||||||
|
for (let journey of journeys) {
|
||||||
|
assertValidStation(t, journey.origin)
|
||||||
|
assertValidStationProducts(t, journey.origin.products)
|
||||||
|
t.ok(journey.origin.name.indexOf('(Berlin)') === -1)
|
||||||
|
t.strictEqual(journey.origin.id, spichernstr)
|
||||||
|
assertValidWhen(t, journey.departure)
|
||||||
|
|
||||||
|
assertValidStation(t, journey.destination)
|
||||||
|
assertValidStationProducts(t, journey.destination.products)
|
||||||
|
t.strictEqual(journey.destination.id, amrumerStr)
|
||||||
|
assertValidWhen(t, journey.arrival)
|
||||||
|
|
||||||
|
t.ok(Array.isArray(journey.legs))
|
||||||
|
t.strictEqual(journey.legs.length, 1)
|
||||||
|
const leg = journey.legs[0]
|
||||||
|
|
||||||
|
t.equal(typeof leg.id, 'string')
|
||||||
|
t.ok(leg.id)
|
||||||
|
assertValidStation(t, leg.origin)
|
||||||
|
assertValidStationProducts(t, leg.origin.products)
|
||||||
|
t.ok(leg.origin.name.indexOf('(Berlin)') === -1)
|
||||||
|
t.strictEqual(leg.origin.id, spichernstr)
|
||||||
|
assertValidWhen(t, leg.departure)
|
||||||
|
|
||||||
|
assertValidStation(t, leg.destination)
|
||||||
|
assertValidStationProducts(t, leg.destination.products)
|
||||||
|
t.strictEqual(leg.destination.id, amrumerStr)
|
||||||
|
assertValidWhen(t, leg.arrival)
|
||||||
|
|
||||||
|
assertValidLine(t, leg.line)
|
||||||
|
t.ok(findStation(leg.direction))
|
||||||
|
t.ok(leg.direction.indexOf('(Berlin)') === -1)
|
||||||
|
|
||||||
|
t.ok(Array.isArray(leg.passed))
|
||||||
|
for (let passed of leg.passed) assertValidStopover(t, passed)
|
||||||
|
|
||||||
|
// todo: find a journey where there ticket info is always available
|
||||||
|
if (journey.tickets) {
|
||||||
|
t.ok(Array.isArray(journey.tickets))
|
||||||
|
for (let ticket of journey.tickets) assertValidTicket(t, ticket)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.end()
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('journeys – only subway', co.wrap(function* (t) {
|
||||||
|
const journeys = yield client.journeys(spichernstr, bismarckstr, {
|
||||||
|
results: 20, when,
|
||||||
|
products: {
|
||||||
|
suburban: false,
|
||||||
|
subway: true,
|
||||||
|
tram: false,
|
||||||
|
bus: false,
|
||||||
|
ferry: false,
|
||||||
|
express: false,
|
||||||
|
regional: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.ok(Array.isArray(journeys))
|
||||||
|
t.ok(journeys.length > 1)
|
||||||
|
|
||||||
|
for (let journey of journeys) {
|
||||||
|
for (let leg of journey.legs) {
|
||||||
|
if (leg.line) {
|
||||||
|
assertValidLine(t, leg.line)
|
||||||
|
t.equal(leg.line.mode, 'train')
|
||||||
|
t.equal(leg.line.product, 'subway')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.end()
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('journeys – fails with no product', co.wrap(function* (t) {
|
||||||
|
try {
|
||||||
|
yield client.journeys(spichernstr, bismarckstr, {
|
||||||
|
when,
|
||||||
|
products: {
|
||||||
|
suburban: false,
|
||||||
|
subway: false,
|
||||||
|
tram: false,
|
||||||
|
bus: false,
|
||||||
|
ferry: false,
|
||||||
|
express: false,
|
||||||
|
regional: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
t.ok(err, 'error thrown')
|
||||||
|
t.end()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('journey leg details', co.wrap(function* (t) {
|
||||||
|
const journeys = yield client.journeys(spichernstr, amrumerStr, {
|
||||||
|
results: 1, when
|
||||||
|
})
|
||||||
|
|
||||||
|
const p = journeys[0].legs[0]
|
||||||
|
t.ok(p.id, 'precondition failed')
|
||||||
|
t.ok(p.line.name, 'precondition failed')
|
||||||
|
const leg = yield client.journeyLeg(p.id, p.line.name, {when})
|
||||||
|
|
||||||
|
t.equal(typeof leg.id, 'string')
|
||||||
|
t.ok(leg.id)
|
||||||
|
|
||||||
|
assertValidLine(t, leg.line)
|
||||||
|
|
||||||
|
t.equal(typeof leg.direction, 'string')
|
||||||
|
t.ok(leg.direction)
|
||||||
|
|
||||||
|
t.ok(Array.isArray(leg.passed))
|
||||||
|
for (let passed of leg.passed) assertValidStopover(t, passed)
|
||||||
|
|
||||||
|
t.end()
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
test('journeys – station to address', co.wrap(function* (t) {
|
||||||
|
const journeys = yield client.journeys(spichernstr, {
|
||||||
|
type: 'location', address: 'Torfstraße 17',
|
||||||
|
latitude: 52.5416823, longitude: 13.3491223
|
||||||
|
}, {results: 1, when})
|
||||||
|
|
||||||
|
t.ok(Array.isArray(journeys))
|
||||||
|
t.strictEqual(journeys.length, 1)
|
||||||
|
const journey = journeys[0]
|
||||||
|
const leg = journey.legs[journey.legs.length - 1]
|
||||||
|
|
||||||
|
assertValidStation(t, leg.origin)
|
||||||
|
assertValidStationProducts(t, leg.origin.products)
|
||||||
|
assertValidWhen(t, leg.departure)
|
||||||
|
|
||||||
|
const dest = leg.destination
|
||||||
|
assertValidAddress(t, dest)
|
||||||
|
t.strictEqual(dest.address, 'Torfstraße 17')
|
||||||
|
t.ok(isRoughlyEqual(.0001, dest.latitude, 52.5416823))
|
||||||
|
t.ok(isRoughlyEqual(.0001, dest.longitude, 13.3491223))
|
||||||
|
assertValidWhen(t, leg.arrival)
|
||||||
|
|
||||||
|
t.end()
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
test('journeys – station to POI', co.wrap(function* (t) {
|
||||||
|
const journeys = yield client.journeys(spichernstr, {
|
||||||
|
type: 'location', id: '9980720', name: 'ATZE Musiktheater',
|
||||||
|
latitude: 52.543333, longitude: 13.351686
|
||||||
|
}, {results: 1, when})
|
||||||
|
|
||||||
|
t.ok(Array.isArray(journeys))
|
||||||
|
t.strictEqual(journeys.length, 1)
|
||||||
|
const journey = journeys[0]
|
||||||
|
const leg = journey.legs[journey.legs.length - 1]
|
||||||
|
|
||||||
|
assertValidStation(t, leg.origin)
|
||||||
|
assertValidStationProducts(t, leg.origin.products)
|
||||||
|
assertValidWhen(t, leg.departure)
|
||||||
|
|
||||||
|
const dest = leg.destination
|
||||||
|
assertValidPoi(t, dest)
|
||||||
|
t.strictEqual(dest.name, 'ATZE Musiktheater')
|
||||||
|
t.ok(isRoughlyEqual(.0001, dest.latitude, 52.543333))
|
||||||
|
t.ok(isRoughlyEqual(.0001, dest.longitude, 13.351686))
|
||||||
|
assertValidWhen(t, leg.arrival)
|
||||||
|
|
||||||
|
t.end()
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
test('departures', co.wrap(function* (t) {
|
||||||
|
const deps = yield client.departures(spichernstr, {duration: 5, when})
|
||||||
|
|
||||||
|
t.ok(Array.isArray(deps))
|
||||||
|
t.deepEqual(deps, deps.sort((a, b) => t.when > b.when))
|
||||||
|
for (let dep of deps) {
|
||||||
|
t.equal(typeof dep.journeyId, 'string')
|
||||||
|
t.ok(dep.journeyId)
|
||||||
|
|
||||||
|
t.equal(dep.station.name, 'U Spichernstr.')
|
||||||
|
assertValidStation(t, dep.station)
|
||||||
|
assertValidStationProducts(t, dep.station.products)
|
||||||
|
t.strictEqual(dep.station.id, spichernstr)
|
||||||
|
|
||||||
|
assertValidWhen(t, dep.when)
|
||||||
|
t.ok(findStation(dep.direction))
|
||||||
|
assertValidLine(t, dep.line)
|
||||||
|
}
|
||||||
|
t.end()
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('departures with station object', co.wrap(function* (t) {
|
||||||
|
yield client.departures({
|
||||||
|
type: 'station',
|
||||||
|
id: spichernstr,
|
||||||
|
name: 'U Spichernstr',
|
||||||
|
location: {
|
||||||
|
type: 'location',
|
||||||
|
latitude: 1.23,
|
||||||
|
longitude: 2.34
|
||||||
|
}
|
||||||
|
}, {when})
|
||||||
|
|
||||||
|
t.ok('did not fail')
|
||||||
|
t.end()
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('departures at 7-digit station', co.wrap(function* (t) {
|
||||||
|
const eisenach = '8010097' // see derhuerst/vbb-hafas#22
|
||||||
|
yield client.departures(eisenach, {when})
|
||||||
|
t.pass('did not fail')
|
||||||
|
|
||||||
|
t.end()
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
test('nearby', co.wrap(function* (t) {
|
||||||
|
// Berliner Str./Bundesallee
|
||||||
|
const nearby = yield client.nearby({
|
||||||
|
type: 'location',
|
||||||
|
latitude: 52.4873452,
|
||||||
|
longitude: 13.3310411
|
||||||
|
}, {distance: 200})
|
||||||
|
|
||||||
|
t.ok(Array.isArray(nearby))
|
||||||
|
for (let n of nearby) {
|
||||||
|
if (n.type === 'station') assertValidStation(t, n)
|
||||||
|
else assertValidLocation(t, n, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.equal(nearby[0].id, '900000044201')
|
||||||
|
t.equal(nearby[0].name, 'U Berliner Str.')
|
||||||
|
t.ok(nearby[0].distance > 0)
|
||||||
|
t.ok(nearby[0].distance < 100)
|
||||||
|
|
||||||
|
t.equal(nearby[1].id, '900000043252')
|
||||||
|
t.equal(nearby[1].name, 'Landhausstr.')
|
||||||
|
t.ok(nearby[1].distance > 100)
|
||||||
|
t.ok(nearby[1].distance < 200)
|
||||||
|
|
||||||
|
t.end()
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
test('locations', co.wrap(function* (t) {
|
||||||
|
const locations = yield client.locations('Alexanderplatz', {results: 10})
|
||||||
|
|
||||||
|
t.ok(Array.isArray(locations))
|
||||||
|
t.ok(locations.length > 0)
|
||||||
|
t.ok(locations.length <= 10)
|
||||||
|
for (let l of locations) {
|
||||||
|
if (l.type === 'station') assertValidStation(t, l)
|
||||||
|
else assertValidLocation(t, l)
|
||||||
|
}
|
||||||
|
t.ok(locations.find(s => s.type === 'station'))
|
||||||
|
t.ok(locations.find(s => s.id && s.name)) // POIs
|
||||||
|
t.ok(locations.find(s => !s.name && s.address)) // addresses
|
||||||
|
|
||||||
|
t.end()
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
test('radar', co.wrap(function* (t) {
|
||||||
|
const vehicles = yield client.radar(52.52411, 13.41002, 52.51942, 13.41709, {
|
||||||
|
duration: 5 * 60, when
|
||||||
|
})
|
||||||
|
|
||||||
|
t.ok(Array.isArray(vehicles))
|
||||||
|
t.ok(vehicles.length > 0)
|
||||||
|
for (let v of vehicles) {
|
||||||
|
|
||||||
|
t.ok(findStation(v.direction))
|
||||||
|
assertValidLine(t, v.line)
|
||||||
|
|
||||||
|
t.equal(typeof v.location.latitude, 'number')
|
||||||
|
t.ok(v.location.latitude <= 55, 'vehicle is too far away')
|
||||||
|
t.ok(v.location.latitude >= 45, 'vehicle is too far away')
|
||||||
|
t.equal(typeof v.location.longitude, 'number')
|
||||||
|
t.ok(v.location.longitude >= 9, 'vehicle is too far away')
|
||||||
|
t.ok(v.location.longitude <= 15, 'vehicle is too far away')
|
||||||
|
|
||||||
|
t.ok(Array.isArray(v.nextStops))
|
||||||
|
for (let st of v.nextStops) {
|
||||||
|
assertValidStopover(t, st, true)
|
||||||
|
t.strictEqual(st.station.name.indexOf('(Berlin)'), -1)
|
||||||
|
|
||||||
|
if (st.arrival) {
|
||||||
|
t.equal(typeof st.arrival, 'string')
|
||||||
|
const arr = +new Date(st.arrival)
|
||||||
|
// note that this can be an ICE train
|
||||||
|
t.ok(isRoughlyEqual(14 * hour, +when, arr))
|
||||||
|
}
|
||||||
|
if (st.departure) {
|
||||||
|
t.equal(typeof st.departure, 'string')
|
||||||
|
const dep = +new Date(st.departure)
|
||||||
|
// note that this can be an ICE train
|
||||||
|
t.ok(isRoughlyEqual(14 * hour, +when, dep))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.ok(Array.isArray(v.frames))
|
||||||
|
for (let f of v.frames) {
|
||||||
|
assertValidStation(t, f.origin, true)
|
||||||
|
assertValidStationProducts(t, f.origin.products)
|
||||||
|
t.strictEqual(f.origin.name.indexOf('(Berlin)'), -1)
|
||||||
|
assertValidStation(t, f.destination, true)
|
||||||
|
assertValidStationProducts(t, f.destination.products)
|
||||||
|
t.strictEqual(f.destination.name.indexOf('(Berlin)'), -1)
|
||||||
|
t.equal(typeof f.t, 'number')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.end()
|
||||||
|
}))
|
Loading…
Add table
Reference in a new issue