merge next into master

This commit is contained in:
Jannis R 2018-08-17 21:52:40 +02:00
commit abf3661edc
No known key found for this signature in database
GPG key ID: 0FE83946296A88A5
100 changed files with 5025 additions and 3092 deletions

2
.gitignore vendored
View file

@ -5,4 +5,6 @@ Thumbs.db
node_modules
npm-debug.log
package-lock.json
/id.json

3
docs/arrivals.md Normal file
View file

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

View file

@ -1,5 +1,48 @@
# Changelog
## `3.0.0`
This version is not fully backwords-compatible. Check out [the migration guide](migrating-to-3.md).
### new features ✨
- 2d3796a BVG profile
- 0db84ce #61 parse remarks for stopovers and journey legs
- ac9819b `arrivals()` method  [docs](arrivals.md)
- 5b754aa `refreshJourney()` method  [docs](refresh-journey.md)
- 21c273c `journeys()`/`trip()`: leg stopovers: parse & expose delays
- 021ae45 `journeys()`/`trip()`: leg stopovers: parse & expose platforms
- 84bce0c `arrivals()`/`departures()`: parse & expose platforms
- 85e0bdf `journeys()`: `startWithWalking` option with default `true`
- f6ae29c journey legs with `type: 'walking'` now have a `distance` in meters
- 0d5a8fa departures, arrivals, stopovers: former scheduled platform(s)
- 0199749 `language` option with default `en`
- 1551943 `arrivals()`/`departures()`: `includeRelatedStations` option with default `true`
### breaking changes 💥
- c4935bc new mandatory `User-Agent` parameter
- b7c1ee3 profiles: new products markup ([guide](https://github.com/public-transport/hafas-client/blob/ebe4fa64d871f711ced99d528c0171b180edc135/docs/writing-a-profile.md#3-products))
- 40b559f change `radar(n, w, s, e)` signature to `radar({north, west, south, east})`
- 005f3f8 remove `journey.departure`, `journey.arrival`, …
- 0ef0301 validate `opt.when`
- 431574b parse polylines using `profile.parsePolyLine` [docs for the output format](https://github.com/public-transport/hafas-client/blob/ebe4fa64d871f711ced99d528c0171b180edc135/docs/journey-leg.md#polyline-option)
- a356a26 throw if 0 products enabled
- c82ad23 `journeys()`: `opt.when``opt.departure`/`opt.arrival`
- 665bed9 rename `location(id)` to `station(id)`
- 6611f26 `journeys()`/`trip()`: `leg.passed``leg.stopovers`
- ebe4fa6 `journeys()`/`trip()`: `opt.passedStations``opt.stopovers`
- 3e672ee `journeys()`/`trip()`: `stopover.station``stopover.stop`
- 2e6aefe journey leg, departure, movement: `journeyId` -> `tripId`
- 8881d8a & b6fbaa5: change parsers signature to `parse…(profile, opt, data)`
- cabe5fa: option to parse & expose `station.lines`, default off
- c8ff217 rename `journeyLeg()` to `trip()`
- 8de4447 rename `profile.journeyLeg` to `profile.trip`
### bugfixes
- dd0a9b2 `parseStopover`: fix first/last canceled stopovers 🐛
## `2.10.3`
- 50bd440 better `User-Agent` randomization

View file

@ -23,17 +23,24 @@ With `opt`, you can override the default options, which look like this:
```js
{
// todo: products
when: new Date(),
direction: null, // only show departures heading to this station
duration: 10 // show departures for the next n minutes
duration: 10, // show departures for the next n minutes
stationLines: false, // parse & expose lines of the station?
remarks: true, // parse & expose hints & warnings?
// departures at related stations
// e.g. those that belong together on the metro map.
includeRelatedStations: true,
language: 'en' // language to get results in
}
```
## 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.
*Note:* As stated in the [*Friendly Public Transport Format* `1.1.1`](https://github.com/public-transport/friendly-public-transport-format/tree/1.1.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.
You may pass the `tripId` field into [`trip(id, lineName, [opt])`](trip.md) to get details on the vehicle's trip.
As an example, we're going to use the [VBB profile](../p/vbb):
@ -41,7 +48,7 @@ As an example, we're going to use the [VBB profile](../p/vbb):
const createClient = require('hafas-client')
const vbbProfile = require('hafas-client/p/vbb')
const client = createClient(vbbProfile)
const client = createClient(vbbProfile, 'my-awesome-program')
// S Charlottenburg
client.departures('900000024101', {duration: 3})
@ -53,9 +60,8 @@ The response may look like this:
```js
[ {
journeyId: '1|31431|28|86|17122017',
trip: 31431,
station: {
tripId: '1|31431|28|86|17122017',
stop: {
type: 'station',
id: '900000024101',
name: 'S Charlottenburg',
@ -80,10 +86,10 @@ The response may look like this:
line: {
type: 'line',
id: '18299',
name: 'S9',
public: true,
mode: 'train',
product: 'suburban',
public: true,
name: 'S9',
symbol: 'S',
nr: 9,
metro: false,
@ -96,21 +102,21 @@ The response may look like this:
name: 'S-Bahn Berlin GmbH'
}
},
direction: 'S Spandau'
direction: 'S Spandau',
trip: 31431
}, {
journeyId: '1|30977|8|86|17122017',
trip: 30977,
station: { /* … */ },
tripId: '1|30977|8|86|17122017',
stop: { /* … */ },
when: null,
delay: null,
cancelled: true,
line: {
type: 'line',
id: '16441',
name: 'S5',
public: true,
mode: 'train',
product: 'suburban',
public: true,
name: 'S5',
symbol: 'S',
nr: 5,
metro: false,
@ -119,39 +125,22 @@ The response may look like this:
productCode: 0,
operator: { /* … */ }
},
direction: 'S Westkreuz'
direction: 'S Westkreuz',
trip: 30977
}, {
journeyId: '1|28671|4|86|17122017',
tripId: '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
}
},
stop: { /* … */ },
when: '2017-12-17T19:35:00.000+01:00',
delay: 0,
platform: null,
line: {
type: 'line',
id: '19494',
name: 'U7',
public: true,
mode: 'train',
product: 'subway',
public: true,
name: 'U7',
symbol: 'U',
nr: 7,
metro: false,

View file

@ -1,121 +0,0 @@
# `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?
polyline: false // return a shape for the leg?
}
```
## 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: [ /* … */ ]
}
```
If you pass `polyline: true`, the leg will have a `polyline` field, containing an encoded shape. You can use e.g. [`@mapbox/polyline`](https://www.npmjs.com/package/@mapbox/polyline) to decode it.

View file

@ -40,13 +40,15 @@ With `opt`, you can override the default options, which look like this:
```js
{
when: new Date(),
whenRepresents: 'departure', // use 'arrival' for journeys arriving before `when`
// Use either `departure` or `arrival` to specify a date/time.
departure: new Date(),
arrival: null,
earlierThan: null, // ref to get journeys earlier than the last query
laterThan: null, // ref to get journeys later than the last query
results: 5, // how many journeys?
via: null, // let journeys pass this station
passedStations: false, // return stations on the way?
stopovers: 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'
@ -63,14 +65,16 @@ With `opt`, you can override the default options, which look like this:
},
tickets: false, // return tickets? only available with some profiles
polylines: false, // return a shape for each leg?
remarks: true, // parse & expose hints & warnings?
// Consider walking to nearby stations at the beginning of a journey?
startWithWalking: false
startWithWalking: true,
language: 'en' // language to get results in
}
```
## 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.
*Note:* As stated in the [*Friendly Public Transport Format* `1.1.1`](https://github.com/public-transport/friendly-public-transport-format/tree/1.1.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):
@ -78,12 +82,12 @@ As an example, we're going to use the [VBB profile](../p/vbb):
const createClient = require('hafas-client')
const vbbProfile = require('hafas-client/p/vbb')
const client = createClient(vbbProfile)
const client = createClient(vbbProfile, 'my-awesome-program')
// Hauptbahnhof to Heinrich-Heine-Str.
client.journeys('900000003201', '900000100008', {
results: 1,
passedStations: true
stopovers: true
})
.then(console.log)
.catch(console.error)
@ -95,7 +99,7 @@ The response may look like this:
[
{
legs: [ {
id: '1|31041|35|86|17122017',
id: '1|32615|6|86|10072018',
origin: {
type: 'station',
id: '900000003201',
@ -115,30 +119,23 @@ The response may look like this:
regional: true
}
},
departure: '2017-12-17T19:07:00.000+01:00',
departurePlatform: '16',
destination: {
type: 'station',
id: '900000024101',
name: 'S Charlottenburg',
id: '900000100004',
name: 'S+U Jannowitzbrücke',
location: {
type: 'location',
latitude: 52.504806,
longitude: 13.303846
},
products: {
suburban: true,
subway: false,
tram: false,
bus: true,
ferry: false,
express: false,
regional: true
}
products: { /* … */ }
},
arrival: '2017-12-17T19:47:00.000+01:00',
arrivalPlatform: '8',
arrivalDelay: 30,
departure: '2018-07-10T23:54:00.000+02:00',
departureDelay: 60,
departurePlatform: '15',
arrival: '2018-07-11T00:02:00.000+02:00',
arrivalDelay: 60,
arrivalPlatform: '3',
line: {
type: 'line',
id: '16845',
@ -146,21 +143,21 @@ The response may look like this:
public: true,
mode: 'train',
product: 'suburban',
operator: {
type: 'operator',
id: 's-bahn-berlin-gmbh',
name: 'S-Bahn Berlin GmbH'
},
symbol: 'S',
nr: 7,
metro: false,
express: false,
night: false,
productCode: 0,
operator: {
type: 'operator',
id: 's-bahn-berlin-gmbh',
name: 'S-Bahn Berlin GmbH'
}
productCode: 0
},
direction: 'S Potsdam Hauptbahnhof',
passed: [ {
station: {
direction: 'S Ahrensfelde',
stopovers: [ {
stop: {
type: 'station',
id: '900000003201',
name: 'S+U Berlin Hauptbahnhof',
@ -169,46 +166,64 @@ The response may look like this:
},
arrival: null,
departure: null,
cancelled: true
cancelled: true,
remarks: [
{type: 'hint', code: 'bf', text: 'barrier-free'},
{type: 'hint', code: 'FB', text: 'Bicycle conveyance'}
]
}, {
station: {
stop: {
type: 'station',
id: '900000003102',
name: 'S Bellevue',
id: '900000100001',
name: 'S+U Friedrichstr.',
location: { /* … */ },
products: { /* … */ }
},
arrival: '2017-12-17T19:09:00.000+01:00',
departure: '2017-12-17T19:09:00.000+01:00'
}, /* … */ {
station: {
arrival: '2018-07-10T23:56:00.000+02:00',
arrivalDelay: 60,
arrivalPlatform: null,
departure: '2018-07-10T23:57:00.000+02:00',
departureDelay: 60,
departurePlatform: null,
remarks: [ /* … */ ]
},
/* … */
{
type: 'station',
id: '900000024101',
name: 'S Charlottenburg',
id: '900000100004',
name: 'S+U Jannowitzbrücke',
location: { /* … */ },
products: { /* … */ }
},
arrival: '2017-12-17T19:17:00.000+01:00',
departure: '2017-12-17T19:17:00.000+01:00'
arrival: '2018-07-11T00:02:00.000+02:00',
arrivalDelay: 60,
arrivalPlatform: null,
departure: '2018-07-11T00:02:00.000+02:00',
departureDelay: null,
departurePlatform: null,
remarks: [ /* … */ ]
} ]
} ],
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
}, {
origin: {
type: 'station',
id: '900000100004',
name: 'S+U Jannowitzbrücke',
location: { /* … */ },
products: { /* … */ }
},
destination: {
type: 'station',
id: '900000100008',
name: 'U Heinrich-Heine-Str.',
location: { /* … */ },
products: { /* … */ }
},
departure: '2018-07-11T00:01:00.000+02:00',
arrival: '2018-07-11T00:10:00.000+02:00',
mode: 'walking',
public: true,
distance: 558
} ]
},
earlierRef: '…', // use with the `earlierThan` option
laterRef: '…' // use with the `laterThan` option
@ -263,16 +278,16 @@ const heinrichHeineStr = '900000100008'
client.journeys(hbf, heinrichHeineStr)
.then((journeys) => {
const lastJourney = journeys[journeys.length - 1]
console.log('departure of last journey', lastJourney.departure)
console.log('departure of last journey', lastJourney.legs[0].departure)
// get later journeys
return client.journeys(hbf, heinrichHeineStr, {
laterThan: journeys.laterRef
})
})
.then((laterourneys) => {
const firstJourney = laterourneys[laterourneys.length - 1]
console.log('departure of first (later) journey', firstJourney.departure)
.then((laterJourneys) => {
const firstJourney = laterJourneys[laterJourneys.length - 1]
console.log('departure of first (later) journey', firstJourney.legs[0].departure)
})
.catch(console.error)
```
@ -282,4 +297,4 @@ departure of last journey 2017-12-17T19:07:00.000+01:00
departure of first (later) journey 2017-12-17T19:19:00.000+01:00
```
If you pass `polylines: true`, each journey leg will have a `polyline` field, containing an encoded shape. You can use e.g. [`@mapbox/polyline`](https://www.npmjs.com/package/@mapbox/polyline) to decode it.
If you pass `polylines: true`, each journey leg will have a `polyline` field. Refer to [the section in the `trip()` docs](trip.md#polyline-option) for details.

View file

@ -11,6 +11,8 @@ With `opt`, you can override the default options, which look like this:
, stations: true
, addresses: true
, poi: true // points of interest
, stationLines: false // parse & expose lines of the station?
, language: 'en' // language to get results in
}
```
@ -22,7 +24,7 @@ As an example, we're going to use the [VBB profile](../p/vbb):
const createClient = require('hafas-client')
const vbbProfile = require('hafas-client/p/vbb')
const client = createClient(vbbProfile)
const client = createClient(vbbProfile, 'my-awesome-program')
client.locations('Alexanderplatz', {results: 3})
.then(console.log)
@ -33,7 +35,7 @@ The response may look like this:
```js
[ {
type: 'station',
type: 'stop',
id: '900000100003',
name: 'S+U Alexanderplatz',
location: {
@ -52,14 +54,14 @@ The response may look like this:
}
}, { // point of interest
type: 'location',
name: 'Berlin, Holiday Inn Centre Alexanderplatz****',
id: '900980709',
name: 'Berlin, Holiday Inn Centre Alexanderplatz****',
latitude: 52.523549,
longitude: 13.418441
}, { // point of interest
type: 'location',
name: 'Berlin, Hotel Agon am Alexanderplatz',
id: '900980176',
name: 'Berlin, Hotel Agon am Alexanderplatz',
latitude: 52.524556,
longitude: 13.420266
} ]

71
docs/migrating-to-3.md Normal file
View file

@ -0,0 +1,71 @@
# Migrating to `hafas-client@3`
## New `User-Agent` parameter
Pass an additional `User-Agent` string into `createClient`:
```js
const createClient = require('hafas-client')
const dbProfile = require('hafas-client/p/db')
const client = createClient(dbProfile, 'my-awesome-program')
```
Pick a name that describes your program and if possible the website/repo of it.
## If you use the `journeyLeg()` method…
…change the `journeyLeg(id, lineName)` call to `trip(id, lineName)`. c8ff217
## If you use the `journeys()` or `trip()` methods…
- …instead of `journey.departure`, use `journey.legs[0].departure`. 005f3f8
- …instead of `journey.arrival`, use `journey.legs[last].arrival`. 005f3f8
- …rename `opt.passedStations` to `opt.stopovers`. ebe4fa6
- …rename `leg.journeyId` to `leg.tripId`. 2e6aefe
- …rename `leg.passed` to `leg.stopovers`. 6611f26
- …rename `leg.stopovers[].station` to `leg.stopovers[].stop`. 3e672ee
## If you use the `journeys()` method and `opt.when`
…use `opt.departure` instead. Use `opt.arrival` to get journeys arriving before the specified date+time. This replaces the `opt.when` & `opt.whenRepresents` options from `hafas-client@2`. c82ad23
## If you use the `journeys()` and `opt.polylines` or `trip()` and `opt.polyline`
`leg.polyline` will be [parsed for you now](https://github.com/public-transport/hafas-client/blob/f6c824eecb459181ea90ddf41bf1a1e8b64539ec/docs/journey-leg.md#polyline-option).
## If you use the `departures()` method…
…rename `departure.journeyId` to `departure.tripId`. 2e6aefe
## If you use the `location()` method…
…change the `location(id)` call to `station(id)`. 665bed9
## If you use the `radar()` method…
- …change the `radar(north, west, south, east)` call to `radar({north, west, south, east})`. 40b559f
- …rename `movement.journeyId` to `movement.tripId`. 2e6aefe
## If you use `hafas-client` with a custom profile…
- …write your profile in [the new format](writing-a-profile.md). Then, you can pass it into `hafas-client` just like before. #32/b7c1ee3
- …rename the `profile.journeyLeg` flag to `profile.trip`. 8de4447
## If you use `hafas-client` with custom parse functions…
…change the following parsers to the `parse…(profile, opt, data)` signature. 8881d8a/b6fbaa5
- `parseDeparture`
- `parseJourney`
- `parseJourneyLeg`
- `parseLine`
- `parseMovement`
- `parseLocation`
- `parseNearby`
- `parsePolyline`
- `parseStopover`
## If you use `station.lines` array anywhere…
…add the `stationLines: true` option to the method call, e.g. `hafas.departures('123', {stationLines: true}). cabe5fa

View file

@ -2,7 +2,7 @@
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).
`location` must be an [*FPTF* `location` object](https://github.com/public-transport/friendly-public-transport-format/blob/1.1.1/spec/readme.md#location-objects).
With `opt`, you can override the default options, which look like this:
@ -11,6 +11,8 @@ With `opt`, you can override the default options, which look like this:
distance: null, // maximum walking distance in meters
poi: false, // return points of interest?
stations: true, // return stations?
stationLines: false, // parse & expose lines of the station?
language: 'en' // language to get results in
}
```
@ -22,7 +24,7 @@ As an example, we're going to use the [VBB profile](../p/vbb):
const createClient = require('hafas-client')
const vbbProfile = require('hafas-client/p/vbb')
const client = createClient(vbbProfile)
const client = createClient(vbbProfile, 'my-awesome-program')
client.nearby({
type: 'location',
@ -37,7 +39,7 @@ The response may look like this:
```js
[ {
type: 'station',
type: 'stop',
id: '900000120001',
name: 'S+U Frankfurter Allee',
location: {
@ -56,7 +58,7 @@ The response may look like this:
},
distance: 56
}, {
type: 'station',
type: 'stop',
id: '900000120540',
name: 'Scharnweberstr./Weichselstr.',
location: {
@ -67,7 +69,7 @@ The response may look like this:
products: { /* … */ },
distance: 330
}, {
type: 'station',
type: 'stop',
id: '900000160544',
name: 'Rathaus Lichtenberg',
location: {

View file

@ -0,0 +1,44 @@
'use strict'
// see the ./writing-a-profile.md guide
const products = [
{
id: 'nationalExp',
mode: 'train',
bitmasks: [1],
name: 'InterCityExpress',
short: 'ICE',
default: true
},
{
id: 'national',
mode: 'train',
bitmasks: [2],
name: 'InterCity & EuroCity',
short: 'IC/EC',
default: true
}
]
const transformReqBody = (body) => {
// get these from the recorded app requests
// body.client = { … }
// body.ver = …
// body.auth = { … }
// body.lang = …
return body
}
const insaProfile = {
// locale: …,
// timezone: …,
// endpoint: …,
transformReqBody,
products: products,
trip: false,
radar: false
}
module.exports = insaProfile

View file

@ -1,4 +1,4 @@
# `radar(north, west, south, east, [opt])`
# `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.
@ -11,13 +11,14 @@ With `opt`, you can override the default options, which look like this:
results: 256, // maximum number of vehicles
duration: 30, // compute frames for the next n seconds
frames: 3, // nr of frames to compute
polylines: false // return a track shape for each vehicle?
polylines: false, // return a track shape for each vehicle?
language: 'en' // language to get results in
}
```
## 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.
*Note:* As stated in the [*Friendly Public Transport Format* `1.1.1`](https://github.com/public-transport/friendly-public-transport-format/tree/1.1.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):
@ -25,9 +26,14 @@ As an example, we're going to use the [VBB profile](../p/vbb):
const createClient = require('hafas-client')
const vbbProfile = require('hafas-client/p/vbb')
const client = createClient(vbbProfile)
const client = createClient(vbbProfile, 'my-awesome-program')
client.radar(52.52411, 13.41002, 52.51942, 13.41709, {results: 5})
client.radar({
north: 52.52411,
west: 13.41002,
south: 52.51942,
east: 13.41709
}, {results: 5})
.then(console.log)
.catch(console.error)
```
@ -62,8 +68,8 @@ The response may look like this:
direction: 'S Flughafen Berlin-Schönefeld',
trip: 31463,
nextStops: [ {
station: {
type: 'station',
stop: {
type: 'stop',
id: '900000029101',
name: 'S Spandau',
location: {
@ -88,14 +94,14 @@ The response may look like this:
} /* … */ ],
frames: [ {
origin: {
type: 'station',
type: 'stop',
id: '900000100003',
name: 'S+U Alexanderplatz',
location: { /* … */ },
products: { /* … */ }
},
destination: {
type: 'station',
type: 'stop',
id: '900000100004',
name: 'S+U Jannowitzbrücke',
location: { /* … */ },
@ -134,13 +140,13 @@ The response may look like this:
direction: 'Heinersdorf',
trip: 26321,
nextStops: [ {
station: { /* S+U Alexanderplatz/Dircksenstr. */ },
stop: { /* S+U Alexanderplatz/Dircksenstr. */ },
arrival: null,
arrivalDelay: null,
departure: '2017-12-17T19:52:00.000+01:00',
departureDelay: null
}, {
station: { /* Memhardstr. */ },
stop: { /* Memhardstr. */ },
arrival: '2017-12-17T19:54:00.000+01:00',
arrivalDelay: null,
departure: '2017-12-17T19:54:00.000+01:00',
@ -158,4 +164,4 @@ The response may look like this:
}, /* … */ ]
```
If you pass `polylines: true`, each result will have a `polyline` field, containing an encoded shape. You can use e.g. [`@mapbox/polyline`](https://www.npmjs.com/package/@mapbox/polyline) to decode it.
If you pass `polylines: true`, each movement will have a `polyline` field, as documented in [the corresponding section in the `trip()` docs](trip.md#polyline-option), with the exception that station info is missing.

View file

@ -1,10 +1,12 @@
# API documentation
- [`journeys(from, to, [opt])`](journeys.md) get journeys between locations
- [`journeyLeg(ref, lineName, [opt])`](journey-leg.md) get details for a leg of a journey
- [`refreshJourney(refreshToken, [opt])`](refresh-journey.md) fetch up-to-date/more details of a `journey`
- [`trip(id, lineName, [opt])`](trip.md) get details for a trip
- [`departures(station, [opt])`](departures.md) query the next departures at a station
- [`arrivals(station, [opt])`](arrivals.md) query the next arrivals at a station
- [`locations(query, [opt])`](locations.md) find stations, POIs and addresses
- [`location(id)`](location.md) get details about a location
- [`station(id, [opt])`](station.md) get details about a station
- [`nearby(location, [opt])`](nearby.md) show stations & POIs around
- [`radar(north, west, south, east, [opt])`](radar.md) find all vehicles currently in a certain area

38
docs/refresh-journey.md Normal file
View file

@ -0,0 +1,38 @@
# `refreshJourney(refreshToken, [opt])`
`refreshToken` must be a string, taken from `journey.refreshToken`.
With `opt`, you can override the default options, which look like this:
```js
{
stopovers: false, // return stations on the way?
polylines: false, // return a shape for each leg?
tickets: false, // return tickets? only available with some profiles
remarks: true, // parse & expose hints & warnings?
language: 'en' // language to get results in
}
```
## Response
As an example, we're going to use the [VBB profile](../p/vbb):
```js
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]) => {
// later, fetch up-to-date info on the journey
client.refreshJourney(journey.refreshToken, {stopovers: true, remarks: true})
.then(console.log)
.catch(console.error)
})
.catch(console.error)
```
`refreshJourney()` will return a *single* [*Friendly Public Transport Format* `1.1.1`](https://github.com/public-transport/friendly-public-transport-format/tree/1.1.1) `journey`, in the same format as with `journeys()`.

View file

@ -1,9 +1,9 @@
# `location(station)`
# `station(id, [opt])`
`station` must be in one of these formats:
`id` must be in one of these formats:
```js
// a station ID, in a format compatible to the profile you use
// a station ID, in a format compatible with the profile you use
'900000123456'
// an FPTF `station` object
@ -19,6 +19,15 @@
}
```
With `opt`, you can override the default options, which look like this:
```js
{
stationLines: false, // parse & expose lines of the station?
language: 'en' // language to get results in
}
```
## Response
As an example, we're going to use the [VBB profile](../p/vbb):
@ -27,9 +36,9 @@ As an example, we're going to use the [VBB profile](../p/vbb):
const createClient = require('hafas-client')
const vbbProfile = require('hafas-client/p/vbb')
const client = createClient(vbbProfile)
const client = createClient(vbbProfile, 'my-awesome-program')
client.location('900000042101') // U Spichernstr.
client.station('900000042101') // U Spichernstr.
.then(console.log)
.catch(console.error)
```
@ -38,7 +47,7 @@ The response may look like this:
```js
{
type: 'station',
type: 'stop',
id: '900000042101',
name: 'U Spichernstr.',
location: {
@ -58,24 +67,26 @@ The response may look like this:
lines: [ {
type: 'line',
id: 'u1',
name: 'U1',
public: true,
class: 2,
product: 'subway',
mode: 'train',
product: 'subway',
public: true,
name: 'U1',
class: 2,
symbol: 'U',
nr: 1,
metro: false,
express: false,
night: false },
// …
{ type: 'line',
night: false
},
// …
{
type: 'line',
id: 'n9',
name: 'N9',
public: true,
class: 8,
product: 'bus',
mode: 'bus',
product: 'bus',
public: true,
name: 'N9',
class: 8,
symbol: 'N',
nr: 9,
metro: false,

187
docs/trip.md Normal file
View file

@ -0,0 +1,187 @@
# `trip(id, lineName, [opt])`
This method can be used to refetch information about a trip  a vehicle stopping at a set of stops at specific times.
*Note*: This method is not supported by every profile/endpoint.
Let's say you used [`journeys`](journeys.md) and now want to get more up-to-date data about the arrival/departure of a leg. You'd pass in the trip ID from `leg.id`, e.g. `'1|24983|22|86|18062017'`, and the name of the line from `leg.line.name` like this:
```js
const createClient = require('hafas-client')
const vbbProfile = require('hafas-client/p/vbb')
const client = createClient(vbbProfile, 'my-awesome-program')
// Hauptbahnhof to Heinrich-Heine-Str.
client.journeys('900000003201', '900000100008', {results: 1})
.then(([journey]) => {
const leg = journey.legs[0]
return client.trip(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(),
stopovers: true, // return stations on the way?
polyline: false, // return a shape for the trip?
remarks: true, // parse & expose hints & warnings?
language: 'en' // language to get results in
}
```
## Response
*Note:* As stated in the [*Friendly Public Transport Format* `1.1.1`](https://github.com/public-transport/friendly-public-transport-format/tree/1.1.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.trip('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',
stopovers: [ /* … */ ]
}
```
### `polyline` option
If you pass `polyline: true`, the trip will have a `polyline` field, containing a [GeoJSON](http://geojson.org) [`FeatureCollection`](https://tools.ietf.org/html/rfc7946#section-3.3) of [`Point`s](https://tools.ietf.org/html/rfc7946#appendix-A.1). Every `Point` next to a station will have `properties` containing the station's metadata.
We'll look at an example for *U6* from *Alt-Mariendorf* to *Alt-Tegel*, taken from the [VBB profile](../p/vbb):
```js
{
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: {
type: 'station',
id: '900000070301',
name: 'U Alt-Mariendorf',
/* … */
},
geometry: {
type: 'Point',
coordinates: [13.3875, 52.43993] // longitude, latitude
}
},
/* … */
{
type: 'Feature',
properties: {
type: 'station',
id: '900000017101',
name: 'U Mehringdamm',
/* … */
},
geometry: {
type: 'Point',
coordinates: [13.38892, 52.49448] // longitude, latitude
}
},
/* … */
{
// intermediate point, without associated station
type: 'Feature',
properties: {},
geometry: {
type: 'Point',
coordinates: [13.28599, 52.58742] // longitude, latitude
}
},
{
type: 'Feature',
properties: {
type: 'station',
id: '900000089301',
name: 'U Alt-Tegel',
/* … */
},
geometry: {
type: 'Point',
coordinates: [13.28406, 52.58915] // longitude, latitude
}
}
]
}
```

View file

@ -12,7 +12,7 @@ This guide is about writing such a profile. If you just want to use an already s
## 0. How do the profiles work?
A profile contains of three things:
A profile may consist of three things:
- **mandatory details about the HAFAS endpoint**
- `endpoint`: The protocol, host and path of the endpoint.
@ -37,8 +37,8 @@ Assuming the endpoint returns all lines names prefixed with `foo `, We can strip
// get the default line parser
const createParseLine = require('hafas-client/parse/line')
const createParseLineWithoutFoo = (profile, operators) => {
const parseLine = createParseLine(profile, operators)
const createParseLineWithoutFoo = (profile, opt, data) => {
const parseLine = createParseLine(profile, opt, data)
// wrapper function with additional logic
const parseLineWithoutFoo = (l) => {
@ -78,47 +78,50 @@ If you pass this profile into `hafas-client`, the `parseLine` method will overri
- Add a function `transformReqBody(body)` to your profile, which assigns them to `body`.
- Some profiles have a `checksum` parameter (like [here](https://gist.github.com/derhuerst/2a735268bd82a0a6779633f15dceba33#file-journey-details-1-http-L1)) or two `mic` & `mac` parameters (like [here](https://gist.github.com/derhuerst/5fa86ed5aec63645e5ae37e23e555886#file-1-http-L1)). If you see one of them in your requests, jump to [*Appendix A: checksum, mic, mac*](#appendix-a-checksum-mic-mac). Unfortunately, this is necessary to get the profile working.
You may want to use the [profile boilerplate code](profile-boilerplate.js).
## 3. Products
In `hafas-client`, there's a difference between the `mode` and the `product` field:
- The `mode` field describes the mode of transport in general. [Standardised by the *Friendly Public Transport Format* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/blob/1.0.1/spec/readme.md#modes), it is on purpose limited to a very small number of possible values, e.g. `train` or `bus`.
- The `mode` field describes the mode of transport in general. [Standardised by the *Friendly Public Transport Format* `1.1.1`](https://github.com/public-transport/friendly-public-transport-format/blob/1.1.1/spec/readme.md#modes), it is on purpose limited to a very small number of possible values, e.g. `train` or `bus`.
- The value for `product` relates to how a means of transport "works" *in local context*. Example: Even though [*S-Bahn*](https://en.wikipedia.org/wiki/Berlin_S-Bahn) and [*U-Bahn*](https://en.wikipedia.org/wiki/Berlin_U-Bahn) in Berlin are both `train`s, they have different operators, service patterns, stations and look different. Therefore, they are two distinct `product`s `subway` and `suburban`.
**Specify `product`s that appear in the app** you recorded requests of. For a fictional transit network, this may look like this:
```js
const products = {
commuterTrain: {
product: 'commuterTrain',
const products = [
{
id: 'commuterTrain',
mode: 'train',
bitmask: 1,
bitmasks: [16],
name: 'ACME Commuter Rail',
short: 'CR'
short: 'CR',
default: true
},
metro: {
product: 'metro',
{
id: 'metro',
mode: 'train',
bitmask: 2,
bitmasks: [8],
name: 'Foo Bar Metro',
short: 'M'
short: 'M',
default: true
}
}
]
```
Let's break this down:
- `product`: A sensible, [camelCased](https://en.wikipedia.org/wiki/Camel_case#Variations_and_synonyms), alphanumeric identifier. Use it for the key in the `products` object as well.
- `mode`: A [valid *Friendly Public Transport Format* `1.0.1` mode](https://github.com/public-transport/friendly-public-transport-format/blob/1.0.1/spec/readme.md#modes).
- `bitmask`: HAFAS endpoints work with a [bitmask](https://en.wikipedia.org/wiki/Mask_(computing)#Arguments_to_functions) that toggles the individual products. the value should toggle the appropriate bit(s) in the bitmask (see below).
- `id`: A sensible, [camelCased](https://en.wikipedia.org/wiki/Camel_case#Variations_and_synonyms), alphanumeric identifier. Use it for the key in the `products` array as well.
- `mode`: A [valid *Friendly Public Transport Format* `1.1.1` mode](https://github.com/public-transport/friendly-public-transport-format/blob/1.1.1/spec/readme.md#modes).
- `bitmasks`: HAFAS endpoints work with a [bitmask](https://en.wikipedia.org/wiki/Mask_(computing)#Arguments_to_functions) that toggles the individual products. It should be an array of values that toggle the appropriate bit(s) in the bitmask (see below).
- `name`: A short, but distinct name for the means of transport, *just precise enough in local context*, and in the local language. In Berlin, `S-Bahn-Schnellzug` would be too much, because everyone knows what `S-Bahn` means.
- `short`: The shortest possible symbol that identifies the product.
- `default`: Should the product be used for queries (e.g. journeys) by default?
todo: `defaultProducts`, `allProducts`, `bitmasks`, add to profile
If you want, you can now **verify that the profile works**; We've prepared [a script](https://runkit.com/derhuerst/hafas-client-profile-example/0.2.1) for that. Alternatively, [submit a Pull Request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/) and we will help you out with testing and improvements.
If you want, you can now **verify that the profile works**; We've prepared [a script](https://runkit.com/public-transport/hafas-client-profile-example/0.1.0) for that. Alternatively, [submit a Pull Request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/) and we will help you out with testing and improvements.
### Finding the right values for the `bitmask` field
### Finding the right values for the `bitmasks` field
As shown in [the video](https://stuff.jannisr.de/how-to-record-hafas-requests.mp4), search for a journey and toggle off one product at a time, recording the requests. After extracting the products bitmask ([example](https://gist.github.com/derhuerst/193ef489f8aa50c2343f8bf1f2a22069#file-via-http-L34)) you will end up with values looking like these:
@ -127,17 +130,17 @@ toggles value binary subtraction bit(s)
all products 31 11111 31 - 0
all but ACME Commuter Rail 15 01111 31 - 2^4 2^4
all but Foo Bar Metro 23 10111 31 - 2^3 2^3
all but product E 30 11001 31 - 2^2 - 2^1 2^2, 2^1
all but product F 253 11110 31 - 2^1 2^0
all but product E 25 11001 31 - 2^2 - 2^1 2^2, 2^1
all but product F 30 11110 31 - 2^0 2^0
```
## 4. Additional info
We consider these improvements to be *optional*:
- **Check if the endpoint supports the journey legs call.**
- **Check if the endpoint supports the trips call.**
- In the app, check if you can query details for the status of a single journey leg. It should load realtime delays and the current progress.
- If this feature is supported, add `journeyLeg: true` to the profile.
- If this feature is supported, add `trip: true` to the profile.
- **Check if the endpoint supports the live map call.** Does the app have a "live map" showing all vehicles within an area? If so, add `radar: true` to the profile.
- **Consider transforming station & line names** into the formats that's most suitable for *local users*. Some examples:
- `M13 (Tram)` -> `M13`. With Berlin context, it is obvious that `M13` is a tram.

View file

@ -1,14 +1,15 @@
'use strict'
const formatLocation = (profile, l) => {
const formatLocation = (profile, l, name = 'location') => {
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)
if (!l.type) throw new Error(`missing ${name}.type`)
throw new Error(`invalid ${name}.type: ${l.type}`)
}
throw new Error('valid station, address or poi required.')
throw new Error(name + ': valid station, address or poi required.')
}
module.exports = formatLocation

View file

@ -1,16 +0,0 @@
'use strict'
const createFormatBitmask = (allProducts) => {
const formatBitmask = (products) => {
if(Object.keys(products).length === 0) throw new Error('products filter must not be empty')
let bitmask = 0
for (let product in products) {
if (!allProducts[product]) throw new Error('unknown product ' + product)
if (products[product] === true) bitmask += allProducts[product].bitmask
}
return bitmask
}
return formatBitmask
}
module.exports = createFormatBitmask

37
format/products-filter.js Normal file
View file

@ -0,0 +1,37 @@
'use strict'
const isObj = require('lodash/isObject')
const hasProp = (o, k) => Object.prototype.hasOwnProperty.call(o, k)
const createFormatProductsFilter = (profile) => {
const byProduct = {}
const defaultProducts = {}
for (let product of profile.products) {
byProduct[product.id] = product
defaultProducts[product.id] = product.default
}
const formatProductsFilter = (filter) => {
if (!isObj(filter)) throw new Error('products filter must be an object')
filter = Object.assign({}, defaultProducts, filter)
let res = 0, products = 0
for (let product in filter) {
if (!hasProp(filter, product) || filter[product] !== true) continue
if (!byProduct[product]) throw new Error('unknown product ' + product)
products++
for (let bitmask of byProduct[product].bitmasks) res += bitmask
}
if (products === 0) throw new Error('no products used')
return {
type: 'PROD',
mode: 'INC',
value: res + ''
}
}
return formatProductsFilter
}
module.exports = createFormatProductsFilter

244
index.js
View file

@ -2,66 +2,104 @@
const minBy = require('lodash/minBy')
const maxBy = require('lodash/maxBy')
const isObj = require('lodash/isObject')
const validateProfile = require('./lib/validate-profile')
const defaultProfile = require('./lib/default-profile')
const createParseBitmask = require('./parse/products-bitmask')
const createFormatProductsFilter = require('./format/products-filter')
const validateProfile = require('./lib/validate-profile')
const _request = require('./lib/request')
const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o)
const isNonEmptyString = str => 'string' === typeof str && str.length > 0
const createClient = (profile, request = _request) => {
const createClient = (profile, userAgent, request = _request) => {
profile = Object.assign({}, defaultProfile, profile)
if (!profile.parseProducts) {
profile.parseProducts = createParseBitmask(profile)
}
if (!profile.formatProductsFilter) {
profile.formatProductsFilter = createFormatProductsFilter(profile)
}
validateProfile(profile)
const departures = (station, opt = {}) => {
if ('string' !== typeof userAgent) {
throw new Error('userAgent must be a string');
}
const _stationBoard = (station, type, parser, opt = {}) => {
if (isObj(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.')
if ('string' !== typeof type || !type) {
throw new Error('type must be a non-empty string.')
}
opt = Object.assign({
direction: null, // only show departures heading to this station
duration: 10 // show departures for the next n minutes
duration: 10, // show departures for the next n minutes
stationLines: false, // parse & expose lines of the station?
remarks: true, // parse & expose hints & warnings?
// departures at related stations
// e.g. those that belong together on the metro map.
includeRelatedStations: true
}, opt)
opt.when = opt.when || new Date()
const products = profile.formatProducts(opt.products || {})
opt.when = new Date(opt.when || Date.now())
if (Number.isNaN(+opt.when)) throw new Error('opt.when is invalid')
const products = profile.formatProductsFilter(opt.products || {})
const dir = opt.direction ? profile.formatStation(opt.direction) : null
return request(profile, {
return request(profile, userAgent, opt, {
meth: 'StationBoard',
req: {
type: 'DEP',
type,
date: profile.formatDate(profile, opt.when),
time: profile.formatTime(profile, opt.when),
stbLoc: station,
dirLoc: dir,
jnyFltrL: [products],
dur: opt.duration,
getPasslist: false
getPasslist: false, // todo
stbFltrEquiv: !opt.includeRelatedStations
}
})
.then((d) => {
if (!Array.isArray(d.jnyL)) return [] // todo: throw err?
const parse = profile.parseDeparture(profile, d.locations, d.lines, d.remarks)
if (!Array.isArray(d.jnyL)) return []
const parse = parser(profile, opt, {
locations: d.locations,
lines: d.lines,
hints: d.hints,
warnings: d.warnings
})
return d.jnyL.map(parse)
.sort((a, b) => new Date(a.when) - new Date(b.when))
})
}
const departures = (station, opt = {}) => {
return _stationBoard(station, 'DEP', profile.parseDeparture, opt)
}
const arrivals = (station, opt = {}) => {
return _stationBoard(station, 'ARR', profile.parseArrival, opt)
}
const journeys = (from, to, opt = {}) => {
from = profile.formatLocation(profile, from)
to = profile.formatLocation(profile, to)
from = profile.formatLocation(profile, from, 'from')
to = profile.formatLocation(profile, to, 'to')
if (('earlierThan' in opt) && ('laterThan' in opt)) {
throw new Error('opt.laterThan and opt.laterThan are mutually exclusive.')
throw new Error('opt.earlierThan and opt.laterThan are mutually exclusive.')
}
if (('departure' in opt) && ('arrival' in opt)) {
throw new Error('opt.departure and opt.arrival are mutually exclusive.')
}
let journeysRef = null
if ('earlierThan' in opt) {
if (!isNonEmptyString(opt.earlierThan)) {
throw new Error('opt.earlierThan must be a non-empty string.')
}
if ('when' in opt) {
throw new Error('opt.earlierThan and opt.when are mutually exclusive.')
if (('departure' in opt) || ('arrival' in opt)) {
throw new Error('opt.earlierThan and opt.departure/opt.arrival are mutually exclusive.')
}
journeysRef = opt.earlierThan
}
@ -69,8 +107,8 @@ const createClient = (profile, request = _request) => {
if (!isNonEmptyString(opt.laterThan)) {
throw new Error('opt.laterThan must be a non-empty string.')
}
if ('when' in opt) {
throw new Error('opt.laterThan and opt.when are mutually exclusive.')
if (('departure' in opt) || ('arrival' in opt)) {
throw new Error('opt.laterThan and opt.departure/opt.arrival are mutually exclusive.')
}
journeysRef = opt.laterThan
}
@ -78,8 +116,7 @@ const createClient = (profile, request = _request) => {
opt = Object.assign({
results: 5, // how many journeys?
via: null, // let journeys pass this station?
passedStations: false, // return stations on the way?
whenRepresents: 'departure', // use 'arrival' for journeys arriving before `when`
stopovers: 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?
@ -87,18 +124,31 @@ const createClient = (profile, request = _request) => {
bike: false, // only bike-friendly journeys
tickets: false, // return tickets?
polylines: false, // return leg shapes?
remarks: true, // parse & expose hints & warnings?
// Consider walking to nearby stations at the beginning of a journey?
startWithWalking: false
startWithWalking: true
}, opt)
if (opt.via) opt.via = profile.formatLocation(profile, opt.via)
opt.when = opt.when || new Date()
if (opt.via) opt.via = profile.formatLocation(profile, opt.via, 'opt.via')
if (opt.when !== undefined) {
throw new Error('opt.when is not supported anymore. Use opt.departure/opt.arrival.')
}
let when = new Date(), outFrwd = true
if (opt.departure !== undefined && opt.departure !== null) {
when = new Date(opt.departure)
if (Number.isNaN(+when)) throw new Error('opt.departure is invalid')
} else if (opt.arrival !== undefined && opt.arrival !== null) {
when = new Date(opt.arrival)
if (Number.isNaN(+when)) throw new Error('opt.arrival is invalid')
outFrwd = false
}
if (opt.whenRepresents !== 'departure' && opt.whenRepresents !== 'arrival') {
throw new Error('opt.whenRepresents must be `departure` or `arrival`.')
}
const filters = [
profile.formatProducts(opt.products || {})
profile.formatProductsFilter(opt.products || {})
]
if (
opt.accessibility &&
@ -121,7 +171,7 @@ const createClient = (profile, request = _request) => {
outDate: profile.formatDate(profile, when),
outTime: profile.formatTime(profile, when),
ctxScr: journeysRef,
getPasslist: !!opt.passedStations,
getPasslist: !!opt.stopovers,
maxChg: opt.transfers,
minChgTime: opt.transferTime,
depLocL: [from],
@ -129,7 +179,7 @@ const createClient = (profile, request = _request) => {
arrLocL: [to],
jnyFltrL: filters,
getTariff: !!opt.tickets,
outFrwd: opt.whenRepresents !== 'arrival',
outFrwd,
ushrp: !!opt.startWithWalking,
// todo: what is req.gisFltrL?
@ -139,7 +189,7 @@ const createClient = (profile, request = _request) => {
}
if (profile.journeysNumF) query.numF = opt.results
return request(profile, {
return request(profile, userAgent, opt, {
cfg: {polyEnc: 'GPA'},
meth: 'TripSearch',
req: profile.transformJourneysQuery(query, opt)
@ -147,11 +197,13 @@ const createClient = (profile, request = _request) => {
.then((d) => {
if (!Array.isArray(d.outConL)) return []
let polylines = []
if (opt.polylines && Array.isArray(d.common.polyL)) {
polylines = d.common.polyL
}
const parse = profile.parseJourney(profile, d.locations, d.lines, d.remarks, polylines)
const parse = profile.parseJourney(profile, opt, {
locations: d.locations,
lines: d.lines,
hints: d.hints,
warnings: d.warnings,
polylines: opt.polylines && d.common.polyL || []
})
if (!journeys.earlierRef) journeys.earlierRef = d.outCtxScrB
@ -160,11 +212,11 @@ const createClient = (profile, request = _request) => {
j = parse(j)
journeys.push(j)
if (journeys.length === opt.results) { // collected enough
if (journeys.length >= opt.results) { // collected enough
journeys.laterRef = d.outCtxScrF
return journeys
}
const dep = +new Date(j.departure)
const dep = +new Date(j.legs[0].departure)
if (dep > latestDep) latestDep = dep
}
@ -173,7 +225,45 @@ const createClient = (profile, request = _request) => {
})
}
return more(opt.when, journeysRef)
return more(when, journeysRef)
}
const refreshJourney = (refreshToken, opt = {}) => {
if ('string' !== typeof refreshToken || !refreshToken) {
new Error('refreshToken must be a non-empty string.')
}
opt = Object.assign({
stopovers: false, // return stations on the way?
tickets: false, // return tickets?
polylines: false, // return leg shapes?
remarks: true // parse & expose hints & warnings?
}, opt)
return request(profile, userAgent, opt, {
meth: 'Reconstruction',
req: {
ctxRecon: refreshToken,
getIST: true, // todo: make an option
getPasslist: !!opt.stopovers,
getPolyline: !!opt.polylines,
getTariff: !!opt.tickets
}
})
.then((d) => {
if (!Array.isArray(d.outConL) || !d.outConL[0]) {
throw new Error('invalid response')
}
const parse = profile.parseJourney(profile, opt, {
locations: d.locations,
lines: d.lines,
hints: d.hints,
warnings: d.warnings,
polylines: opt.polylines && d.common.polyL || []
})
return parse(d.outConL[0])
})
}
const locations = (query, opt = {}) => {
@ -185,11 +275,12 @@ const createClient = (profile, request = _request) => {
results: 10, // how many search results?
stations: true,
addresses: true,
poi: true // points of interest
poi: true, // points of interest
stationLines: false // parse & expose lines of the station?
}, opt)
const f = profile.formatLocationFilter(opt.stations, opt.addresses, opt.poi)
return request(profile, {
return request(profile, userAgent, opt, {
cfg: {polyEnc: 'GPA'},
meth: 'LocMatch',
req: {input: {
@ -204,16 +295,19 @@ const createClient = (profile, request = _request) => {
.then((d) => {
if (!d.match || !Array.isArray(d.match.locL)) return []
const parse = profile.parseLocation
return d.match.locL.map(loc => parse(profile, loc, d.lines))
return d.match.locL.map(loc => parse(profile, opt, {lines: d.lines}, loc))
})
}
const location = (station) => {
const station = (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.')
return request(profile, {
opt = Object.assign({
stationLines: false // parse & expose lines of the station?
}, opt)
return request(profile, userAgent, opt, {
meth: 'LocDetails',
req: {
locL: [station]
@ -224,7 +318,7 @@ const createClient = (profile, request = _request) => {
// todo: proper stack trace?
throw new Error('invalid response')
}
return profile.parseLocation(profile, d.locL[0], d.lines)
return profile.parseLocation(profile, opt, {lines: d.lines}, d.locL[0])
})
}
@ -244,9 +338,10 @@ const createClient = (profile, request = _request) => {
distance: null, // maximum walking distance in meters
poi: false, // return points of interest?
stations: true, // return stations?
stationLines: false // parse & expose lines of the station?
}, opt)
return request(profile, {
return request(profile, userAgent, opt, {
cfg: {polyEnc: 'GPA'},
meth: 'LocGeoPos',
req: {
@ -266,40 +361,44 @@ const createClient = (profile, request = _request) => {
.then((d) => {
if (!Array.isArray(d.locL)) return []
const parse = profile.parseNearby
return d.locL.map(loc => parse(profile, loc))
return d.locL.map(loc => parse(profile, opt, d, loc))
})
}
const journeyLeg = (ref, lineName, opt = {}) => {
if (!isNonEmptyString(ref)) {
throw new Error('ref must be a non-empty string.')
const trip = (id, lineName, opt = {}) => {
if (!isNonEmptyString(id)) {
throw new Error('id must be a non-empty string.')
}
if (!isNonEmptyString(lineName)) {
throw new Error('lineName must be a non-empty string.')
}
opt = Object.assign({
passedStations: true, // return stations on the way?
polyline: false
stopovers: true, // return stations on the way?
polyline: false, // return a track shape?
remarks: true // parse & expose hints & warnings?
}, opt)
opt.when = opt.when || new Date()
opt.when = new Date(opt.when || Date.now())
if (Number.isNaN(+opt.when)) throw new Error('opt.when is invalid')
return request(profile, {
return request(profile, userAgent, opt, {
cfg: {polyEnc: 'GPA'},
meth: 'JourneyDetails',
req: {
// todo: getTrainComposition
jid: ref,
jid: id,
name: lineName,
date: profile.formatDate(profile, opt.when),
getPolyline: !!opt.polyline
}
})
.then((d) => {
let polylines = []
if (opt.polyline && Array.isArray(d.common.polyL)) {
polylines = d.common.polyL
}
const parse = profile.parseJourneyLeg(profile, d.locations, d.lines, d.remarks, polylines)
const parse = profile.parseJourneyLeg(profile, opt, {
locations: d.locations,
lines: d.lines,
hints: d.hints,
warnings: d.warnings,
polylines: opt.polyline && d.common.polyL || []
})
const leg = { // pretend the leg is contained in a journey
type: 'JNY',
@ -307,27 +406,31 @@ const createClient = (profile, request = _request) => {
arr: maxBy(d.journey.stopL, 'idx'),
jny: d.journey
}
return parse(d.journey, leg, !!opt.passedStations)
return parse(d.journey, leg, !!opt.stopovers)
})
}
const radar = (north, west, south, east, opt) => {
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.')
if (north <= south) throw new Error('north must be larger than south.')
if (east <= west) throw new Error('east must be larger than west.')
opt = Object.assign({
results: 256, // maximum number of vehicles
duration: 30, // compute frames for the next n seconds
// todo: what happens with `frames: 0`?
frames: 3, // nr of frames to compute
products: null, // optionally an object of booleans
polylines: false // return a track shape for each vehicle?
}, opt || {})
opt.when = opt.when || new Date()
opt.when = new Date(opt.when || Date.now())
if (Number.isNaN(+opt.when)) throw new Error('opt.when is invalid')
const durationPerStep = opt.duration / Math.max(opt.frames, 1) * 1000
return request(profile, {
return request(profile, userAgent, opt, {
meth: 'JourneyGeoPos',
req: {
maxJny: opt.results,
@ -340,7 +443,7 @@ const createClient = (profile, request = _request) => {
perStep: Math.round(durationPerStep),
ageOfReport: true, // todo: what is this?
jnyFltrL: [
profile.formatProducts(opt.products || {})
profile.formatProductsFilter(opt.products || {})
],
trainPosMode: 'CALC' // todo: what is this? what about realtime?
}
@ -348,18 +451,21 @@ const createClient = (profile, request = _request) => {
.then((d) => {
if (!Array.isArray(d.jnyL)) return []
let polylines = []
if (opt.polylines && d.common && Array.isArray(d.common.polyL)) {
polylines = d.common.polyL
}
const parse = profile.parseMovement(profile, d.locations, d.lines, d.remarks, polylines)
const parse = profile.parseMovement(profile, opt, {
locations: d.locations,
lines: d.lines,
hints: d.hints,
warnings: d.warnings,
polylines: opt.polyline && d.common.polyL || []
})
return d.jnyL.map(parse)
})
}
const client = {departures, journeys, locations, location, nearby}
if (profile.journeyLeg) client.journeyLeg = journeyLeg
const client = {departures, arrivals, journeys, locations, station, nearby}
if (profile.trip) client.trip = trip
if (profile.radar) client.radar = radar
if (profile.refreshJourney) client.refreshJourney = refreshJourney
Object.defineProperty(client, 'profile', {value: profile})
return client
}

View file

@ -2,14 +2,17 @@
const parseDateTime = require('../parse/date-time')
const parseDeparture = require('../parse/departure')
const parseArrival = require('../parse/arrival')
const parseJourneyLeg = require('../parse/journey-leg')
const parseJourney = require('../parse/journey')
const parseLine = require('../parse/line')
const parseLocation = require('../parse/location')
const parsePolyline = require('../parse/polyline')
const parseMovement = require('../parse/movement')
const parseNearby = require('../parse/nearby')
const parseOperator = require('../parse/operator')
const parseRemark = require('../parse/remark')
const parseHint = require('../parse/hint')
const parseWarning = require('../parse/warning')
const parseStopover = require('../parse/stopover')
const formatAddress = require('../format/address')
@ -37,15 +40,18 @@ const defaultProfile = {
parseDateTime,
parseDeparture,
parseArrival,
parseJourneyLeg,
parseJourney,
parseLine,
parseStationName: id,
parseLocation,
parsePolyline,
parseMovement,
parseNearby,
parseOperator,
parseRemark,
parseHint,
parseWarning,
parseStopover,
formatAddress,
@ -60,8 +66,9 @@ const defaultProfile = {
filters,
journeysNumF: true, // `journeys()` method: support for `numF` field?
journeyLeg: false,
radar: false
trip: false,
radar: false,
refreshJourney: true
}
module.exports = defaultProfile

6
lib/generate-install-id.js Executable file
View file

@ -0,0 +1,6 @@
#!/usr/bin/env node
const {randomBytes} = require('crypto')
const id = randomBytes(6).toString('hex')
process.stdout.write(JSON.stringify(id) + '\n')

View file

@ -1,26 +1,36 @@
'use strict'
const {join} = require('path')
const createHash = require('create-hash')
let captureStackTrace = () => {}
if (process.env.NODE_ENV === 'dev') {
if (process.env.NODE_DEBUG === 'hafas-client') {
captureStackTrace = require('capture-stack-trace')
}
const {stringify} = require('query-string')
const Promise = require('pinkie-promise')
const {fetch} = require('fetch-ponyfill')({Promise})
const userAgent = 'https://github.com/public-transport/hafas-client'
const clientId = Math.random().toString(16).substr(2, 10)
let id
try {
id = require('../id.json')
} catch (err) {
const p = join(__dirname, '..', 'id.json')
console.error(`Failed to load the install-unique ID from ${p}.`)
process.exit(1)
}
const randomizeUserAgent = (userAgent) => {
const i = Math.round(Math.random() * userAgent.length)
return userAgent.slice(0, i) + id + userAgent.slice(i)
}
const md5 = input => createHash('md5').update(input).digest()
const randomizeUserAgent = () => {
const i = Math.round(Math.random() * userAgent.length)
return userAgent.slice(0, i) + clientId + userAgent.slice(i)
}
const request = (profile, data) => {
const body = profile.transformReqBody({lang: 'en', svcReqL: [data]})
const request = (profile, userAgent, opt, data) => {
const body = profile.transformReqBody({
lang: opt.language || 'en',
svcReqL: [data]
})
const req = profile.transformReq({
method: 'post',
// todo: CORS? referrer policy?
@ -29,7 +39,7 @@ const request = (profile, data) => {
'Content-Type': 'application/json',
'Accept-Encoding': 'gzip, deflate',
'Accept': 'application/json',
'user-agent': randomizeUserAgent()
'user-agent': randomizeUserAgent(userAgent)
},
query: {}
})
@ -89,19 +99,38 @@ const request = (profile, data) => {
const d = b.svcResL[0].res
const c = d.common || {}
if (Array.isArray(c.remL)) {
d.remarks = c.remL.map(rem => profile.parseRemark(profile, rem))
d.hints = []
if (opt.remarks && Array.isArray(c.remL)) {
const icons = opt.remarks && c.icoL || []
d.hints = c.remL.map(hint => profile.parseHint(profile, hint, icons))
}
d.warnings = []
if (opt.remarks && Array.isArray(c.himL)) {
const icons = opt.remarks && c.icoL || []
d.warnings = c.himL.map(w => profile.parseWarning(profile, w, icons))
}
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)
const parse = profile.parseLine(profile, opt, {
operators: d.operators
})
d.lines = c.prodL.map(parse)
}
if (Array.isArray(c.locL)) {
const parse = loc => profile.parseLocation(profile, loc, d.lines)
const data = {lines: d.lines}
const parse = loc => profile.parseLocation(profile, opt, data, loc)
d.locations = c.locL.map(parse)
for (let i = 0; i < d.locations.length; i++) {
const raw = c.locL[i]
const loc = d.locations[i]
if ('number' === typeof raw.mMastLocX) {
loc.station = Object.assign({}, d.locations[raw.mMastLocX])
loc.station.type = 'station'
} else if (raw.isMainMast) loc.type = 'station'
}
}
return d
})

View file

@ -11,15 +11,18 @@ const types = {
parseDateTime: 'function',
parseDeparture: 'function',
parseArrival: 'function',
parseJourneyLeg: 'function',
parseJourney: 'function',
parseLine: 'function',
parseStationName: 'function',
parseLocation: 'function',
parsePolyline: 'function',
parseMovement: 'function',
parseNearby: 'function',
parseOperator: 'function',
parseRemark: 'function',
parseHint: 'function',
parseWarning: 'function',
parseStopover: 'function',
formatAddress: 'function',
@ -47,6 +50,34 @@ const validateProfile = (profile) => {
throw new Error(`profile.${key} must not be null.`)
}
}
if (!Array.isArray(profile.products)) {
throw new Error('profile.products must be an array.')
}
if (profile.products.length === 0) throw new Error('profile.products is empty.')
for (let product of profile.products) {
if ('string' !== typeof product.id) {
throw new Error('profile.products[].id must be a string.')
}
if ('boolean' !== typeof product.default) {
throw new Error('profile.products[].default must be a boolean.')
}
if (!Array.isArray(product.bitmasks)) {
throw new Error(product.id + '.bitmasks must be an array.')
}
for (let bitmask of product.bitmasks) {
if ('number' !== typeof bitmask) {
throw new Error(product.id + '.bitmasks[] must be a number.')
}
}
}
if ('trip' in profile && 'boolean' !== typeof profile.trip) {
throw new Error('profile.trip must be a boolean.')
}
if ('journeyLeg' in profile) {
throw new Error('profile.journeyLeg has been removed. Use profile.trip.')
}
}
module.exports = validateProfile

37
p/bvg/example.js Normal file
View file

@ -0,0 +1,37 @@
'use strict'
const createClient = require('../..')
const vbbProfile = require('.')
const client = createClient(vbbProfile, 'hafas-client-example')
// Hauptbahnhof to Charlottenburg
client.journeys('900000003201', '900000024101', {results: 1, polylines: true})
// client.departures('900000013102', {duration: 1})
// client.arrivals('900000013102', {duration: 10, stationLines: true})
// client.locations('Alexanderplatz', {results: 2})
// client.station('900000042101', {stationLines: true}) // Spichernstr
// client.nearby({
// type: 'location',
// latitude: 52.5137344,
// longitude: 13.4744798
// }, {distance: 60})
// client.radar({
// north: 52.52411,
// west: 13.41002,
// south: 52.51942,
// east: 13.41709
// }, {results: 10})
// .then(([journey]) => {
// const leg = journey.legs[0]
// return client.trip(leg.id, leg.line.name, {polyline: true})
// })
// .then(([journey]) => {
// return client.refreshJourney(journey.refreshToken, {stopovers: true, remarks: true})
// })
.then((data) => {
console.log(require('util').inspect(data, {depth: null}))
})
.catch(console.error)

112
p/bvg/index.js Normal file
View file

@ -0,0 +1,112 @@
'use strict'
const shorten = require('vbb-short-station-name')
const {to12Digit, to9Digit} = require('vbb-translate-ids')
const parseLineName = require('vbb-parse-line')
const getStations = require('vbb-stations')
const _createParseLine = require('../../parse/line')
const _parseLocation = require('../../parse/location')
const _createParseDeparture = require('../../parse/departure')
const _formatStation = require('../../format/station')
const products = require('./products')
const transformReqBody = (body) => {
body.client = {type: 'IPA', id: 'BVG', name: 'FahrInfo', v: '4070700'}
body.ext = 'BVG.1'
body.ver = '1.15' // todo: 1.16 with `mic` and `mac` query params
body.auth = {type: 'AID', aid: '1Rxs112shyHLatUX4fofnmdxK'}
return body
}
const createParseLine = (profile, opt, data) => {
const parseLine = _createParseLine(profile, opt, data)
const parseLineWithMoreDetails = (l) => {
const res = parseLine(l)
res.name = l.name.replace(/^(bus|tram)\s+/i, '')
const details = parseLineName(res.name)
res.symbol = details.symbol
res.nr = details.nr
res.metro = details.metro
res.express = details.express
res.night = details.night
return res
}
return parseLineWithMoreDetails
}
const parseLocation = (profile, opt, data, l) => {
const res = _parseLocation(profile, opt, data, l)
if (res.type === 'stop' || 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.location)
}
}
return res
}
const createParseDeparture = (profile, opt, data) => {
const parseDeparture = _createParseDeparture(profile, opt, data)
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.')
}
// BVG 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)
}
// todo: adapt/extend `vbb-parse-ticket` to support the BVG markup
const bvgProfile = {
locale: 'de-DE',
timezone: 'Europe/Berlin',
endpoint: 'https://bvg-apps.hafas.de/bin/mgate.exe',
transformReqBody,
products,
parseStationName: shorten,
parseLocation,
parseLine: createParseLine,
parseDeparture: createParseDeparture,
formatStation,
trip: true,
radar: true
}
module.exports = bvgProfile

60
p/bvg/products.js Normal file
View file

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

21
p/bvg/readme.md Normal file
View file

@ -0,0 +1,21 @@
# BVG profile for `hafas-client`
[*Verkehrsverbund Berlin-Brandenburg (BVG)*](https://en.wikipedia.org/wiki/Verkehrsverbund_Berlin-Brandenburg) is the major local transport provider in [Berlin](https://en.wikipedia.org/wiki/Berlin). This profile adds *BVG*-specific customizations to `hafas-client`.
## Usage
```js
const createClient = require('hafas-client')
const bvgProfile = require('hafas-client/p/bvg')
// create a client with BVG profile
const client = createClient(bvgProfile, 'my-awesome-program')
```
## Customisations
- parses *BVG*-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?")
- renames *Ringbahn* line names to contain `⟳` and `⟲`

View file

@ -3,15 +3,20 @@
const createClient = require('../../')
const dbProfile = require('.')
const client = createClient(dbProfile)
const client = createClient(dbProfile, 'hafas-client-example')
// Berlin Jungfernheide to München Hbf
client.journeys('8011167', '8000261', {results: 1, tickets: true})
// client.departures('8011167', {duration: 1})
// client.arrivals('8011167', {duration: 10, stationLines: true})
// client.locations('Berlin Jungfernheide')
// client.locations('Atze Musiktheater', {poi: true, addressses: false, fuzzy: false})
// client.location('8000309') // Regensburg Hbf
// client.nearby(52.4751309, 13.3656537, {results: 1})
// client.station('8000309') // Regensburg Hbf
// client.nearby({
// type: 'location',
// latitude: 52.4751309,
// longitude: 13.3656537
// }, {results: 1})
.then((data) => {
console.log(require('util').inspect(data, {depth: null}))

View file

@ -1,17 +1,15 @@
'use strict'
const _createParseLine = require('../../parse/line')
const trim = require('lodash/trim')
const _createParseJourney = require('../../parse/journey')
const _parseHint = require('../../parse/hint')
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 products = require('./products')
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'
@ -39,28 +37,8 @@ const transformJourneysQuery = (query, opt) => {
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, polylines) => {
const parseJourney = _createParseJourney(profile, stations, lines, remarks, polylines)
const createParseJourney = (profile, opt, data) => {
const parseJourney = _createParseJourney(profile, opt, data)
// todo: j.sotRating, j.conSubscr, j.isSotCon, j.showARSLink, k.sotCtxt
// todo: j.conSubscr, j.showARSLink, j.useableTime
@ -87,7 +65,11 @@ const createParseJourney = (profile, stations, lines, remarks, polylines) => {
) {
const tariff = j.trfRes.fareSetL[0].fareL[0]
if (tariff.prc >= 0) { // wat
res.price = {amount: tariff.prc / 100, hint: null}
res.price = {
amount: tariff.prc / 100,
currency: 'EUR',
hint: null
}
}
}
@ -97,33 +79,225 @@ const createParseJourney = (profile, stations, lines, remarks, polylines) => {
return parseJourneyWithPrice
}
const hintsByCode = Object.assign(Object.create(null), {
fb: {
type: 'hint',
code: 'bicycle-conveyance',
summary: 'bicycles conveyed'
},
fr: {
type: 'hint',
code: 'bicycle-conveyance-reservation',
summary: 'bicycles conveyed, subject to reservation'
},
nf: {
type: 'hint',
code: 'no-bicycle-conveyance',
summary: 'bicycles not conveyed'
},
k2: {
type: 'hint',
code: '2nd-class-only',
summary: '2. class only'
},
eh: {
type: 'hint',
code: 'boarding-ramp',
summary: 'vehicle-mounted boarding ramp available'
},
ro: {
type: 'hint',
code: 'wheelchairs-space',
summary: 'space for wheelchairs'
},
oa: {
type: 'hint',
code: 'wheelchairs-space-reservation',
summary: 'space for wheelchairs, subject to reservation'
},
wv: {
type: 'hint',
code: 'wifi',
summary: 'WiFi available'
},
wi: {
type: 'hint',
code: 'wifi',
summary: 'WiFi available'
},
sn: {
type: 'hint',
code: 'snacks',
summary: 'snacks available for purchase'
},
mb: {
type: 'hint',
code: 'snacks',
summary: 'snacks available for purchase'
},
mp: {
type: 'hint',
code: 'snacks',
summary: 'snacks available for purchase at the seat'
},
bf: {
type: 'hint',
code: 'barrier-free',
summary: 'barrier-free'
},
rg: {
type: 'hint',
code: 'barrier-free-vehicle',
summary: 'barrier-free vehicle'
},
bt: {
type: 'hint',
code: 'on-board-bistro',
summary: 'Bordbistro available'
},
br: {
type: 'hint',
code: 'on-board-restaurant',
summary: 'Bordrestaurant available'
},
ki: {
type: 'hint',
code: 'childrens-area',
summary: `children's area available`
},
kk: {
type: 'hint',
code: 'parents-childrens-compartment',
summary: `parent-and-children compartment available`
},
kr: {
type: 'hint',
code: 'kids-service',
summary: 'DB Kids Service available'
},
ls: {
type: 'hint',
code: 'power-sockets',
summary: 'power sockets available'
},
ev: {
type: 'hint',
code: 'replacement-service',
summary: 'replacement service'
},
kl: {
type: 'hint',
code: 'air-conditioned',
summary: 'air-conditioned vehicle'
},
r0: {
type: 'hint',
code: 'upward-escalator',
summary: 'upward escalator'
},
au: {
type: 'hint',
code: 'elevator',
summary: 'elevator available'
},
ck: {
type: 'hint',
code: 'komfort-checkin',
summary: 'Komfort-Checkin available'
},
it: {
type: 'hint',
code: 'ice-sprinter',
summary: 'ICE Sprinter service'
},
rp: {
type: 'hint',
code: 'compulsory-reservation',
summary: 'compulsory seat reservation'
},
rm: {
type: 'hint',
code: 'optional-reservation',
summary: 'optional seat reservation'
},
scl: {
type: 'hint',
code: 'all-2nd-class-seats-reserved',
summary: 'all 2nd class seats reserved'
},
acl: {
type: 'hint',
code: 'all-seats-reserved',
summary: 'all seats reserved'
},
sk: {
type: 'hint',
code: 'oversize-luggage-forbidden',
summary: 'oversize luggage not allowed'
},
hu: {
type: 'hint',
code: 'animals-forbidden',
summary: 'animals not allowed, except guide dogs'
},
ik: {
type: 'hint',
code: 'baby-cot-required',
summary: 'baby cot/child seat required'
},
ee: {
type: 'hint',
code: 'on-board-entertainment',
summary: 'on-board entertainment available'
},
toilet: {
type: 'hint',
code: 'toilet',
summary: 'toilet available'
},
oc: {
type: 'hint',
code: 'wheelchair-accessible-toilet',
summary: 'wheelchair-accessible toilet available'
},
iz: {
type: 'hint',
code: 'intercity-2',
summary: 'Intercity 2'
}
})
const codesByText = Object.assign(Object.create(null), {
'journey cancelled': 'journey-cancelled', // todo: German variant
'stop cancelled': 'stop-cancelled', // todo: change to `stopover-cancelled`, German variant
'signal failure': 'signal-failure',
'signalstörung': 'signal-failure',
'additional stop': 'additional-stopover', // todo: German variant
'platform change': 'changed platform', // todo: use dash, German variant
})
const parseHint = (profile, h, icons) => {
if (h.type === 'A') {
const hint = hintsByCode[h.code && h.code.trim().toLowerCase()]
if (hint) {
return Object.assign({text: h.txtN}, hint)
}
}
const res = _parseHint(profile, h, icons)
if (res && h.txtN) {
const text = trim(h.txtN.toLowerCase(), ' ()')
if (codesByText[text]) res.code = codesByText[text]
}
return res
}
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,
taxi: false
}
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 = {
@ -137,17 +311,15 @@ const dbProfile = {
transformReqBody,
transformJourneysQuery,
products: modes.allProducts,
products: products,
// todo: parseLocation
parseLine: createParseLine,
parseProducts: createParseBitmask(modes.allProducts, defaultProducts),
parseJourney: createParseJourney,
parseHint,
formatStation,
formatProducts,
journeyLeg: true // todo: #49
trip: true // todo: #49
}
module.exports = dbProfile

View file

@ -1,108 +0,0 @@
'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: 'train',
product: 'tram'
},
taxi: {
bitmask: 512,
name: 'Group Taxi',
short: 'Taxi',
mode: 'taxi',
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

85
p/db/products.js Normal file
View file

@ -0,0 +1,85 @@
'use strict'
// todo: https://gist.github.com/anonymous/d3323a5d2d6e159ed42b12afd0380434#file-haf_products-properties-L1-L95
module.exports = [
{
id: 'nationalExp',
mode: 'train',
bitmasks: [1],
name: 'InterCityExpress',
short: 'ICE',
default: true
},
{
id: 'national',
mode: 'train',
bitmasks: [2],
name: 'InterCity & EuroCity',
short: 'IC/EC',
default: true
},
{
id: 'regionalExp',
mode: 'train',
bitmasks: [4],
name: 'RegionalExpress & InterRegio',
short: 'RE/IR',
default: true
},
{
id: 'regional',
mode: 'train',
bitmasks: [8],
name: 'Regio',
short: 'RB',
default: true
},
{
id: 'suburban',
mode: 'train',
bitmasks: [16],
name: 'S-Bahn',
short: 'S',
default: true
},
{
id: 'bus',
mode: 'bus',
bitmasks: [32],
name: 'Bus',
short: 'B',
default: true
},
{
id: 'ferry',
mode: 'watercraft',
bitmasks: [64],
name: 'Ferry',
short: 'F',
default: true
},
{
id: 'subway',
mode: 'train',
bitmasks: [128],
name: 'U-Bahn',
short: 'U',
default: true
},
{
id: 'tram',
mode: 'train',
bitmasks: [256],
name: 'Tram',
short: 'T',
default: true
},
{
id: 'taxi',
mode: 'taxi',
bitmasks: [512],
name: 'Group Taxi',
short: 'Taxi',
default: true
}
]

View file

@ -9,7 +9,7 @@ const createClient = require('hafas-client')
const dbProfile = require('hafas-client/p/db')
// create a client with DB profile
const client = createClient(dbProfile)
const client = createClient(dbProfile, 'my-awesome-program')
```

View file

@ -3,24 +3,34 @@
const createClient = require('../..')
const insaProfile = require('.')
const client = createClient(insaProfile)
const client = createClient(insaProfile, 'hafas-client-example')
// from Magdeburg-Neustadt to Magdeburg-Buckau
client.journeys('008010226', '008013456', {results: 1})
// client.departures('008010226', { duration: 5 })
// client.arrivals('8010226', {duration: 10, stationLines: true})
// client.locations('Magdeburg Hbf', {results: 2})
// client.locations('Kunstmuseum Kloster Unser Lieben Frauen Magdeburg', {results: 2})
// client.location('008010226') // Magdeburg-Neustadt
// client.station('008010226') // Magdeburg-Neustadt
// client.nearby({
// type: 'location',
// latitude: 52.148842,
// longitude: 11.641705
// }, {distance: 200})
// client.radar(52.148364, 11.600826, 52.108486, 11.651451, {results: 10})
// client.radar({
// north: 52.148364,
// west: 11.600826,
// south: 52.108486,
// east: 11.651451
// }, {results: 10})
// .then(([journey]) => {
// const leg = journey.legs[0]
// return client.journeyLeg(leg.id, leg.line.name)
// return client.trip(leg.id, leg.line.name)
// })
// .then(([journey]) => {
// return client.refreshJourney(journey.refreshToken, {stopovers: true, remarks: true})
// })
.then(data => {

View file

@ -1,20 +1,6 @@
'use strict'
const _createParseLine = require('../../parse/line')
const products = require('./products')
const createParseBitmask = require('../../parse/products-bitmask')
const createFormatBitmask = require('../../format/products-bitmask')
const defaultProducts = {
nationalExp: true,
national: true,
regional: true,
suburban: true,
bus: true,
tram: true,
tourismTrain: true,
}
const transformReqBody = (body) => {
body.client = {
@ -31,52 +17,17 @@ const transformReqBody = (body) => {
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 = products.bitmasks[parseInt(res.class)]
if (data) {
res.mode = data.mode
res.product = data.product
}
}
return res
}
return parseLineWithMode
}
const formatProducts = (products) => {
products = Object.assign(Object.create(null), defaultProducts, products)
return {
type: 'PROD',
mode: 'INC',
value: formatBitmask(products) + ''
}
}
const formatBitmask = createFormatBitmask(products)
const insaProfile = {
locale: 'de-DE',
timezone: 'Europe/Berlin',
endpoint: 'https://reiseauskunft.insa.de/bin/mgate.exe',
transformReqBody,
products: products.allProducts,
parseProducts: createParseBitmask(products.allProducts, defaultProducts),
formatProducts,
products: products,
parseLine: createParseLine,
journeyLeg: true,
radar: true
trip: true,
radar: true,
refreshJourney: false
}
module.exports = insaProfile;

View file

@ -1,82 +1,60 @@
'use strict'
// TODO Jannis R.: DRY
const p = {
nationalExp: {
bitmask: 1,
module.exports = [
{
id: 'nationalExp',
mode: 'train',
bitmasks: [1],
name: 'InterCityExpress',
short: 'ICE',
mode: 'train',
product: 'nationalExp'
default: true
},
national: {
bitmask: 2,
{
id: 'national',
mode: 'train',
bitmasks: [2],
name: 'InterCity & EuroCity',
short: 'IC/EC',
mode: 'train',
product: 'national'
default: true
},
regional: {
bitmask: 8,
{
id: 'regional',
mode: 'train',
bitmasks: [8],
name: 'RegionalExpress & RegionalBahn',
short: 'RE/RB',
mode: 'train',
product: 'regional'
default: true
},
suburban: {
bitmask: 16,
{
id: 'suburban',
mode: 'train',
bitmasks: [16],
name: 'S-Bahn',
short: 'S',
mode: 'train',
product: 'suburban'
default: true
},
tram: {
bitmask: 32,
{
id: 'tram',
mode: 'train',
bitmasks: [32],
name: 'Tram',
short: 'T',
mode: 'train',
product: 'tram'
default: true
},
bus: {
bitmask: 64+128,
{
id: 'bus',
mode: 'bus',
bitmasks: [64, 128],
name: 'Bus',
short: 'B',
mode: 'bus',
product: 'bus'
default: true
},
tourismTrain: {
bitmask: 256,
{
id: 'tourismTrain',
mode: 'train',
bitmasks: [256],
name: 'Tourism Train',
short: 'TT',
mode: 'train',
product: 'tourismTrain'
},
unknown: {
bitmask: 0,
name: 'unknown',
short: '?',
product: 'unknown'
default: true
}
}
p.bitmasks = []
p.bitmasks[1] = p.nationalExp
p.bitmasks[2] = p.national
p.bitmasks[8] = p.regional
p.bitmasks[16] = p.suburban
p.bitmasks[32] = p.tram
p.bitmasks[64] = p.bus
p.bitmasks[128] = p.bus
p.bitmasks[256] = p.tourismTrain
p.allProducts = [
p.nationalExp,
p.national,
p.regional,
p.suburban,
p.tram,
p.bus,
p.tourismTrain
]
module.exports = p

View file

@ -9,7 +9,7 @@ const createClient = require('hafas-client')
const insaProfile = require('hafas-client/p/insa')
// create a client with INSA profile
const client = createClient(insaProfile)
const client = createClient(insaProfile, 'my-awesome-program')
```

View file

@ -3,15 +3,20 @@
const createClient = require('../..')
const nahshProfile = require('.')
const client = createClient(nahshProfile)
const client = createClient(nahshProfile, 'hafas-client-example')
// Flensburg Hbf to Kiel Hbf
client.journeys('8000103', '8000199', {results: 10, tickets: true})
// client.departures('8000199', {duration: 10})
// client.journeyLeg('1|30161|5|100|14032018', 'Bus 52')
// client.arrivals('8000199', {duration: 5, stationLines: true})
// client.trip('1|30161|5|100|14032018', 'Bus 52')
// client.locations('Schleswig', {results: 1})
// client.location('706990') // Kiel Holunderbusch
// client.nearby({type: 'location', latitude: 54.295691, longitude: 10.116424}, {distance: 60})
// client.station('706990') // Kiel Holunderbusch
// client.nearby({
// type: 'location',
// latitude: 54.295691,
// longitude: 10.116424
// }, {distance: 60})
// client.radar(54.4, 10.0, 54.2, 10.2, {results: 10})
.then((data) => {

View file

@ -1,10 +1,8 @@
'use strict'
const createParseBitmask = require('../../parse/products-bitmask')
const createFormatBitmask = require('../../format/products-bitmask')
const _createParseLine = require('../../parse/line')
const _parseLocation = require('../../parse/location')
const _createParseJourney = require('../../parse/journey')
const _createParseMovement = require('../../parse/movement')
const products = require('./products')
@ -23,8 +21,8 @@ const transformReqBody = (body) => {
return body
}
const parseLocation = (profile, l, lines) => {
const res = _parseLocation(profile, l, lines)
const parseLocation = (profile, opt, data, l) => {
const res = _parseLocation(profile, opt, data, l)
// weird fix for empty lines, e.g. IC/EC at Flensburg Hbf
if (res.lines) {
res.lines = res.lines.filter(x => x.id && x.name)
@ -38,28 +36,8 @@ const parseLocation = (profile, l, lines) => {
return res
}
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 = products.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)
const createParseJourney = (profile, opt, data) => {
const parseJourney = _createParseJourney(profile, opt, data)
const parseJourneyWithTickets = (j) => {
const res = parseJourney(j)
@ -100,26 +78,17 @@ const createParseJourney = (profile, stations, lines, remarks) => {
return parseJourneyWithTickets
}
const defaultProducts = {
nationalExp: true,
national: true,
interregional: true,
regional: true,
suburban: true,
bus: true,
ferry: true,
subway: true,
tram: true,
onCall: true
}
const formatBitmask = createFormatBitmask(products)
const formatProducts = (products) => {
products = Object.assign(Object.create(null), defaultProducts, products)
return {
type: 'PROD',
mode: 'INC',
value: formatBitmask(products) + ''
const createParseMovement = (profile, opt, data) => {
const _parseMovement = _createParseMovement(profile, opt, data)
const parseMovement = (m) => {
const res = _parseMovement(m)
// filter out empty nextStops entries
res.nextStops = res.nextStops.filter((f) => {
return f.stop !== null || f.arrival !== null || f.departure !== null
})
return res
}
return parseMovement
}
const nahshProfile = {
@ -128,17 +97,14 @@ const nahshProfile = {
endpoint: 'https://nah.sh.hafas.de/bin/mgate.exe',
transformReqBody,
products: products.allProducts,
products,
parseProducts: createParseBitmask(products.allProducts, defaultProducts),
parseLine: createParseLine,
parseLocation,
parseJourney: createParseJourney,
parseMovement: createParseMovement,
formatProducts,
journeyLeg: true,
radar: false // todo: see #34
trip: true,
radar: true // todo: see #34
}
module.exports = nahshProfile

View file

@ -1,101 +1,86 @@
'use strict'
const p = {
nationalExp: {
bitmask: 1,
const p = [
{
id: 'nationalExp',
mode: 'train',
bitmasks: [1],
name: 'High-speed rail',
short: 'ICE/HSR',
mode: 'train',
product: 'nationalExp'
default: true
},
national: {
bitmask: 2,
{
id: 'national',
mode: 'train',
bitmasks: [2],
name: 'InterCity & EuroCity',
short: 'IC/EC',
mode: 'train',
product: 'national'
default: true
},
interregional: { // todo: also includes EN?
bitmask: 4,
{ // todo: also includes EN?
id: 'interregional',
mode: 'train',
bitmasks: [4],
name: 'Interregional',
short: 'IR',
mode: 'train',
product: 'interregional'
default: true
},
regional: {
bitmask: 8,
{
id: 'regional',
mode: 'train',
bitmasks: [8],
name: 'Regional & RegionalExpress',
short: 'RB/RE',
mode: 'train',
product: 'regional'
default: true
},
suburban: {
bitmask: 16,
{
id: 'suburban',
mode: 'train',
bitmasks: [16],
name: 'S-Bahn',
short: 'S',
mode: 'train',
product: 'suburban'
default: true
},
bus: {
bitmask: 32,
{
id: 'bus',
mode: 'bus',
bitmasks: [32],
name: 'Bus',
short: 'B',
mode: 'bus',
product: 'bus'
default: true
},
ferry: {
bitmask: 64,
{
id: 'ferry',
mode: 'watercraft',
bitmasks: [64],
name: 'Ferry',
short: 'F',
mode: 'watercraft',
product: 'ferry'
default: true
},
subway: {
bitmask: 128,
{
id: 'subway',
mode: 'train',
bitmasks: [128],
name: 'U-Bahn',
short: 'U',
mode: 'train',
product: 'subway'
default: true
},
tram: {
bitmask: 256,
{
id: 'tram',
mode: 'train',
bitmasks: [256],
name: 'Tram',
short: 'T',
mode: 'train',
product: 'tram'
default: true
},
onCall: {
bitmask: 512,
{
id: 'onCall',
mode: 'bus', // todo: is this correct?
bitmasks: [512],
name: 'On-call transit',
short: 'on-call',
mode: null, // todo
product: 'onCall'
default: true
}
}
p.bitmasks = []
p.bitmasks[1] = p.nationalExp
p.bitmasks[2] = p.national
p.bitmasks[4] = p.interregional
p.bitmasks[8] = p.regional
p.bitmasks[16] = p.suburban
p.bitmasks[32] = p.bus
p.bitmasks[64] = p.ferry
p.bitmasks[128] = p.subway
p.bitmasks[256] = p.tram
p.bitmasks[512] = p.onCall
p.allProducts = [
p.nationalExp,
p.national,
p.interregional,
p.regional,
p.suburban,
p.bus,
p.ferry,
p.subway,
p.tram,
p.onCall
]
module.exports = p

View file

@ -9,7 +9,7 @@ const createClient = require('hafas-client')
const nahshProfile = require('hafas-client/p/nahsh')
// create a client with NAH.SH profile
const client = createClient(nahshProfile)
const client = createClient(nahshProfile, 'my-awesome-program')
```

View file

@ -3,15 +3,25 @@
const createClient = require('../..')
const oebbProfile = require('.')
const client = createClient(oebbProfile)
const client = createClient(oebbProfile, 'hafas-client-example')
// Wien Westbahnhof to Salzburg Hbf
client.journeys('1291501', '8100002', {results: 1})
// client.departures('8100002', {duration: 1})
// client.arrivals('8100002', {duration: 10, stationLines: true})
// client.locations('Salzburg', {results: 2})
// client.location('8100173') // Graz Hbf
// client.nearby(47.812851, 13.045604, {distance: 60})
// client.radar(47.827203, 13.001261, 47.773278, 13.07562, {results: 10})
// client.station('8100173') // Graz Hbf
// client.nearby({
// type: 'location',
// latitude: 47.812851,
// longitude: 13.045604
// }, {distance: 60})
// client.radar({
// north: 47.827203,
// west: 13.001261,
// south: 47.773278,
// east: 13.07562
// }, {results: 10})
.then((data) => {
console.log(require('util').inspect(data, {depth: null}))

View file

@ -3,9 +3,6 @@
// todo: https://gist.github.com/anonymous/a5fc856bc80ae7364721943243f934f4#file-haf_config_base-properties-L5
// todo: https://gist.github.com/anonymous/a5fc856bc80ae7364721943243f934f4#file-haf_config_base-properties-L47-L234
const createParseBitmask = require('../../parse/products-bitmask')
const createFormatBitmask = require('../../format/products-bitmask')
const _createParseLine = require('../../parse/line')
const _parseLocation = require('../../parse/location')
const _createParseMovement = require('../../parse/movement')
@ -28,32 +25,12 @@ const transformReqBody = (body) => {
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 = products.bitmasks[parseInt(res.class)]
if (data) {
res.mode = data.mode
res.product = data.product
}
}
return res
}
return parseLineWithMode
}
const parseLocation = (profile, l, lines) => {
const parseLocation = (profile, opt, data, l) => {
// ÖBB has some 'stations' **in austria** with no departures/products,
// like station entrances, that are actually POIs.
const res = _parseLocation(profile, l, lines)
const res = _parseLocation(profile, opt, data, l)
if (
res.type === 'station' &&
(res.type === 'station' || res.type === 'stop') &&
!res.products &&
res.name &&
res.id && res.id.length !== 7
@ -67,8 +44,8 @@ const parseLocation = (profile, l, lines) => {
return res
}
const createParseMovement = (profile, locations, lines, remarks) => {
const _parseMovement = _createParseMovement(profile, locations, lines, remarks)
const createParseMovement = (profile, opt, data) => {
const _parseMovement = _createParseMovement(profile, opt, data)
const parseMovement = (m) => {
const res = _parseMovement(m)
// filter out POIs
@ -82,28 +59,6 @@ const createParseMovement = (profile, locations, lines, remarks) => {
return parseMovement
}
const defaultProducts = {
nationalExp: true,
national: true,
interregional: true,
regional: true,
suburban: true,
bus: true,
ferry: true,
subway: true,
tram: true,
onCall: true
}
const formatBitmask = createFormatBitmask(products)
const formatProducts = (products) => {
products = Object.assign(Object.create(null), defaultProducts, products)
return {
type: 'PROD',
mode: 'INC',
value: formatBitmask(products) + ''
}
}
const oebbProfile = {
locale: 'de-AT',
timezone: 'Europe/Vienna',
@ -111,16 +66,12 @@ const oebbProfile = {
endpoint: 'http://fahrplan.oebb.at/bin/mgate.exe',
transformReqBody,
products: products.allProducts,
products: products,
parseProducts: createParseBitmask(products.allProducts, defaultProducts),
parseLine: createParseLine,
parseLocation,
parseMovement: createParseMovement,
formatProducts,
journeyLeg: true,
trip: true,
radar: true
}

View file

@ -1,112 +1,84 @@
'use strict'
const p = {
nationalExp: {
bitmask: 1,
module.exports = [
{
id: 'nationalExp',
mode: 'train',
bitmasks: [1],
name: 'InterCityExpress & RailJet',
short: 'ICE/RJ',
mode: 'train',
product: 'nationalExp'
default: true
},
national: {
bitmask: 2 + 4,
{
id: 'national',
mode: 'train',
bitmasks: [2, 4],
name: 'InterCity & EuroCity',
short: 'IC/EC',
mode: 'train',
product: 'national'
default: true
},
interregional: {
bitmask: 8 + 4096,
{
id: 'interregional',
mode: 'train',
bitmasks: [8, 4096],
name: 'Durchgangszug & EuroNight',
short: 'D/EN',
mode: 'train',
product: 'interregional'
default: true
},
regional: {
bitmask: 16,
{
id: 'regional',
mode: 'train',
bitmasks: [16],
name: 'Regional & RegionalExpress',
short: 'R/REX',
mode: 'train',
product: 'regional'
default: true
},
suburban: {
bitmask: 32,
{
id: 'suburban',
mode: 'train',
bitmasks: [32],
name: 'S-Bahn',
short: 'S',
mode: 'train',
product: 'suburban'
default: true
},
bus: {
bitmask: 64,
{
id: 'bus',
mode: 'bus',
bitmasks: [64],
name: 'Bus',
short: 'B',
mode: 'bus',
product: 'bus'
default: true
},
ferry: {
bitmask: 128,
{
id: 'ferry',
mode: 'watercraft',
bitmasks: [128],
name: 'Ferry',
short: 'F',
mode: 'watercraft',
product: 'ferry'
default: true
},
subway: {
bitmask: 256,
{
id: 'subway',
mode: 'train',
bitmasks: [256],
name: 'U-Bahn',
short: 'U',
mode: 'train',
product: 'subway'
default: true
},
tram: {
bitmask: 512,
{
id: 'tram',
mode: 'train',
bitmasks: [512],
name: 'Tram',
short: 'T',
mode: 'train',
product: 'tram'
default: true
},
onCall: {
bitmask: 2048,
{
id: 'onCall',
mode: null, // todo
bitmasks: [2048],
name: 'On-call transit',
short: 'on-call',
mode: null, // todo
product: 'onCall'
},
unknown: {
bitmask: 0,
name: 'unknown',
short: '?',
product: 'unknown'
default: true
}
}
p.bitmasks = []
p.bitmasks[1] = p.nationalExp
p.bitmasks[2] = p.national
p.bitmasks[4] = p.national
p.bitmasks[2+4] = p.national
p.bitmasks[8] = p.interregional
p.bitmasks[16] = p.regional
p.bitmasks[32] = p.suburban
p.bitmasks[64] = p.bus
p.bitmasks[128] = p.ferry
p.bitmasks[256] = p.subway
p.bitmasks[512] = p.tram
p.bitmasks[1024] = p.unknown
p.bitmasks[2048] = p.onCall
p.bitmasks[4096] = p.interregional
p.bitmasks[8+4096] = p.interregional
p.allProducts = [
p.nationalExp,
p.national,
p.interregional,
p.regional,
p.suburban,
p.bus,
p.ferry,
p.subway,
p.tram,
p.onCall
]
module.exports = p

View file

@ -9,11 +9,11 @@ const createClient = require('hafas-client')
const oebbProfile = require('hafas-client/p/oebb')
// create a client with ÖBB profile
const client = createClient(oebbProfile)
const client = createClient(oebbProfile, 'my-awesome-program')
```
## Customisations
- parses *ÖBB*-specific products (such as *RailJet*)
- parses invalid empty stations from the API as [`location`](https://github.com/public-transport/friendly-public-transport-format/blob/1.0.1/spec/readme.md#location-objects)s
- parses invalid empty stations from the API as [`location`](https://github.com/public-transport/friendly-public-transport-format/blob/1.1.1/spec/readme.md#location-objects)s

View file

@ -1,6 +1,6 @@
# 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.
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 [`trip`](../docs/trip.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:

View file

@ -3,19 +3,33 @@
const createClient = require('../..')
const vbbProfile = require('.')
const client = createClient(vbbProfile)
const client = createClient(vbbProfile, 'hafas-client-example')
// Hauptbahnhof to Charlottenburg
client.journeys('900000003201', '900000024101', {results: 1, polylines: true})
// client.departures('900000013102', {duration: 1})
// client.arrivals('900000013102', {duration: 10, stationLines: true})
// client.locations('Alexanderplatz', {results: 2})
// client.location('900000042101') // Spichernstr
// client.nearby(52.5137344, 13.4744798, {distance: 60})
// client.radar(52.52411, 13.41002, 52.51942, 13.41709, {results: 10})
// client.station('900000042101', {stationLines: true}) // Spichernstr
// client.nearby({
// type: 'location',
// latitude: 52.5137344,
// longitude: 13.4744798
// }, {distance: 60})
// client.radar({
// north: 52.52411,
// west: 13.41002,
// south: 52.51942,
// east: 13.41709
// }, {results: 10})
// .then(([journey]) => {
// const leg = journey.legs[0]
// return client.journeyLeg(leg.id, leg.line.name, {polyline: true})
// return client.trip(leg.id, leg.line.name, {polyline: true})
// })
// .then(([journey]) => {
// return client.refreshJourney(journey.refreshToken, {stopovers: true, remarks: true})
// })
.then((data) => {
console.log(require('util').inspect(data, {depth: null}))

View file

@ -11,12 +11,8 @@ const _parseLocation = require('../../parse/location')
const _createParseJourney = require('../../parse/journey')
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 products = require('./products')
const transformReqBody = (body) => {
body.client = {type: 'IPA', id: 'VBB', name: 'vbbPROD', v: '4010300'}
@ -27,21 +23,12 @@ const transformReqBody = (body) => {
return body
}
const createParseLine = (profile, operators) => {
const parseLine = _createParseLine(profile, operators)
const createParseLine = (profile, opt, data) => {
const parseLine = _createParseLine(profile, opt, data)
const parseLineWithMode = (l) => {
const parseLineWithMoreDetails = (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
}
}
res.name = l.name.replace(/^(bus|tram)\s+/i, '')
const details = parseLineName(res.name)
res.symbol = details.symbol
@ -52,13 +39,13 @@ const createParseLine = (profile, operators) => {
return res
}
return parseLineWithMode
return parseLineWithMoreDetails
}
const parseLocation = (profile, l, lines) => {
const res = _parseLocation(profile, l, lines)
const parseLocation = (profile, opt, data, l) => {
const res = _parseLocation(profile, opt, data, l)
if (res.type === 'station') {
if (res.type === 'stop' || res.type === 'station') {
res.name = shorten(res.name)
res.id = to12Digit(res.id)
if (!res.location.latitude || !res.location.longitude) {
@ -69,8 +56,8 @@ const parseLocation = (profile, l, lines) => {
return res
}
const createParseJourney = (profile, stations, lines, remarks, polylines) => {
const parseJourney = _createParseJourney(profile, stations, lines, remarks, polylines)
const createParseJourney = (profile, opt, data) => {
const parseJourney = _createParseJourney(profile, opt, data)
const parseJourneyWithTickets = (j) => {
const res = parseJourney(j)
@ -99,8 +86,8 @@ const createParseJourney = (profile, stations, lines, remarks, polylines) => {
return parseJourneyWithTickets
}
const createParseDeparture = (profile, stations, lines, remarks) => {
const parseDeparture = _createParseDeparture(profile, stations, lines, remarks)
const createParseDeparture = (profile, opt, data) => {
const parseDeparture = _createParseDeparture(profile, opt, data)
const ringbahnClockwise = /^ringbahn s\s?41$/i
const ringbahnAnticlockwise = /^ringbahn s\s?42$/i
@ -132,24 +119,6 @@ const formatStation = (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',
@ -162,20 +131,18 @@ const vbbProfile = {
transformReqBody,
products: modes.allProducts,
products: products,
parseStationName: shorten,
parseLocation,
parseLine: createParseLine,
parseProducts: createParseBitmask(modes.allProducts, defaultProducts),
parseJourney: createParseJourney,
parseDeparture: createParseDeparture,
formatStation,
formatProducts,
journeysNumF: false,
journeyLeg: true,
trip: true,
radar: true
}

View file

@ -1,112 +0,0 @@
'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

60
p/vbb/products.js Normal file
View file

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

View file

@ -9,7 +9,7 @@ const createClient = require('hafas-client')
const vbbProfile = require('hafas-client/p/vbb')
// create a client with VBB profile
const client = createClient(vbbProfile)
const client = createClient(vbbProfile, 'my-awesome-program')
```

View file

@ -32,11 +32,14 @@
"node": ">=6"
},
"dependencies": {
"@mapbox/polyline": "^1.0.0",
"br2nl": "^1.0.0",
"capture-stack-trace": "^1.0.0",
"create-hash": "^1.2.0",
"fetch-ponyfill": "^6.0.0",
"gps-distance": "0.0.4",
"lodash": "^4.17.5",
"luxon": "^1.2.1",
"luxon": "^1.3.0",
"p-throttle": "^1.1.0",
"pinkie-promise": "^2.0.1",
"query-string": "^6.0.0",
@ -51,14 +54,15 @@
"db-stations": "^2.3.0",
"is-coordinates": "^2.0.2",
"is-roughly-equal": "^0.1.0",
"tap-spec": "^4.1.1",
"tap-spec": "^5.0.0",
"tape": "^4.8.0",
"tape-promise": "^3.0.0",
"validate-fptf": "^1.2.1",
"validate-fptf": "^2.0.1",
"vbb-stations-autocomplete": "^3.1.0"
},
"scripts": {
"test": "env NODE_ENV=dev node test/index.js",
"prepublishOnly": "npm test | tap-spec"
"test": "env NODE_ENV=dev NODE_DEBUG=hafas-client node test/index.js",
"prepublishOnly": "npm test | tap-spec",
"install": "lib/generate-install-id.js >id.json"
}
}

View file

@ -0,0 +1,68 @@
'use strict'
const findRemark = require('./find-remark')
// todo: what is d.jny.dirFlg?
// todo: d.stbStop.dProgType/d.stbStop.aProgType
const createParseArrOrDep = (profile, opt, data, prefix) => {
const {locations, lines, hints, warnings} = data
if (prefix !== 'a' && prefix !== 'd') throw new Error('invalid prefix')
const parseArrOrDep = (d) => {
const t = d.stbStop[prefix + 'TimeR'] || d.stbStop[prefix + 'TimeS']
const when = profile.parseDateTime(profile, d.date, t)
const res = {
tripId: d.jid,
stop: locations[parseInt(d.stbStop.locX)] || null,
when: when.toISO(),
direction: profile.parseStationName(d.dirTxt),
line: lines[parseInt(d.prodX)] || null,
remarks: [],
// todo: res.trip from rawLine.prodCtx.num?
trip: +d.jid.split('|')[1] // todo: this seems brittle
}
// todo: DRY with parseStopover
// todo: DRY with parseJourneyLeg
const tR = d.stbStop[prefix + 'TimeR']
const tP = d.stbStop[prefix + 'TimeS']
if (tR && tP) {
const realtime = profile.parseDateTime(profile, d.date, tR)
const planned = profile.parseDateTime(profile, d.date, tP)
res.delay = Math.round((realtime - planned) / 1000)
} else res.delay = null
// todo: DRY with parseStopover
// todo: DRY with parseJourneyLeg
const pR = d.stbStop[prefix + 'PlatfR']
const pP = d.stbStop[prefix + 'PlatfS']
res.platform = pR || pP || null
if (pR && pP && pR !== pP) res.formerScheduledPlatform = pP
// todo: DRY with parseStopover
// todo: DRY with parseJourneyLeg
if (d.stbStop[prefix + 'Cncl']) {
res.cancelled = true
Object.defineProperty(res, 'canceled', {value: true})
res.when = res.delay = null
const when = profile.parseDateTime(profile, d.date, tP)
res.formerScheduledWhen = when.toISO()
}
if (opt.remarks) {
res.remarks = []
.concat(d.remL || [], d.msgL || [])
.map(ref => findRemark(hints, warnings, ref))
.filter(rem => !!rem) // filter unparsable
}
return res
}
return parseArrOrDep
}
module.exports = createParseArrOrDep

10
parse/arrival.js Normal file
View file

@ -0,0 +1,10 @@
'use strict'
const createParseArrOrDep = require('./arrival-or-departure')
const ARRIVAL = 'a'
const createParseArrival = (profile, opt, data) => {
return createParseArrOrDep(profile, opt, data, ARRIVAL)
}
module.exports = createParseArrival

View file

@ -2,8 +2,6 @@
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]) {

View file

@ -1,53 +1,10 @@
'use strict'
// todo: what is d.jny.dirFlg?
// todo: d.stbStop.dProgType
// todo: d.freq, d.freq.jnyL, see https://github.com/public-transport/hafas-client/blob/9203ed1481f08baacca41ac5e3c19bf022f01b0b/parse.js#L115
const createParseArrOrDep = require('./arrival-or-departure')
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?
// todo: DRY with parseStopover
// todo: DRY with parseJourneyLeg
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: DRY with parseStopover
// todo: DRY with parseJourneyLeg
res.platform = d.stbStop.dPlatfR || d.stbStop.dPlatfS || null
// todo: `formerScheduledPlatform`
// todo: DRY with parseStopover
// todo: DRY with parseJourneyLeg
if (d.stbStop.aCncl || d.stbStop.dCncl) {
res.cancelled = true
Object.defineProperty(res, 'canceled', {value: true})
res.when = res.delay = null
const when = profile.parseDateTime(profile, d.date, d.stbStop.dTimeS)
res.formerScheduledWhen = when.toISO()
}
return res
}
return parseDeparture
const DEPARTURE = 'd'
const createParseDeparture = (profile, opt, data) => {
return createParseArrOrDep(profile, opt, data, DEPARTURE)
}
module.exports = createParseDeparture

17
parse/find-remark.js Normal file
View file

@ -0,0 +1,17 @@
'use strict'
// There are two kinds of notes: "remarks" (in `remL`) and HAFAS
// Information Manager (HIM) notes (in `himL`). The former describe
// the regular operating situation, e.g. "bicycles allows", whereas
// the latter describe cancellations, construction work, etc.
// hafas-client's naming scheme:
// - hints: notes from `remL` for regular operation
// - warnings: notes from `himL` for cancellations, construction, etc
// - remarks: both "notes" and "warnings"
const findRemark = (hints, warnings, ref) => {
return warnings[ref.himX] || hints[ref.remX] || null
}
module.exports = findRemark

248
parse/hint.js Normal file
View file

@ -0,0 +1,248 @@
'use strict'
const hints = Object.assign(Object.create(null), {
fb: {
type: 'hint',
code: 'bicycle-conveyance',
summary: 'bicycles conveyed'
},
fr: {
type: 'hint',
code: 'bicycle-conveyance-reservation',
summary: 'bicycles conveyed, subject to reservation'
},
nf: {
type: 'hint',
code: 'no-bicycle-conveyance',
summary: 'bicycles not conveyed'
},
k2: {
type: 'hint',
code: '2nd-class-only',
summary: '2. class only'
},
eh: {
type: 'hint',
code: 'boarding-ramp',
summary: 'vehicle-mounted boarding ramp available'
},
ro: {
type: 'hint',
code: 'wheelchairs-space',
summary: 'space for wheelchairs'
},
oa: {
type: 'hint',
code: 'wheelchairs-space-reservation',
summary: 'space for wheelchairs, subject to reservation'
},
wv: {
type: 'hint',
code: 'wifi',
summary: 'WiFi available'
},
wi: {
type: 'hint',
code: 'wifi',
summary: 'WiFi available'
},
sn: {
type: 'hint',
code: 'snacks',
summary: 'snacks available for purchase'
},
mb: {
type: 'hint',
code: 'snacks',
summary: 'snacks available for purchase'
},
mp: {
type: 'hint',
code: 'snacks',
summary: 'snacks available for purchase at the seat'
},
bf: {
type: 'hint',
code: 'barrier-free',
summary: 'barrier-free'
},
rg: {
type: 'hint',
code: 'barrier-free-vehicle',
summary: 'barrier-free vehicle'
},
bt: {
type: 'hint',
code: 'on-board-bistro',
summary: 'Bordbistro available'
},
br: {
type: 'hint',
code: 'on-board-restaurant',
summary: 'Bordrestaurant available'
},
ki: {
type: 'hint',
code: 'childrens-area',
summary: `children's area available`
},
kk: {
type: 'hint',
code: 'parents-childrens-compartment',
summary: `parent-and-children compartment available`
},
kr: {
type: 'hint',
code: 'kids-service',
summary: 'DB Kids Service available'
},
ls: {
type: 'hint',
code: 'power-sockets',
summary: 'power sockets available'
},
ev: {
type: 'hint',
code: 'replacement-service',
summary: 'replacement service'
},
kl: {
type: 'hint',
code: 'air-conditioned',
summary: 'air-conditioned vehicle'
},
r0: {
type: 'hint',
code: 'upward-escalator',
summary: 'upward escalator'
},
au: {
type: 'hint',
code: 'elevator',
summary: 'elevator available'
},
ck: {
type: 'hint',
code: 'komfort-checkin',
summary: 'Komfort-Checkin available'
},
it: {
type: 'hint',
code: 'ice-sprinter',
summary: 'ICE Sprinter service'
},
rp: {
type: 'hint',
code: 'compulsory-reservation',
summary: 'compulsory seat reservation'
},
rm: {
type: 'hint',
code: 'optional-reservation',
summary: 'optional seat reservation'
},
scl: {
type: 'hint',
code: 'all-2nd-class-seats-reserved',
summary: 'all 2nd class seats reserved'
},
acl: {
type: 'hint',
code: 'all-seats-reserved',
summary: 'all seats reserved'
},
sk: {
type: 'hint',
code: 'oversize-luggage-forbidden',
summary: 'oversize luggage not allowed'
},
hu: {
type: 'hint',
code: 'animals-forbidden',
summary: 'animals not allowed, except guide dogs'
},
ik: {
type: 'hint',
code: 'baby-cot-required',
summary: 'baby cot/child seat required'
},
ee: {
type: 'hint',
code: 'on-board-entertainment',
summary: 'on-board entertainment available'
},
toilet: {
type: 'hint',
code: 'toilet',
summary: 'toilet available'
},
oc: {
type: 'hint',
code: 'wheelchair-accessible-toilet',
summary: 'wheelchair-accessible toilet available'
},
iz: {
type: 'hint',
code: 'intercity-2',
summary: 'Intercity 2'
}
})
const codesByIcon = Object.assign(Object.create(null), {
cancel: 'cancelled'
})
// todo: is passing in profile necessary?
const parseHint = (profile, h, icons) => {
// todo: C
// todo:
// { type: 'Q',
// code: '',
// icoX: 11,
// txtN:
// 'RE 3132: Berlin Zoologischer Garten - Brandenburg Hbf: Information. A railway carriage is missing',
// sIdx: 4 }
const text = h.txtN && h.txtN.trim() || ''
const icon = 'number' === typeof h.icoX && icons[h.icoX] || null
const code = h.code || (icon && icon.res && codesByIcon[icon.res]) || null
if (h.type === 'M') {
return {
type: 'status',
summary: h.txtS && h.txtS.trim() || '',
code,
text
}
}
if (h.type === 'L') {
return {
type: 'status',
code: 'alternative-trip',
text,
tripId: h.jid
}
}
if (h.type === 'A') {
return {
type: 'hint',
code: h.code || null,
text: h.txtN || null
}
}
if (h.type === 'D' || h.type === 'U' || h.type === 'R' || h.type === 'N') {
// todo: how can we identify the individual types?
// todo: does `D` mean "disturbance"?
return {
type: 'status',
code,
text
}
}
return null
}
module.exports = parseHint

View file

@ -4,7 +4,7 @@ module.exports = {
dateTime: require('./date-time'),
location: require('./location'),
line: require('./line'),
remark: require('./remark'),
hint: require('./hint'),
operator: require('./operator'),
stopover: require('./stopover'),
journeyLeg: require('./journey-leg'),

View file

@ -1,24 +1,61 @@
'use strict'
const parseDateTime = require('./date-time')
const findRemark = require('./find-remark')
const clone = obj => Object.assign({}, obj)
const createParseJourneyLeg = (profile, stations, lines, remarks, polylines) => {
// todo: finish parse/remark.js first
const applyRemark = (j, rm) => {}
const locX = Symbol('locX')
const applyRemarks = (leg, hints, warnings, refs) => {
for (let ref of refs) {
const remark = findRemark(hints, warnings, ref)
if (!remark) continue
if ('number' === typeof ref.fLocX && 'number' === typeof ref.tLocX) {
const fromI = leg.stopovers.findIndex(s => s[locX] === ref.fLocX)
const toI = leg.stopovers.findIndex(s => s[locX] === ref.tLocX)
if (fromI < 0 || toI < 0) continue
const wholeLeg = fromI === 0 && toI === (leg.stopovers.length - 1)
if (!wholeLeg) {
for (let i = fromI; i <= toI; i++) {
const stopover = leg.stopovers[i]
if (!stopover) continue
if (Array.isArray(stopover.remarks)) {
stopover.remarks.push(remark)
} else {
stopover.remarks = [remark]
}
}
continue
}
}
if (Array.isArray(leg.remarks)) leg.remarks.push(remark)
else leg.remarks = [remark]
// todo: `ref.tagL`
}
}
const createParseJourneyLeg = (profile, opt, data) => {
const {locations, lines, hints, warnings, polylines} = data
// todo: pt.status
// todo: pt.status, pt.isPartCncl
// todo: pt.isRchbl, pt.chRatingRT, pt.chgDurR, pt.minChg
// 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
// j = journey, pt = part
// todo: pt.planrtTS
const parseJourneyLeg = (j, pt, parseStopovers = true) => {
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)]),
origin: clone(locations[parseInt(pt.dep.locX)]) || null,
destination: clone(locations[parseInt(pt.arr.locX)]),
departure: dep.toISO(),
arrival: arr.toISO()
}
@ -38,32 +75,48 @@ const createParseJourneyLeg = (profile, stations, lines, remarks, polylines) =>
if (pt.jny && pt.jny.polyG) {
let p = pt.jny.polyG.polyXL
p = p && polylines[p[0]]
p = Array.isArray(p) && polylines[p[0]]
// todo: there can be >1 polyline
res.polyline = p && p.crdEncYX || null
const parse = profile.parsePolyline(profile, opt, data)
res.polyline = p && parse(p) || null
}
if (pt.type === 'WALK' || pt.type === 'TRSF') {
res.mode = 'walking'
res.public = true
res.distance = pt.gis && pt.gis.dist || null
if (pt.type === 'TRSF') res.transfer = true
if (opt.remarks && Array.isArray(pt.gis.msgL)) {
applyRemarks(res, hints, warnings, pt.gis.msgL)
}
} 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)
res.direction = profile.parseStationName(pt.jny.dirTxt) || null
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.date)
const passedStations = pt.jny.stopL.map(parse)
if (parseStopovers && pt.jny.stopL) {
const parse = profile.parseStopover(profile, opt, data, j.date)
const stopL = pt.jny.stopL
res.stopovers = stopL.map(parse)
// todo: is there a `pt.jny.remL`?
if (opt.remarks && Array.isArray(pt.jny.msgL)) {
for (let i = 0; i < stopL.length; i++) {
Object.defineProperty(res.stopovers[i], locX, {
value: stopL[i].locX
})
}
// todo: apply leg-wide remarks if `parseStopovers` is false
applyRemarks(res, hints, warnings, pt.jny.msgL)
}
// filter stations the train passes without stopping, as this doesn't comply with fptf (yet)
res.passed = passedStations.filter((x) => !x.passBy)
}
if (Array.isArray(pt.jny.remL)) {
for (let remark of pt.jny.remL) applyRemark(j, remark)
res.stopovers = res.stopovers.filter((x) => !x.passBy)
}
const freq = pt.jny.freq || {}

View file

@ -1,36 +1,35 @@
'use strict'
const clone = obj => Object.assign({}, obj)
const findRemark = require('./find-remark')
const createParseJourney = (profile, stations, lines, remarks, polylines) => {
const parseLeg = profile.parseJourneyLeg(profile, stations, lines, remarks, polylines)
const createParseJourney = (profile, opt, data) => {
const parseLeg = profile.parseJourneyLeg(profile, opt, data)
const {hints, warnings} = data
// todo: c.sDays
// todo: c.dep.dProgType, c.arr.dProgType
// todo: c.conSubscr
// todo: c.trfRes x vbb-parse-ticket
// todo: c.sotRating, c.isSotCon, c.sotCtxt
// todo: c.showARSLink
// todo: c.useableTime
// todo: c.cksum
// todo: c.isNotRdbl
// todo: c.badSecRefX
// todo: c.bfATS, c.bfIOSTS
const parseJourney = (j) => {
const legs = j.secL.map(leg => parseLeg(j, leg))
const res = {
type: 'journey',
legs,
origin: legs[0].origin,
destination: legs[legs.length - 1].destination,
departure: legs[0].departure,
arrival: legs[legs.length - 1].arrival
refreshToken: j.ctxRecon || null
}
if (legs.some(p => p.cancelled)) {
res.cancelled = true
Object.defineProperty(res, 'canceled', {value: true})
res.departure = res.arrival = null
const firstLeg = j.secL[0]
const dep = profile.parseDateTime(profile, j.date, firstLeg.dep.dTimeS)
res.formerScheduledDeparture = dep.toISO()
const lastLeg = j.secL[j.secL.length - 1]
const arr = profile.parseDateTime(profile, j.date, lastLeg.arr.aTimeS)
res.formerScheduledArrival = arr.toISO()
if (opt.remarks && Array.isArray(j.msgL)) {
res.remarks = []
for (let ref of j.msgL) {
const remark = findRemark(hints, warnings, ref)
if (remark) res.remarks.push(remark)
}
}
return res

View file

@ -2,8 +2,14 @@
const slugg = require('slugg')
// todo: are p.number and p.line ever different?
const createParseLine = (profile, operators) => {
const createParseLine = (profile, opt, {operators}) => {
const byBitmask = []
for (let product of profile.products) {
for (let bitmask of product.bitmasks) {
byBitmask[bitmask] = product
}
}
const parseLine = (p) => {
if (!p) return null // todo: handle this upstream
const res = {
@ -13,8 +19,9 @@ const createParseLine = (profile, operators) => {
public: true
}
// todo: what is p.prodCtx && p.prodCtx.num?
// todo: what is p.number?
// This is terrible, but FPTF demands an ID. Let's pray for VBB to expose an ID.
// This is terrible, but FPTF demands an ID. Let's pray for HaCon to expose an ID.
// todo: find a better way
if (p.line) res.id = slugg(p.line.trim())
else if (p.name) res.id = slugg(p.name.trim())
@ -24,7 +31,12 @@ const createParseLine = (profile, operators) => {
res.productCode = +p.prodCtx.catCode
}
// todo: parse mode, remove from profiles
if ('class' in res) {
// todo: what if `res.class` is the sum of two bitmasks?
const product = byBitmask[parseInt(res.class)]
res.mode = product && product.mode || null
res.product = product && product.id || null
}
if ('number' === typeof p.oprX) {
res.operator = operators[p.oprX] || null

View file

@ -5,10 +5,7 @@ const STATION = 'S'
const ADDRESS = 'A'
// todo: what is s.rRefL?
// todo: is passing in profile necessary?
// todo: [breaking] change to createParseLocation(profile, lines) => (l) => loc
const parseLocation = (profile, l, lines) => {
const parseLocation = (profile, opt, {lines}, l) => {
const res = {type: 'location'}
if (l.crd) {
res.latitude = l.crd.y / 1000000
@ -16,24 +13,28 @@ const parseLocation = (profile, l, lines) => {
}
if (l.type === STATION) {
const station = {
type: 'station',
const stop = {
type: l.isMainMast ? 'station' : 'stop',
id: l.extId,
name: profile.parseStationName(l.name),
name: l.name ? profile.parseStationName(l.name) : null,
location: 'number' === typeof res.latitude ? res : null
}
if ('pCls' in l) station.products = profile.parseProducts(l.pCls)
if ('pCls' in l) stop.products = profile.parseProducts(l.pCls)
if (Array.isArray(l.pRefL) && Array.isArray(lines)) {
station.lines = []
if (
opt.stationLines &&
Array.isArray(l.pRefL) &&
Array.isArray(lines)
) {
stop.lines = []
for (let pRef of l.pRefL) {
const line = lines[pRef]
if (line) station.lines.push(line)
if (line) stop.lines.push(line)
}
}
return station
return stop
}
if (l.type === ADDRESS) res.address = l.name

View file

@ -1,6 +1,8 @@
'use strict'
const createParseMovement = (profile, locations, lines, remarks, polylines = []) => {
const createParseMovement = (profile, opt, data) => {
const {locations, lines, polylines} = data
// todo: what is m.dirGeo? maybe the speed?
// todo: what is m.stopL?
// todo: what is m.proc? wut?
@ -8,11 +10,11 @@ const createParseMovement = (profile, locations, lines, remarks, polylines = [])
// todo: what is m.ani.dirGeo[n]? maybe the speed?
// todo: what is m.ani.proc[n]? wut?
const parseMovement = (m) => {
const pStopover = profile.parseStopover(profile, locations, lines, remarks, m.date)
const pStopover = profile.parseStopover(profile, opt, data, m.date)
const res = {
direction: profile.parseStationName(m.dirTxt),
journeyId: m.jid || null,
tripId: m.jid || null,
trip: m.jid && +m.jid.split('|')[1] || null, // todo: this seems brittle
line: lines[m.prodX] || null,
location: m.pos ? {
@ -35,12 +37,15 @@ const createParseMovement = (profile, locations, lines, remarks, polylines = [])
}
}
if (m.ani.poly && m.ani.poly.crdEncYX) {
res.polyline = m.ani.poly.crdEncYX
} else if (m.ani.polyG && Array.isArray(m.ani.polyG.polyXL)) {
let p = m.ani.polyG.polyXL[0]
if (m.ani.poly) {
const parse = profile.parsePolyline(profile, opt, data)
res.polyline = parse(m.ani.poly)
} else if (m.ani.polyG) {
let p = m.ani.polyG.polyXL
p = Array.isArray(p) && polylines[p[0]]
// todo: there can be >1 polyline
res.polyline = polylines[p] && polylines[p].crdEncYX || null
const parse = profile.parsePolyline(profile, opt, data)
res.polyline = p && parse(p) || null
}
}

View file

@ -6,9 +6,9 @@
// todo: what is s.wt?
// todo: what is s.dur?
// todo: [breaking] change to createParseNearby(profile, lines) => (n) => nearby
const parseNearby = (profile, n, lines) => {
const res = profile.parseLocation(profile, n, lines)
// todo: [breaking] change to createParseNearby(profile, data) => (n) => nearby
const parseNearby = (profile, opt, data, n) => {
const res = profile.parseLocation(profile, opt, data, n)
res.distance = n.dist
return res
}

View file

@ -2,7 +2,6 @@
const slugg = require('slugg')
// todo: is passing in profile necessary?
const parseOperator = (profile, a) => {
return {
type: 'operator',

53
parse/polyline.js Normal file
View file

@ -0,0 +1,53 @@
'use strict'
const {toGeoJSON} = require('@mapbox/polyline')
const distance = require('gps-distance')
const createParsePolyline = (profile, opt, {locations}) => {
// todo: what is p.delta?
// todo: what is p.type?
// todo: what is p.crdEncS?
// todo: what is p.crdEncF?
const parsePolyline = (p) => {
const shape = toGeoJSON(p.crdEncYX)
if (shape.coordinates.length === 0) return null
const res = shape.coordinates.map(crd => ({
type: 'Feature',
properties: {},
geometry: {
type: 'Point',
coordinates: crd
}
}))
if (Array.isArray(p.ppLocRefL)) {
for (let ref of p.ppLocRefL) {
const p = res[ref.ppIdx]
const loc = locations[ref.locX]
if (p && loc) p.properties = loc
}
// Often there is one more point right next to each point at a station.
// We filter them here if they are < 5m from each other.
for (let i = 1; i < res.length; i++) {
const p1 = res[i - 1].geometry.coordinates
const p2 = res[i].geometry.coordinates
const d = distance(p1[1], p1[0], p2[1], p2[0])
if (d >= .005) continue
const l1 = Object.keys(res[i - 1].properties).length
const l2 = Object.keys(res[i].properties).length
if (l1 === 0 && l2 > 0) res.splice(i - 1, 1)
else if (l2 === 0 && l1 > 0) res.splice(i, 1)
}
}
return {
type: 'FeatureCollection',
features: res
}
}
return parsePolyline
}
module.exports = createParsePolyline

View file

@ -1,28 +1,26 @@
'use strict'
const createParseBitmask = (allProducts, defaultProducts) => {
allProducts = allProducts.sort((p1, p2) => p2.bitmask - p1.bitmask) // desc
if (allProducts.length === 0) throw new Error('allProducts is empty.')
for (let product of allProducts) {
if ('string' !== typeof product.product) {
throw new Error('allProducts[].product must be a string.')
}
if ('number' !== typeof product.bitmask) {
throw new Error(product.product + '.bitmask must be a number.')
const createParseBitmask = (profile) => {
const defaultProducts = {}
let withBitmask = []
for (let product of profile.products) {
defaultProducts[product.id] = false
for (let bitmask of product.bitmasks) {
withBitmask.push([bitmask, product])
}
}
withBitmask.sort((a, b) => b[0] - a[0]) // descending
const parseBitmask = (bitmask) => {
const res = Object.assign({}, defaultProducts)
for (let product of allProducts) {
if (bitmask === 0) break
if ((product.bitmask & bitmask) > 0) {
res[product.product] = true
bitmask -= product.bitmask
for (let [pBitmask, product] of withBitmask) {
if ((pBitmask & bitmask) > 0) {
res[product.id] = true
bitmask -= pBitmask
}
else{
res[product.product] = false
res[product.id] = false
}
}

View file

@ -1,8 +0,0 @@
'use strict'
// todo: is passing in profile necessary?
const parseRemark = (profile, r) => {
return null // todo
}
module.exports = parseRemark

View file

@ -1,15 +1,19 @@
'use strict'
// todo: arrivalDelay, departureDelay or only delay ?
// todo: arrivalPlatform, departurePlatform
const createParseStopover = (profile, stations, lines, remarks, date) => {
const findRemark = require('./find-remark')
const createParseStopover = (profile, opt, data, date) => {
const {locations, lines, hints, warnings} = data
const parseStopover = (st) => {
const res = {
station: stations[parseInt(st.locX)] || null,
stop: locations[parseInt(st.locX)] || null,
arrival: null,
arrivalDelay: null,
arrivalPlatform: st.aPlatfR || st.aPlatfS || null,
departure: null,
departureDelay: null
departureDelay: null,
departurePlatform: st.dPlatfR || st.dPlatfS || null
}
// todo: DRY with parseDeparture
@ -34,6 +38,13 @@ const createParseStopover = (profile, stations, lines, remarks, date) => {
res.departureDelay = Math.round((realtime - planned) / 1000)
}
if (st.aPlatfR && st.aPlatfS && st.aPlatfR !== st.aPlatfS) {
res.formerScheduledArrivalPlatform = st.aPlatfS
}
if (st.dPlatfR && st.dPlatfS && st.dPlatfR !== st.dPlatfS) {
res.formerScheduledDeparturePlatform = st.dPlatfS
}
// mark stations the train passes without stopping
if(st.dInS === false && st.aOutS === false) res.passBy = true
@ -58,6 +69,14 @@ const createParseStopover = (profile, stations, lines, remarks, date) => {
}
}
if (opt.remarks && Array.isArray(st.msgL)) {
res.remarks = []
for (let ref of st.msgL) {
const remark = findRemark(hints, warnings, ref)
if (remark) res.remarks.push(remark)
}
}
return res
}

42
parse/warning.js Normal file
View file

@ -0,0 +1,42 @@
'use strict'
const brToNewline = require('br2nl')
const parseDateTime = require('./date-time')
const typesByIcon = Object.assign(Object.create(null), {
HimWarn: 'status'
})
// todo: is passing in profile necessary?
const parseWarning = (profile, w, icons) => {
// todo: hid, act, pub, lead, tckr, icoX, fLocX, tLocX, prod, comp,
// todo: cat (1, 2), pubChL
// pubChL:
// [ { name: 'timetable',
// fDate: '20180606',
// fTime: '073000',
// tDate: '20180713',
// tTime: '030000' },
// { name: 'export',
// fDate: '20180606',
// fTime: '073000',
// tDate: '20180713',
// tTime: '030000' } ]
const icon = 'number' === typeof w.icoX && icons[w.icoX] || null
const type = icon && icon.res && typesByIcon[icon.res] || 'warning'
return {
type,
summary: brToNewline(w.head),
text: brToNewline(w.text),
priority: w.prio,
category: w.cat, // todo: parse to sth meaningful
validFrom: parseDateTime(profile, w.sDate, w.sTime).toISO(),
validUntil: parseDateTime(profile, w.eDate, w.eTime).toISO(),
modified: parseDateTime(profile, w.lModDate, w.lModTime).toISO()
}
}
module.exports = parseWarning

View file

@ -6,12 +6,13 @@ HAFAS endpoint | wrapper library | docs | example code | source code
---------------|------------------|------|---------|------------
[Deutsche Bahn (DB)](https://en.wikipedia.org/wiki/Deutsche_Bahn) | [`db-hafas`](https://github.com/derhuerst/db-hafas) | [docs](p/db/readme.md) | [example code](p/db/example.js) | [src](p/db/index.js)
[Berlin & Brandenburg public transport (VBB)](https://en.wikipedia.org/wiki/Verkehrsverbund_Berlin-Brandenburg) | [`vbb-hafas`](https://github.com/derhuerst/vbb-hafas) | [docs](p/vbb/readme.md) | [example code](p/vbb/example.js) | [src](p/vbb/index.js)
[Berlin public transport (BVG)](https://en.wikipedia.org/wiki/Berliner_Verkehrsbetriebe) | | [docs](p/bvg/readme.md) | [example code](p/bvg/example.js) | [src](p/bvg/index.js)
[Österreichische Bundesbahnen (ÖBB)](https://en.wikipedia.org/wiki/Austrian_Federal_Railways) | [`oebb-hafas`](https://github.com/juliuste/oebb-hafas) | [docs](p/oebb/readme.md) | [example code](p/oebb/example.js) | [src](p/oebb/index.js)
[Nahverkehr Sachsen-Anhalt (NASA)](https://de.wikipedia.org/wiki/Nahverkehrsservice_Sachsen-Anhalt)/[INSA](https://insa.de) | [`insa-hafas`](https://github.com/derhuerst/insa-hafas) | [docs](p/insa/readme.md) | [example code](p/insa/example.js) | [src](p/insa/index.js)
[Nahverkehrsverbund Schleswig-Holstein (NAH.SH)](https://de.wikipedia.org/wiki/Nahverkehrsverbund_Schleswig-Holstein) | [`nahsh-hafas`](https://github.com/juliuste/nahsh-hafas) | [docs](p/nahsh/readme.md) | [example code](p/nahsh/example.js) | [src](p/nahsh/index.js)
[![npm version](https://img.shields.io/npm/v/hafas-client.svg)](https://www.npmjs.com/package/hafas-client)
[![build status](https://img.shields.io/travis/public-transport/hafas-client.svg)](https://travis-ci.org/public-transport/hafas-client)
[![build status](https://img.shields.io/travis/public-transport/hafas-client.svg?branch=master)](https://travis-ci.org/public-transport/hafas-client)
![ISC-licensed](https://img.shields.io/github/license/public-transport/hafas-client.svg)
[![chat on gitter](https://badges.gitter.im/public-transport/Lobby.svg)](https://gitter.im/public-transport/Lobby)
[![support me on Patreon](https://img.shields.io/badge/support%20me-on%20patreon-fa7664.svg)](https://patreon.com/derhuerst)
@ -21,7 +22,7 @@ HAFAS endpoint | wrapper library | docs | example code | source code
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.
`hafas-client` contains all logic for communicating with these, as well as serialising from and parsing to [*Friendly Public Transport Format (FPTF)* `1.1.1`](https://github.com/public-transport/friendly-public-transport-format/blob/1.1.1/spec/readme.md). Endpoint-specific customisations (called *profiles* here) increase the quality of the returned data.
## Installing
@ -47,7 +48,7 @@ const createClient = require('hafas-client')
const dbProfile = require('hafas-client/p/db')
// create a client with Deutsche Bahn profile
const client = createClient(dbProfile)
const client = createClient(dbProfile, 'my-awesome-program')
// Berlin Jungfernheide to München Hbf
client.journeys('8011167', '8000261', {results: 1})
@ -55,7 +56,7 @@ client.journeys('8011167', '8000261', {results: 1})
.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).
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.1.1/spec/readme.md#journey).
```js
[ {
@ -177,7 +178,11 @@ The returned [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript
- [`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-monitor-departures`](https://github.com/derhuerst/hafas-monitor-departures#hafas-monitor-departures)  Pass in a HAFAS client, fetch all departures at any set of stations.
- [`hafas-discover-stations`](https://github.com/derhuerst/hafas-discover-stations#hafas-discover-stations) Pass in a HAFAS client, discover stations by querying departures.
- [`hafas-record-delays`](https://github.com/derhuerst/hafas-record-delays#hafas-record-delays)  Record delays from hafas-monitor-departures into a LevelDB.
- [`hafas-estimate-station-weight`](https://github.com/derhuerst/hafas-estimate-station-weight#hafas-estimate-station-weight) Pass in a HAFAS client, estimate the importance of a station.
- [`hafas-client-rpc`](https://github.com/derhuerst/hafas-client-rpc) Make JSON-RPC calls to `hafas-client` via WebSockets.
- [`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)

369
test/bvg.js Normal file
View file

@ -0,0 +1,369 @@
'use strict'
// todo: DRY with vbb tests
const stations = require('vbb-stations-autocomplete')
const a = require('assert')
const shorten = require('vbb-short-station-name')
const tapePromise = require('tape-promise').default
const tape = require('tape')
const isRoughlyEqual = require('is-roughly-equal')
const co = require('./lib/co')
const createClient = require('..')
const bvgProfile = require('../p/bvg')
const products = require('../p/bvg/products')
const createValidate = require('./lib/validate-fptf-with')
const {
cfg,
validateStation,
validateLine,
validateJourneyLeg,
validateDeparture,
validateMovement
} = require('./lib/vbb-bvg-validators')
const testJourneysStationToStation = require('./lib/journeys-station-to-station')
const testJourneysStationToAddress = require('./lib/journeys-station-to-address')
const testJourneysStationToPoi = require('./lib/journeys-station-to-poi')
const testEarlierLaterJourneys = require('./lib/earlier-later-journeys')
const testRefreshJourney = require('./lib/refresh-journey')
const journeysFailsWithNoProduct = require('./lib/journeys-fails-with-no-product')
const testDepartures = require('./lib/departures')
const testDeparturesInDirection = require('./lib/departures-in-direction')
const testDeparturesWithoutRelatedStations = require('./lib/departures-without-related-stations')
const testArrivals = require('./lib/arrivals')
const testJourneysWithDetour = require('./lib/journeys-with-detour')
const when = cfg.when
const validateDirection = (dir, name) => {
if (!stations(dir, true, false)[0]) {
console.error(name + `: station "${dir}" is unknown`)
}
}
const validate = createValidate(cfg, {
station: validateStation,
line: validateLine,
journeyLeg: validateJourneyLeg,
departure: validateDeparture,
movement: validateMovement
})
const test = tapePromise(tape)
const client = createClient(bvgProfile, 'public-transport/hafas-client:test')
const amrumerStr = '900000009101'
const spichernstr = '900000042101'
const bismarckstr = '900000024201'
const westhafen = '900000001201'
const wedding = '900000009104'
const württembergallee = '900000026153'
test('journeys  Spichernstr. to Bismarckstr.', co(function* (t) {
const journeys = yield client.journeys(spichernstr, bismarckstr, {
results: 3,
departure: when,
stopovers: true
})
yield testJourneysStationToStation({
test: t,
journeys,
validate,
fromId: spichernstr,
toId: bismarckstr
})
// todo: find a journey where there ticket info is always available
t.end()
}))
test('journeys  only subway', co(function* (t) {
const journeys = yield client.journeys(spichernstr, bismarckstr, {
results: 20,
departure: when,
products: {
suburban: false,
subway: true,
tram: false,
bus: false,
ferry: false,
express: false,
regional: false
}
})
validate(t, journeys, 'journeys', 'journeys')
t.ok(journeys.length > 1)
for (let i = 0; i < journeys.length; i++) {
const journey = journeys[i]
for (let j = 0; j < journey.legs.length; j++) {
const leg = journey.legs[j]
const name = `journeys[${i}].legs[${i}].line`
if (leg.line) {
t.equal(leg.line.mode, 'train', name + '.mode is invalid')
t.equal(leg.line.product, 'subway', name + '.product is invalid')
}
t.ok(journey.legs.some(l => l.line), name + '.legs has no subway leg')
}
}
t.end()
}))
test('journeys  fails with no product', (t) => {
journeysFailsWithNoProduct({
test: t,
fetchJourneys: client.journeys,
fromId: spichernstr,
toId: bismarckstr,
when,
products
})
t.end()
})
test('earlier/later journeys', co(function* (t) {
yield testEarlierLaterJourneys({
test: t,
fetchJourneys: client.journeys,
validate,
fromId: spichernstr,
toId: bismarckstr
})
t.end()
}))
test('refreshJourney', co(function* (t) {
yield testRefreshJourney({
test: t,
fetchJourneys: client.journeys,
refreshJourney: client.refreshJourney,
validate,
fromId: spichernstr,
toId: bismarckstr,
when
})
t.end()
}))
test('trip details', co(function* (t) {
const journeys = yield client.journeys(spichernstr, amrumerStr, {
results: 1, departure: when
})
const p = journeys[0].legs[0]
t.ok(p.id, 'precondition failed')
t.ok(p.line.name, 'precondition failed')
const trip = yield client.trip(p.id, p.line.name, {when})
validate(t, trip, 'journeyLeg', 'trip')
t.end()
}))
test('journeys  station to address', co(function* (t) {
const torfstr = {
type: 'location',
address: '13353 Berlin-Wedding, Torfstr. 17',
latitude: 52.541797,
longitude: 13.350042
}
const journeys = yield client.journeys(spichernstr, torfstr, {
results: 3,
departure: when
})
yield testJourneysStationToAddress({
test: t,
journeys,
validate,
fromId: spichernstr,
to: torfstr
})
t.end()
}))
test('journeys  station to POI', co(function* (t) {
const atze = {
type: 'location',
id: '900980720',
name: 'Berlin, Atze Musiktheater für Kinder',
latitude: 52.543333,
longitude: 13.351686
}
const journeys = yield client.journeys(spichernstr, atze, {
results: 3,
departure: when
})
yield testJourneysStationToPoi({
test: t,
journeys,
validate,
fromId: spichernstr,
to: atze
})
t.end()
}))
test('journeys: via works with detour', co(function* (t) {
// Going from Westhafen to Wedding via Württembergalle without detour
// is currently impossible. We check if the routing engine computes a detour.
const journeys = yield client.journeys(westhafen, wedding, {
via: württembergallee,
results: 1,
departure: when,
stopovers: true
})
yield testJourneysWithDetour({
test: t,
journeys,
validate,
detourIds: [württembergallee]
})
t.end()
}))
// todo: without detour test
test('departures', co(function* (t) {
const departures = yield client.departures(spichernstr, {
duration: 5, when
})
yield testDepartures({
test: t,
departures,
validate,
id: spichernstr
})
t.end()
}))
test('departures with station object', co(function* (t) {
const deps = yield client.departures({
type: 'station',
id: spichernstr,
name: 'U Spichernstr',
location: {
type: 'location',
latitude: 1.23,
longitude: 2.34
}
}, {when})
validate(t, deps, 'departures', 'departures')
t.end()
}))
test('departures at Spichernstr. in direction of Westhafen', co(function* (t) {
yield testDeparturesInDirection({
test: t,
fetchDepartures: client.departures,
fetchTrip: client.trip,
id: spichernstr,
directionIds: [westhafen],
when,
validate
})
t.end()
}))
test('departures at 7-digit station', co(function* (t) {
const eisenach = '8010097' // see derhuerst/vbb-hafas#22
yield client.departures(eisenach, {when})
t.pass('did not fail')
t.end()
}))
test('departures without related stations', co(function* (t) {
yield testDeparturesWithoutRelatedStations({
test: t,
fetchDepartures: client.departures,
id: '900000024101', // Charlottenburg
when,
products: {bus: false, suburban: false, regional: false},
linesOfRelatedStations: ['U7']
})
t.end()
}))
test('arrivals', co(function* (t) {
const arrivals = yield client.arrivals(spichernstr, {
duration: 5, when
})
yield testArrivals({
test: t,
arrivals,
validate,
id: spichernstr
})
t.end()
}))
test('nearby', co(function* (t) {
const berlinerStr = '900000044201'
const landhausstr = '900000043252'
// Berliner Str./Bundesallee
const nearby = yield client.nearby({
type: 'location',
latitude: 52.4873452,
longitude: 13.3310411
}, {distance: 200})
validate(t, nearby, 'locations', 'nearby')
t.equal(nearby[0].id, berlinerStr)
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, landhausstr)
t.equal(nearby[1].name, 'Landhausstr.')
t.ok(nearby[1].distance > 100)
t.ok(nearby[1].distance < 200)
t.end()
}))
test('locations', co(function* (t) {
const locations = yield client.locations('Alexanderplatz', {results: 20})
validate(t, locations, 'locations', 'locations')
t.ok(locations.length <= 20)
t.ok(locations.find(s => s.type === 'stop' || 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('station', co(function* (t) {
const s = yield client.station(spichernstr)
validate(t, s, ['stop', 'station'], 'station')
t.equal(s.id, spichernstr)
t.end()
}))
test('radar', co(function* (t) {
const vehicles = yield client.radar({
north: 52.52411,
west: 13.41002,
south: 52.51942,
east: 13.41709
}, {
duration: 5 * 60, when
})
validate(t, vehicles, 'movements', 'vehicles')
t.end()
}))

View file

@ -1,83 +1,59 @@
'use strict'
const getStations = require('db-stations').full
const stations = require('db-stations/full.json')
const a = require('assert')
const tapePromise = require('tape-promise').default
const tape = require('tape')
const isRoughlyEqual = require('is-roughly-equal')
const co = require('./co')
const {createWhen} = require('./lib/util')
const co = require('./lib/co')
const createClient = require('..')
const dbProfile = require('../p/db')
const {allProducts} = require('../p/db/modes')
const products = require('../p/db/products')
const {
assertValidStation,
assertValidPoi,
assertValidAddress,
assertValidLocation,
assertValidLine,
assertValidStopover,
createWhen, assertValidWhen
} = require('./util.js')
station: createValidateStation,
journeyLeg: createValidateJourneyLeg
} = require('./lib/validators')
const createValidate = require('./lib/validate-fptf-with')
const testJourneysStationToStation = require('./lib/journeys-station-to-station')
const testJourneysStationToAddress = require('./lib/journeys-station-to-address')
const testJourneysStationToPoi = require('./lib/journeys-station-to-poi')
const testEarlierLaterJourneys = require('./lib/earlier-later-journeys')
const testRefreshJourney = require('./lib/refresh-journey')
const journeysFailsWithNoProduct = require('./lib/journeys-fails-with-no-product')
const testDepartures = require('./lib/departures')
const testDeparturesInDirection = require('./lib/departures-in-direction')
const testDeparturesWithoutRelatedStations = require('./lib/departures-without-related-stations')
const testArrivals = require('./lib/arrivals')
const testJourneysWithDetour = require('./lib/journeys-with-detour')
const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o)
const when = createWhen('Europe/Berlin', 'de-DE')
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 cfg = {
when,
stationCoordsOptional: false,
products
}
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 === jungfernh) &&
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 === jungfernh, '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: DRY with assertValidStationProducts
// todo: DRY with other tests
const assertValidProducts = (t, p) => {
for (let product of allProducts) {
product = product.product // wat
t.equal(typeof p[product], 'boolean', 'product ' + p + ' must be a boolean')
const _validateStation = createValidateStation(cfg)
const validateStation = (validate, s, name) => {
_validateStation(validate, s, name)
const match = stations.some(station => (
station.id === s.id ||
(station.additionalIds && station.additionalIds.includes(s.id))
))
if (!match) {
console.error(name + `.id: unknown ID "${s.id}"`)
}
}
const validate = createValidate(cfg, {
station: validateStation
})
const assertValidPrice = (t, p) => {
t.ok(p)
if (p.amount !== null) {
@ -91,248 +67,185 @@ const assertValidPrice = (t, p) => {
}
const test = tapePromise(tape)
const client = createClient(dbProfile)
const client = createClient(dbProfile, 'public-transport/hafas-client:test')
const jungfernh = '8011167'
const berlinHbf = '8011160'
const münchenHbf = '8000261'
const hannoverHbf = '8000152'
const jungfernheide = '8011167'
const blnSchwedterStr = '732652'
const westhafen = '008089116'
const wedding = '008089131'
const württembergallee = '731084'
const regensburgHbf = '8000309'
const blnOstbahnhof = '8010255'
test('Berlin Jungfernheide to München Hbf', co(function* (t) {
const journeys = yield client.journeys(jungfernh, münchenHbf, {
when, passedStations: true
test('journeys  Berlin Schwedter Str. to München Hbf', co(function* (t) {
const journeys = yield client.journeys(blnSchwedterStr, münchenHbf, {
results: 3,
departure: when,
stopovers: true
})
t.ok(Array.isArray(journeys))
t.ok(journeys.length > 0, 'no journeys')
yield testJourneysStationToStation({
test: t,
journeys,
validate,
fromId: blnSchwedterStr,
toId: münchenHbf
})
// todo: find a journey where there pricing info is always available
for (let journey of journeys) {
t.equal(journey.type, 'journey')
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)
}
assertValidWhen(t, journey.departure, when)
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)
}
assertValidWhen(t, journey.arrival, when)
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)
}
assertValidWhen(t, leg.departure, when)
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)
}
assertValidWhen(t, leg.arrival, when)
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(function* (t) {
const journeys = yield client.journeys(jungfernh, {
type: 'location', address: 'Torfstraße 17',
latitude: 52.5416823, longitude: 13.3491223
}, {when})
// todo: journeys, only one product
t.ok(Array.isArray(journeys))
t.ok(journeys.length >= 1, 'no journeys')
const journey = journeys[0]
const leg = journey.legs[journey.legs.length - 1]
test('journeys  fails with no product', (t) => {
journeysFailsWithNoProduct({
test: t,
fetchJourneys: client.journeys,
fromId: blnSchwedterStr,
toId: münchenHbf,
when,
products
})
t.end()
})
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)
test('Berlin Schwedter Str. to Torfstraße 17', co(function* (t) {
const torfstr = {
type: 'location',
address: 'Torfstraße 17',
latitude: 52.5416823,
longitude: 13.3491223
}
if (leg.origin.products) assertValidProducts(t, leg.origin.products)
assertValidWhen(t, leg.departure, when)
assertValidWhen(t, leg.arrival, when)
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))
const journeys = yield client.journeys(blnSchwedterStr, torfstr, {
results: 3,
departure: when
})
yield testJourneysStationToAddress({
test: t,
journeys,
validate,
fromId: blnSchwedterStr,
to: torfstr
})
t.end()
}))
test('Berlin Jungfernheide to ATZE Musiktheater', co(function* (t) {
const journeys = yield client.journeys(jungfernh, {
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)
test('Berlin Schwedter Str. to ATZE Musiktheater', co(function* (t) {
const atze = {
type: 'location',
id: '991598902',
name: 'ATZE Musiktheater',
latitude: 52.542417,
longitude: 13.350437
}
if (leg.origin.products) assertValidProducts(t, leg.origin.products)
assertValidWhen(t, leg.departure, when)
assertValidWhen(t, leg.arrival, when)
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))
const journeys = yield client.journeys(blnSchwedterStr, atze, {
results: 3,
departure: when
})
yield testJourneysStationToPoi({
test: t,
journeys,
validate,
fromId: blnSchwedterStr,
to: atze
})
t.end()
}))
test('journeys: via works with detour', co(function* (t) {
// Going from Westhafen to Wedding via Württembergalle without detour
// is currently impossible. We check if the routing engine computes a detour.
const westhafen = '008089116'
const wedding = '008089131'
const württembergallee = '731084'
const [journey] = yield client.journeys(westhafen, wedding, {
const journeys = yield client.journeys(westhafen, wedding, {
via: württembergallee,
results: 1,
when,
passedStations: true
departure: when,
stopovers: true
})
t.ok(journey)
const l = journey.legs.some(l => l.passed && l.passed.some(p => p.station.id === württembergallee))
t.ok(l, 'Württembergalle is not being passed')
yield testJourneysWithDetour({
test: t,
journeys,
validate,
detourIds: [württembergallee]
})
t.end()
}))
test('journeys: via works without detour', co(function* (t) {
// When going from Ruhleben to Zoo via Kastanienallee, there is *no need*
// to change trains / no need for a "detour".
const ruhleben = '000731058'
const zoo = '008010406'
const kastanienallee = '730983'
const [journey] = yield client.journeys(ruhleben, zoo, {
via: kastanienallee,
results: 1,
when,
passedStations: true
})
t.ok(journey)
const l = journey.legs.some(l => l.passed && l.passed.some(p => p.station.id === kastanienallee))
t.ok(l, 'Kastanienallee is not being passed')
t.end()
}))
// todo: without detour
test('earlier/later journeys, Jungfernheide -> München Hbf', co(function* (t) {
const model = yield client.journeys(jungfernh, münchenHbf, {
results: 3, when
yield testEarlierLaterJourneys({
test: t,
fetchJourneys: client.journeys,
validate,
fromId: jungfernheide,
toId: münchenHbf
})
t.equal(typeof model.earlierRef, 'string')
t.ok(model.earlierRef)
t.equal(typeof model.laterRef, 'string')
t.ok(model.laterRef)
// when and earlierThan/laterThan should be mutually exclusive
t.throws(() => {
client.journeys(jungfernh, münchenHbf, {
when, earlierThan: model.earlierRef
})
})
t.throws(() => {
client.journeys(jungfernh, münchenHbf, {
when, laterThan: model.laterRef
})
})
let earliestDep = Infinity, latestDep = -Infinity
for (let j of model) {
const dep = +new Date(j.departure)
if (dep < earliestDep) earliestDep = dep
else if (dep > latestDep) latestDep = dep
}
const earlier = yield client.journeys(jungfernh, münchenHbf, {
results: 3,
// todo: single journey ref?
earlierThan: model.earlierRef
})
for (let j of earlier) {
t.ok(new Date(j.departure) < earliestDep)
}
const later = yield client.journeys(jungfernh, münchenHbf, {
results: 3,
// todo: single journey ref?
laterThan: model.laterRef
})
for (let j of later) {
t.ok(new Date(j.departure) > latestDep)
}
t.end()
}))
test('departures at Berlin Jungfernheide', co(function* (t) {
const deps = yield client.departures(jungfernh, {
test('refreshJourney', co(function* (t) {
yield testRefreshJourney({
test: t,
fetchJourneys: client.journeys,
refreshJourney: client.refreshJourney,
validate,
fromId: jungfernheide,
toId: münchenHbf,
when
})
t.end()
}))
test('trip details', co(function* (t) {
const journeys = yield client.journeys(berlinHbf, münchenHbf, {
results: 1, departure: when
})
const p = journeys[0].legs[0]
t.ok(p.id, 'precondition failed')
t.ok(p.line.name, 'precondition failed')
const trip = yield client.trip(p.id, p.line.name, {when})
const validateJourneyLeg = createValidateJourneyLeg(cfg)
const validate = createValidate(cfg, {
journeyLeg: (validate, leg, name) => {
if (!leg.direction) leg.direction = 'foo' // todo, see #49
validateJourneyLeg(validate, leg, name)
}
})
validate(t, trip, 'journeyLeg', 'trip')
t.end()
}))
test('departures at Berlin Schwedter Str.', co(function* (t) {
const departures = yield client.departures(blnSchwedterStr, {
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)
assertValidWhen(t, dep.when, when)
}
yield testDepartures({
test: t,
departures,
validate,
id: blnSchwedterStr
})
t.end()
}))
test('departures with station object', co(function* (t) {
yield client.departures({
const deps = yield client.departures({
type: 'station',
id: jungfernh,
id: jungfernheide,
name: 'Berlin Jungfernheide',
location: {
type: 'location',
@ -341,7 +254,46 @@ test('departures with station object', co(function* (t) {
}
}, {when})
t.ok('did not fail')
validate(t, deps, 'departures', 'departures')
t.end()
}))
test('departures at Berlin Hbf in direction of Berlin Ostbahnhof', co(function* (t) {
yield testDeparturesInDirection({
test: t,
fetchDepartures: client.departures,
fetchTrip: client.trip,
id: berlinHbf,
directionIds: [blnOstbahnhof, '8089185', '732676'],
when,
validate
})
t.end()
}))
test('departures without related stations', co(function* (t) {
yield testDeparturesWithoutRelatedStations({
test: t,
fetchDepartures: client.departures,
id: '8089051', // Berlin Yorckstr. (S1)
when,
products: {bus: false},
linesOfRelatedStations: ['S 2', 'S 25', 'S 26', 'U 7']
})
t.end()
}))
test('arrivals at Berlin Schwedter Str.', co(function* (t) {
const arrivals = yield client.arrivals(blnSchwedterStr, {
duration: 5, when
})
yield testArrivals({
test: t,
arrivals,
validate,
id: blnSchwedterStr
})
t.end()
}))
@ -354,17 +306,18 @@ test('nearby Berlin Jungfernheide', co(function* (t) {
results: 2, distance: 400
})
t.ok(Array.isArray(nearby))
validate(t, nearby, 'locations', '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)
}
const s0 = nearby[0]
// todo: trim IDs
t.ok(s0.id === '008011167' || s0.id === jungfernheide)
t.equal(s0.name, 'Berlin Jungfernheide')
t.ok(isRoughlyEqual(.0005, s0.location.latitude, 52.530408))
t.ok(isRoughlyEqual(.0005, s0.location.longitude, 13.299424))
t.ok(s0.distance >= 0)
t.ok(s0.distance <= 100)
t.end()
}))
@ -374,29 +327,24 @@ test('locations named Jungfernheide', co(function* (t) {
results: 10
})
t.ok(Array.isArray(locations))
t.ok(locations.length > 0)
validate(t, locations, 'locations', 'locations')
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.ok(locations.some((l) => {
// todo: trim IDs
if (l.station) {
if (l.station.id === '008011167' || l.station.id === jungfernheide) return true
}
return l.id === '008011167' || l.id === jungfernheide
}), 'Jungfernheide not found')
t.end()
}))
test('location', co(function* (t) {
const loc = yield client.location(regensburgHbf)
test('station', co(function* (t) {
const s = yield client.station(regensburgHbf)
assertValidStation(t, loc)
t.equal(loc.id, regensburgHbf)
t.ok(Array.isArray(loc.lines))
if (Array.isArray(loc.lines)) {
for (let line of loc.lines) assertValidLine(t, line)
}
validate(t, s, ['stop', 'station'], 'station')
t.equal(s.id, regensburgHbf)
t.end()
}))

View file

@ -2,6 +2,7 @@
require('./db')
require('./vbb')
require('./bvg')
require('./oebb')
require('./insa')
require('./nahsh')

View file

@ -3,388 +3,275 @@
const tapePromise = require('tape-promise').default
const tape = require('tape')
const isRoughlyEqual = require('is-roughly-equal')
const validateFptf = require('validate-fptf')
const co = require('./co')
const {createWhen} = require('./lib/util')
const co = require('./lib/co')
const createClient = require('..')
const insaProfile = require('../p/insa')
const {allProducts} = require('../p/insa/products')
const {
assertValidStation,
assertValidPoi,
assertValidAddress,
assertValidLocation,
assertValidLine,
assertValidStopover,
hour,
createWhen,
assertValidWhen
} = require('./util.js')
const products = require('../p/insa/products')
const createValidate = require('./lib/validate-fptf-with')
const testJourneysStationToStation = require('./lib/journeys-station-to-station')
const testJourneysStationToAddress = require('./lib/journeys-station-to-address')
const testJourneysStationToPoi = require('./lib/journeys-station-to-poi')
const testEarlierLaterJourneys = require('./lib/earlier-later-journeys')
const journeysFailsWithNoProduct = require('./lib/journeys-fails-with-no-product')
const testDepartures = require('./lib/departures')
const testDeparturesInDirection = require('./lib/departures-in-direction')
const testArrivals = require('./lib/arrivals')
const testJourneysWithDetour = require('./lib/journeys-with-detour')
const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o)
const when = createWhen('Europe/Berlin', 'de-DE')
const assertValidStationProducts = (t, p) => {
t.ok(p)
t.equal(typeof p.nationalExp, 'boolean')
t.equal(typeof p.national, 'boolean')
t.equal(typeof p.regional, 'boolean')
t.equal(typeof p.suburban, 'boolean')
t.equal(typeof p.tram, 'boolean')
t.equal(typeof p.bus, 'boolean')
t.equal(typeof p.tourismTrain, 'boolean')
const cfg = {
when,
stationCoordsOptional: false,
products
}
const isMagdeburgHbf = s => {
return (
s.type === 'station' &&
(s.id === '8010224' || s.id === '008010224') &&
s.name === 'Magdeburg Hbf' &&
s.location &&
isRoughlyEqual(s.location.latitude, 52.130352, 0.001) &&
isRoughlyEqual(s.location.longitude, 11.626891, 0.001)
)
}
const assertIsMagdeburgHbf = (t, s) => {
t.equal(s.type, 'station')
t.ok(s.id === '8010224' || s.id === '008010224', 'id should be 8010224')
t.equal(s.name, 'Magdeburg Hbf')
t.ok(s.location)
t.ok(isRoughlyEqual(s.location.latitude, 52.130352, 0.001))
t.ok(isRoughlyEqual(s.location.longitude, 11.626891, 0.001))
}
// todo: DRY with assertValidStationProducts
// todo: DRY with other tests
const assertValidProducts = (t, p) => {
for (let product of allProducts) {
product = product.product // wat
t.equal(typeof p[product], 'boolean', 'product ' + p + ' must be a boolean')
}
}
const validate = createValidate(cfg, {})
const test = tapePromise(tape)
const client = createClient(insaProfile)
const client = createClient(insaProfile, 'public-transport/hafas-client:test')
test('Magdeburg Hbf to Magdeburg-Buckau', co(function*(t) {
const magdeburgHbf = '8010224'
const magdeburgBuckau = '8013456'
const magdeburgHbf = '8010224'
const magdeburgBuckau = '8013456'
const leiterstr = '7464'
const hasselbachplatzSternstrasse = '000006545'
const stendal = '008010334'
const dessau = '008010077'
const universitaet = '19686'
test('journeys  Magdeburg Hbf to Magdeburg-Buckau', co(function* (t) {
const journeys = yield client.journeys(magdeburgHbf, magdeburgBuckau, {
when,
passedStations: true
results: 3,
departure: when,
stopovers: 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 (journey.origin.products) {
assertValidProducts(t, journey.origin.products)
}
assertValidWhen(t, journey.departure, when)
assertValidStation(t, journey.destination)
assertValidStationProducts(t, journey.origin.products)
if (journey.destination.products) {
assertValidProducts(t, journey.destination.products)
}
assertValidWhen(t, journey.arrival, when)
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)
assertValidWhen(t, leg.departure, when)
t.equal(typeof leg.departurePlatform, 'string')
assertValidStation(t, leg.destination)
assertValidStationProducts(t, leg.origin.products)
assertValidWhen(t, leg.arrival, when)
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)
}
yield testJourneysStationToStation({
test: t,
journeys,
validate,
fromId: magdeburgHbf,
toId: magdeburgBuckau
})
t.end()
}))
// todo: journeys, only one product
test('journeys  fails with no product', (t) => {
journeysFailsWithNoProduct({
test: t,
fetchJourneys: client.journeys,
fromId: magdeburgHbf,
toId: magdeburgBuckau,
when,
products
})
t.end()
})
test('Magdeburg Hbf to 39104 Magdeburg, Sternstr. 10', co(function*(t) {
const magdeburgHbf = '8010224'
const sternStr = {
type: 'location',
address: 'Magdeburg - Altenstadt, Sternstraße 10',
latitude: 52.118414,
longitude: 11.422332,
address: 'Magdeburg - Altenstadt, Sternstraße 10'
longitude: 11.422332
}
const journeys = yield client.journeys(magdeburgHbf, sternStr, {
when
results: 3,
departure: when
})
t.ok(Array.isArray(journeys))
t.ok(journeys.length >= 1, 'no journeys')
const journey = journeys[0]
const firstLeg = journey.legs[0]
const lastLeg = journey.legs[journey.legs.length - 1]
assertValidStation(t, firstLeg.origin)
assertValidStationProducts(t, firstLeg.origin.products)
if (firstLeg.origin.products)
assertValidProducts(t, firstLeg.origin.products)
assertValidWhen(t, firstLeg.departure, when)
assertValidWhen(t, firstLeg.arrival, when)
assertValidWhen(t, lastLeg.departure, when)
assertValidWhen(t, lastLeg.arrival, when)
const d = lastLeg.destination
assertValidAddress(t, d)
t.equal(d.address, 'Magdeburg - Altenstadt, Sternstraße 10')
t.ok(isRoughlyEqual(0.0001, d.latitude, 52.118414))
t.ok(isRoughlyEqual(0.0001, d.longitude, 11.422332))
yield testJourneysStationToAddress({
test: t,
journeys,
validate,
fromId: magdeburgHbf,
to: sternStr
})
t.end()
}))
test('Kloster Unser Lieben Frauen to Magdeburg Hbf', co(function*(t) {
test('Magdeburg Hbf to Kloster Unser Lieben Frauen', co(function*(t) {
const kloster = {
type: 'location',
latitude: 52.127601,
longitude: 11.636437,
id: '970012223',
name: 'Magdeburg, Kloster Unser Lieben Frauen (Denkmal)',
id: '970012223'
latitude: 52.127601,
longitude: 11.636437
}
const magdeburgHbf = '8010224'
const journeys = yield client.journeys(kloster, magdeburgHbf, {
when
const journeys = yield client.journeys(magdeburgHbf, kloster, {
results: 3,
departure: when
})
t.ok(Array.isArray(journeys))
t.ok(journeys.length >= 1, 'no journeys')
const journey = journeys[0]
const firstLeg = journey.legs[0]
const lastLeg = journey.legs[journey.legs.length - 1]
const o = firstLeg.origin
assertValidPoi(t, o)
t.equal(o.name, 'Magdeburg, Kloster Unser Lieben Frauen (Denkmal)')
t.ok(isRoughlyEqual(0.0001, o.latitude, 52.127601))
t.ok(isRoughlyEqual(0.0001, o.longitude, 11.636437))
assertValidWhen(t, firstLeg.departure, when)
assertValidWhen(t, firstLeg.arrival, when)
assertValidWhen(t, lastLeg.departure, when)
assertValidWhen(t, lastLeg.arrival, when)
assertValidStation(t, lastLeg.destination)
assertValidStationProducts(t, lastLeg.destination.products)
if (lastLeg.destination.products)
assertValidProducts(t, lastLeg.destination.products)
yield testJourneysStationToPoi({
test: t,
journeys,
validate,
fromId: magdeburgHbf,
to: kloster
})
t.end()
}))
test('journeys: via works with detour', co(function* (t) {
// Going from Magdeburg, Hasselbachplatz (Sternstr.) (Tram/Bus) to Stendal via Dessau without detour
// is currently impossible. We check if the routing engine computes a detour.
const hasselbachplatzSternstrasse = '000006545'
const stendal = '008010334'
const dessau = '008010077'
const dessauPassed = '8010077'
const [journey] = yield client.journeys(hasselbachplatzSternstrasse, stendal, {
// Going from Magdeburg, Hasselbachplatz (Sternstr.) (Tram/Bus) to Stendal
// via Dessau without detour is currently impossible. We check if the routing
// engine computes a detour.
const journeys = yield client.journeys(hasselbachplatzSternstrasse, stendal, {
via: dessau,
results: 1,
when,
passedStations: true
departure: when,
stopovers: true
})
t.ok(journey)
yield testJourneysWithDetour({
test: t,
journeys,
validate,
detourIds: ['8010077', dessau] // todo: trim IDs
})
t.end()
}))
const l = journey.legs.some(l => l.passed && l.passed.some(p => p.station.id === dessauPassed))
t.ok(l, 'Dessau is not being passed')
// todo: without detour
test('earlier/later journeys', co(function* (t) {
yield testEarlierLaterJourneys({
test: t,
fetchJourneys: client.journeys,
validate,
fromId: magdeburgHbf,
toId: magdeburgBuckau
})
t.end()
}))
test('journeys: via works without detour', co(function* (t) {
// When going from Magdeburg, Hasselbachplatz (Sternstr.) (Tram/Bus) to Magdeburg, Universität via Magdeburg, Breiter Weg, there is *no need*
// to change trains / no need for a "detour".
const hasselbachplatzSternstrasse = '000006545'
const universitaet = '000019686'
const breiterWeg = '000013519'
const breiterWegPassed = '13519'
const [journey] = yield client.journeys(hasselbachplatzSternstrasse, universitaet, {
via: breiterWeg,
results: 1,
when,
passedStations: true
test('trip details', co(function* (t) {
const journeys = yield client.journeys(magdeburgHbf, magdeburgBuckau, {
results: 1, departure: when
})
t.ok(journey)
const l = journey.legs.some(l => l.passed && l.passed.some(p => p.station.id === breiterWegPassed))
t.ok(l, 'Magdeburg, Breiter Weg is not being passed')
const p = journeys[0].legs[0]
t.ok(p.id, 'precondition failed')
t.ok(p.line.name, 'precondition failed')
const trip = yield client.trip(p.id, p.line.name, {when})
validate(t, trip, 'journeyLeg', 'trip')
t.end()
}))
test('departures at Magdeburg Hbf', co(function*(t) {
const magdeburgHbf = '8010224'
const deps = yield client.departures(magdeburgHbf, {
duration: 5,
when
test('departures at Magdeburg Leiterstr.', co(function*(t) {
const departures = yield client.departures(leiterstr, {
duration: 5, when
})
t.ok(Array.isArray(deps))
for (let dep of deps) {
assertValidStation(t, dep.station)
assertValidStationProducts(t, dep.station.products)
if (dep.station.products) {
assertValidProducts(t, dep.station.products)
yield testDepartures({
test: t,
departures,
validate,
id: leiterstr
})
t.end()
}))
test('departures with station object', co(function* (t) {
const deps = yield client.departures({
type: 'station',
id: magdeburgHbf,
name: 'Magdeburg Hbf',
location: {
type: 'location',
latitude: 1.23,
longitude: 2.34
}
assertValidWhen(t, dep.when, when)
}
}, {when})
validate(t, deps, 'departures', 'departures')
t.end()
}))
test('nearby Magdeburg Hbf', co(function*(t) {
const magdeburgHbfPosition = {
type: 'location',
latitude: 52.130352,
longitude: 11.626891
}
const nearby = yield client.nearby(magdeburgHbfPosition, {
results: 2,
distance: 400
test('departures at Leiterstr in direction of Universität', co(function* (t) {
yield testDeparturesInDirection({
test: t,
fetchDepartures: client.departures,
fetchTrip: client.trip,
id: leiterstr,
directionIds: [universitaet],
when,
validate
})
t.end()
}))
test('arrivals at Magdeburg Leiterstr.', co(function*(t) {
const arrivals = yield client.arrivals(leiterstr, {
duration: 5, when
})
t.ok(Array.isArray(nearby))
t.equal(nearby.length, 2)
assertIsMagdeburgHbf(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('journey leg details', co(function* (t) {
const magdeburgHbf = '8010224'
const magdeburgBuckau = '8013456'
const [journey] = yield client.journeys(magdeburgHbf, magdeburgBuckau, {
results: 1, when
yield testArrivals({
test: t,
arrivals,
validate,
id: leiterstr
})
const p = journey.legs[0]
t.ok(p, 'missing legs[0]')
t.ok(p.id, 'missing legs[0].id')
t.ok(p.line, 'missing legs[0].line')
t.ok(p.line.name, 'missing legs[0].line.name')
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()
}))
// todo: nearby
test('locations named Magdeburg', co(function*(t) {
const locations = yield client.locations('Magdeburg', {
results: 10
results: 20
})
t.ok(Array.isArray(locations))
t.ok(locations.length > 0)
t.ok(locations.length <= 10)
validate(t, locations, 'locations', 'locations')
t.ok(locations.length <= 20)
for (let l of locations) {
if (l.type === 'station') assertValidStation(t, l)
else assertValidLocation(t, l)
}
t.ok(locations.some(isMagdeburgHbf))
t.ok(locations.find(s => s.type === 'stop' || s.type === 'station'))
t.ok(locations.find(s => s.id && s.name)) // POIs
t.ok(locations.some((l) => {
// todo: trim IDs
if (l.station) {
if (l.station.id === '008010224' || l.station.id === magdeburgHbf) return true
}
return l.id === '008010224' || l.id === magdeburgHbf
}))
t.end()
}))
test('location', co(function*(t) {
const magdeburgBuckau = '8013456'
const loc = yield client.location(magdeburgBuckau)
test('station Magdeburg-Buckau', co(function* (t) {
const s = yield client.station(magdeburgBuckau)
assertValidStation(t, loc)
t.equal(loc.id, magdeburgBuckau)
validate(t, s, ['stop', 'station'], 'station')
t.equal(s.id, magdeburgBuckau)
t.end()
}))
test('radar', co(function* (t) {
const north = 52.148364
const west = 11.600826
const south = 52.108486
const east = 11.651451
const vehicles = yield client.radar(north, west, south, east, {
const vehicles = yield client.radar({
north: 52.148364,
west: 11.600826,
south: 52.108486,
east: 11.651451
}, {
duration: 5 * 60, when, results: 10
})
t.ok(Array.isArray(vehicles))
t.ok(vehicles.length > 0)
for (let v of vehicles) {
assertValidLine(t, v.line)
const customCfg = Object.assign({}, cfg, {
stationCoordsOptional: true, // see #28
})
const validate = createValidate(customCfg, {})
validate(t, vehicles, 'movements', 'vehicles')
t.equal(typeof v.location.latitude, 'number')
t.ok(v.location.latitude <= 57, 'vehicle is too far away')
t.ok(v.location.latitude >= 47, 'vehicle is too far away')
t.equal(typeof v.location.longitude, 'number')
t.ok(v.location.longitude >= 8, 'vehicle is too far away')
t.ok(v.location.longitude <= 14, 'vehicle is too far away')
t.ok(Array.isArray(v.nextStops))
for (let st of v.nextStops) {
assertValidStopover(t, st, true)
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) {
// see #28
// todo: check if this works by now
assertValidStation(t, f.origin, true)
assertValidStationProducts(t, f.origin.products)
assertValidStation(t, f.destination, true)
assertValidStationProducts(t, f.destination.products)
t.equal(typeof f.t, 'number')
}
}
t.end()
}))

25
test/lib/arrivals.js Normal file
View file

@ -0,0 +1,25 @@
'use strict'
const co = require('./co')
const testArrivals = co(function* (cfg) {
const {test: t, arrivals: arrs, validate, id} = cfg
validate(t, arrs, 'arrivals', 'arrivals')
t.ok(arrs.length > 0, 'must be >0 arrivals')
for (let i = 0; i < arrs.length; i++) {
let stop = arrs[i].stop
let name = `arrs[${i}].stop`
if (stop.station) {
stop = stop.station
name += '.station'
}
t.equal(stop.id, id, name + '.id is invalid')
}
// todo: move into arrivals validator
t.deepEqual(arrs, arrs.sort((a, b) => t.when > b.when))
})
module.exports = testArrivals

View file

@ -1,3 +1,5 @@
'use strict'
// https://github.com/babel/babel/blob/3c8d831fe41f502cbe2459a271d19c7329ffe369/packages/babel-helpers/src/helpers.js#L242-L270
const co = (fn) => {
return function run () {

View file

@ -0,0 +1,38 @@
'use strict'
const co = require('./co')
const testDeparturesInDirection = co(function* (cfg) {
const {
test: t,
fetchDepartures,
fetchTrip,
id,
directionIds,
when,
validate
} = cfg
const deps = yield fetchDepartures(id, {
direction: directionIds[0],
when
})
validate(t, deps, 'departures', 'departures')
t.ok(deps.length > 0, 'must be >0 departures')
for (let i = 0; i < deps.length; i++) {
const dep = deps[i]
const name = `deps[${i}]`
const line = dep.line && dep.line.name
const trip = yield fetchTrip(dep.tripId, line, {
when, stopovers: true
})
t.ok(trip.stopovers.some(st => (
st.stop.station && directionIds.includes(st.stop.station.id) ||
directionIds.includes(st.stop.id)
)), `trip ${dep.tripId} of ${name} has no stopover at ${directionIds}`)
}
})
module.exports = testDeparturesInDirection

View file

@ -0,0 +1,41 @@
'use strict'
const co = require('./co')
const testDeparturesWithoutUnrelatedStations = co(function* (cfg) {
const {
test: t,
fetchDepartures,
id,
when
// duration, products
} = cfg
const relatedLines = cfg.linesOfRelatedStations
.map(lName => lName.toLowerCase().trim())
const isUnrelatedLine = (dep) => {
if (!dep.line || !dep.line.name) return false
return relatedLines.includes(dep.line.name.toLowerCase().trim())
}
const depsWith = yield fetchDepartures(id, {
when,
duration: cfg.duration || 20,
products: cfg.products || {}
})
t.ok(depsWith.some(isUnrelatedLine), 'precondition failed: no line at related station found')
const depsWithout = yield fetchDepartures(id, {
includeRelatedStations: false,
when,
duration: cfg.duration || 20,
products: cfg.products || {}
})
const unrelatedDep = depsWithout.find(isUnrelatedLine)
if (unrelatedDep) t.fail('line at related station: ' + unrelatedDep.line.name)
else t.pass('no lines from related stations')
})
module.exports = testDeparturesWithoutUnrelatedStations

25
test/lib/departures.js Normal file
View file

@ -0,0 +1,25 @@
'use strict'
const co = require('./co')
const testDepartures = co(function* (cfg) {
const {test: t, departures: deps, validate, id} = cfg
validate(t, deps, 'departures', 'departures')
t.ok(deps.length > 0, 'must be >0 departures')
for (let i = 0; i < deps.length; i++) {
let stop = deps[i].stop
let name = `deps[${i}].stop`
if (stop.station) {
stop = stop.station
name += '.station'
}
t.equal(stop.id, id, name + '.id is invalid')
}
// todo: move into deps validator
t.deepEqual(deps, deps.sort((a, b) => t.when > b.when))
})
module.exports = testDepartures

View file

@ -0,0 +1,82 @@
'use strict'
const co = require('./co')
const testEarlierLaterJourneys = co(function* (cfg) {
const {
test: t,
fetchJourneys,
fromId,
toId,
when,
// todo: validate
} = cfg
const model = yield fetchJourneys(fromId, toId, {
results: 3, departure: when
})
// todo: move to journeys validator?
t.equal(typeof model.earlierRef, 'string')
t.ok(model.earlierRef)
t.equal(typeof model.laterRef, 'string')
t.ok(model.laterRef)
// departure/arrival and earlierThan/laterThan should be mutually exclusive
t.throws(() => {
fetchJourneys(fromId, toId, {
departure: when, earlierThan: model.earlierRef
})
// silence rejections, we're only interested in exceptions
.catch(() => {})
})
t.throws(() => {
fetchJourneys(fromId, toId, {
departure: when, laterThan: model.laterRef
})
// silence rejections, we're only interested in exceptions
.catch(() => {})
})
t.throws(() => {
fetchJourneys(fromId, toId, {
arrival: when, earlierThan: model.earlierRef
})
// silence rejections, we're only interested in exceptions
.catch(() => {})
})
t.throws(() => {
fetchJourneys(fromId, toId, {
arrival: when, laterThan: model.laterRef
})
// silence rejections, we're only interested in exceptions
.catch(() => {})
})
let earliestDep = Infinity, latestDep = -Infinity
for (let j of model) {
if (j.legs[0].departure === null) continue
const dep = +new Date(j.legs[0].departure)
if (dep < earliestDep) earliestDep = dep
else if (dep > latestDep) latestDep = dep
}
const earlier = yield fetchJourneys(fromId, toId, {
results: 3,
// todo: single journey ref?
earlierThan: model.earlierRef
})
for (let j of earlier) {
t.ok(new Date(j.legs[0].departure) < earliestDep)
}
const later = yield fetchJourneys(fromId, toId, {
results: 3,
// todo: single journey ref?
laterThan: model.laterRef
})
for (let j of later) {
t.ok(new Date(j.legs[0].departure) > latestDep)
}
})
module.exports = testEarlierLaterJourneys

View file

@ -0,0 +1,23 @@
'use strict'
const journeysFailsWithNoProduct = (cfg) => {
const {
test: t,
fetchJourneys,
fromId,
toId,
when,
products
} = cfg
const productsObj = Object.create(null)
for (let p of products) productsObj[p.id] = false
t.throws(() => {
client.journeys(fromId, toId, {departure: when, products})
// silence rejections, we're only interested in exceptions
.catch(() => {})
})
}
module.exports = journeysFailsWithNoProduct

View file

@ -0,0 +1,30 @@
'use strict'
const isRoughlyEqual = require('is-roughly-equal')
const co = require('./co')
const testJourneysStationToAddress = co(function* (cfg) {
const {test: t, journeys, validate, fromId} = cfg
const {address, latitude, longitude} = cfg.to
validate(t, journeys, 'journeys', 'journeys')
t.strictEqual(journeys.length, 3)
for (let i = 0; i < journeys.length; i++) {
const j = journeys[i]
const firstLeg = j.legs[0]
const orig = firstLeg.origin.station || firstLeg.origin
t.ok(orig.id, fromId)
const d = j.legs[j.legs.length - 1].destination
const n = `journeys[0].legs[${i}].destination`
t.strictEqual(d.type, 'location', n + '.type is invalid')
t.strictEqual(d.address, address, n + '.address is invalid')
t.ok(isRoughlyEqual(.0001, d.latitude, latitude), n + '.latitude is invalid')
t.ok(isRoughlyEqual(.0001, d.longitude, longitude), n + '.longitude is invalid')
}
})
module.exports = testJourneysStationToAddress

View file

@ -0,0 +1,39 @@
'use strict'
const isRoughlyEqual = require('is-roughly-equal')
const co = require('./co')
const testJourneysStationToPoi = co(function* (cfg) {
const {test: t, journeys, validate, fromId} = cfg
const {id, name, latitude, longitude} = cfg.to
validate(t, journeys, 'journeys', 'journeys')
t.strictEqual(journeys.length, 3)
for (let i = 0; i < journeys.length; i++) {
const j = journeys[i]
let o = j.legs[0].origin
let oN = `journeys[0].legs[0].destination`
if (o.station) {
o = o.station
oN += '.station'
}
t.strictEqual(o.id, fromId)
let d = j.legs[j.legs.length - 1].destination
let dN = `journeys[${i}].legs[${j.legs.length - 1}].destination`
if (d.station) {
d = d.station
dN += '.station'
}
t.strictEqual(d.type, 'location', dN + '.type is invalid')
t.strictEqual(d.id, id, dN + '.id is invalid')
t.strictEqual(d.name, name, dN + '.name is invalid')
t.ok(isRoughlyEqual(.0001, d.latitude, latitude), dN + '.latitude is invalid')
t.ok(isRoughlyEqual(.0001, d.longitude, longitude), dN + '.longitude is invalid')
}
})
module.exports = testJourneysStationToPoi

View file

@ -0,0 +1,22 @@
'use strict'
const co = require('./co')
const testJourneysStationToStation = co(function* (cfg) {
const {test: t, journeys, validate, fromId, toId} = cfg
validate(t, journeys, 'journeys', 'journeys')
t.strictEqual(journeys.length, 3)
for (let i = 0; i < journeys.length; i++) {
const j = journeys[i]
let origin = j.legs[0].origin
if (origin.station) origin = origin.station
let dest = j.legs[j.legs.length - 1].destination
if (dest.station) dest = dest.station
t.strictEqual(origin.id, fromId)
t.strictEqual(dest.id, toId)
}
})
module.exports = testJourneysStationToStation

View file

@ -0,0 +1,22 @@
'use strict'
const co = require('./co')
const testJourneysWithDetour = co(function* (cfg) {
const {test: t, journeys, validate, detourIds} = cfg
// We assume that going from A to B via C *without* detour is currently
// impossible. We check if the routing engine computes a detour.
validate(t, journeys, 'journeys', 'journeys')
const leg = journeys[0].legs.some((leg) => {
return leg.stopovers && leg.stopovers.some((st) => (
st.stop.station && detourIds.includes(st.stop.station.id) ||
detourIds.includes(st.stop.id)
))
})
t.ok(leg, detourIds.join('/') + ' is not being passed')
})
module.exports = testJourneysWithDetour

View file

@ -0,0 +1,51 @@
'use strict'
const co = require('./co')
const simplify = j => j.legs.map(l => {
let departure = null
if (l.departure) {
departure = +new Date(l.departure)
if ('number' === typeof l.departureDelay) departure -= l.departureDelay * 1000
}
let arrival = null
if (l.arrival) {
arrival = +new Date(l.arrival)
if ('number' === typeof l.arrivalDelay) arrival -= l.arrivalDelay * 1000
}
return {
origin: l.origin,
destination: l.destination,
scheduledDeparture: departure,
scheduledArrival: arrival,
line: l.line
}
})
const testRefreshJourney = co(function* (cfg) {
const {
test: t,
fetchJourneys,
refreshJourney,
fromId,
toId,
when,
// todo: validate
} = cfg
const [model] = yield fetchJourneys(fromId, toId, {
results: 1, departure: when,
stopovers: false
})
// todo: move to journeys validator?
t.equal(typeof model.refreshToken, 'string')
t.ok(model.refreshToken)
const refreshed = yield refreshJourney(model.refreshToken, {
stopovers: false
})
t.deepEqual(simplify(refreshed), simplify(model))
})
module.exports = testRefreshJourney

28
test/lib/util.js Normal file
View file

@ -0,0 +1,28 @@
'use strict'
const isRoughlyEqual = require('is-roughly-equal')
const {DateTime} = require('luxon')
const a = require('assert')
const hour = 60 * 60 * 1000
const day = 24 * hour
const week = 7 * day
// next Monday 10 am
const createWhen = (timezone, locale) => {
return DateTime.fromMillis(Date.now(), {
zone: timezone,
locale,
}).startOf('week').plus({weeks: 1, hours: 10}).toJSDate()
}
const assertValidWhen = (actual, expected, name) => {
const ts = +new Date(actual)
a.ok(!Number.isNaN(ts), name + ' is not parsable by Date')
// the timestamps might be from long-distance trains
a.ok(isRoughlyEqual(day, +expected, ts), name + ' is out of range')
}
module.exports = {
hour, createWhen, assertValidWhen
}

View file

@ -0,0 +1,30 @@
'use strict'
const {defaultValidators} = require('validate-fptf')
const anyOf = require('validate-fptf/lib/any-of')
const validators = require('./validators')
const create = (cfg, customValidators = {}) => {
const val = Object.assign({}, defaultValidators)
for (let key of Object.keys(validators)) {
val[key] = validators[key](cfg)
}
Object.assign(val, customValidators)
const validateFptfWith = (t, item, allowedTypes, name) => {
try {
if ('string' === typeof allowedTypes) {
val[allowedTypes](val, item, name)
} else {
anyOf(allowedTypes, val, item, name)
}
t.pass(name + ' is valid')
} catch (err) {
t.ifError(err) // todo: improve error logging
}
}
return validateFptfWith
}
module.exports = create

364
test/lib/validators.js Normal file
View file

@ -0,0 +1,364 @@
'use strict'
const a = require('assert')
const {defaultValidators} = require('validate-fptf')
const anyOf = require('validate-fptf/lib/any-of')
const {assertValidWhen} = require('./util')
const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o)
const is = val => val !== null && val !== undefined
const createValidateStation = (cfg) => {
const validateStation = (val, s, name = 'station') => {
defaultValidators.station(val, s, name)
if (!cfg.stationCoordsOptional) {
a.ok(is(s.location), `missing ${name}.location`)
}
a.ok(isObj(s.products), name + '.products must be an object')
for (let product of cfg.products) {
const msg = name + `.products[${product.id}] must be a boolean`
a.strictEqual(typeof s.products[product.id], 'boolean', msg)
}
if ('lines' in s) {
a.ok(Array.isArray(s.lines), name + `.lines must be an array`)
for (let i = 0; i < s.lines.length; i++) {
val.line(val, s.lines[i], name + `.lines[${i}]`)
}
}
}
return validateStation
}
const validateStop = (val, s, name = 'stop') => {
// HAFAS doesn't always return the station of a stop. We mock it here
// to silence `validate-fptf`.
const station = Object.assign({}, s)
station.type = 'station'
s = Object.assign({station}, s)
defaultValidators.stop(val, s, name)
}
const validatePoi = (val, poi, name = 'location') => {
defaultValidators.location(val, poi, name)
val.ref(val, poi.id, name + '.id')
a.ok(poi.name, name + '.name must not be empty')
}
const validateAddress = (val, addr, name = 'location') => {
defaultValidators.location(val, addr, name)
a.strictEqual(typeof addr.address, 'string', name + '.address must be a string')
a.ok(addr.address, name + '.address must not be empty')
}
const validateLocation = (val, loc, name = 'location') => {
a.ok(isObj(loc), name + ' must be an object')
if (loc.type === 'stop') val.stop(val, loc, name)
else if (loc.type === 'station') val.station(val, loc, name)
else if ('id' in loc) validatePoi(val, loc, name)
else if (!('name' in loc) && ('address' in loc)) {
validateAddress(val, loc, name)
} else defaultValidators.location(val, loc, name)
}
const validateLocations = (val, locs, name = 'locations') => {
a.ok(Array.isArray(locs), name + ' must be an array')
a.ok(locs.length > 0, name + ' must not be empty')
for (let i = 0; i < locs.length; i++) {
val.location(val, locs[i], name + `[${i}]`)
}
}
const createValidateLine = (cfg) => {
const validLineModes = []
for (let product of cfg.products) {
if (!validLineModes.includes(product.mode)) {
validLineModes.push(product.mode)
}
}
const validateLine = (val, line, name = 'line') => {
defaultValidators.line(val, line, name)
a.ok(validLineModes.includes(line.mode), name + '.mode is invalid')
}
return validateLine
}
const createValidateStopover = (cfg) => {
const validateStopover = (val, s, name = 'stopover') => {
if (is(s.arrival)) {
val.date(val, s.arrival, name + '.arrival')
assertValidWhen(s.arrival, cfg.when, name)
}
if (is(s.departure)) {
val.date(val, s.departure, name + '.departure')
assertValidWhen(s.departure, cfg.when, name)
}
if (!is(s.arrival) && !is(s.departure)) {
a.fail(name + ' contains neither arrival nor departure')
}
if (is(s.arrivalDelay)) {
const msg = name + '.arrivalDelay must be a number'
a.strictEqual(typeof s.arrivalDelay, 'number', msg)
}
if (is(s.departureDelay)) {
const msg = name + '.departureDelay must be a number'
a.strictEqual(typeof s.departureDelay, 'number', msg)
}
if (is(s.arrivalPlatform)) {
const msg = name + '.arrivalPlatform must '
a.strictEqual(typeof s.arrivalPlatform, 'string', msg + 'be a string')
a.ok(s.arrivalPlatform, msg + 'not be empty')
}
if (is(s.formerScheduledArrivalPlatform)) {
const msg = name + '.formerScheduledArrivalPlatform must '
a.strictEqual(typeof s.formerScheduledArrivalPlatform, 'string', msg + 'be a string')
a.ok(s.formerScheduledArrivalPlatform, msg + 'not be empty')
}
if (is(s.departurePlatform)) {
const msg = name + '.departurePlatform must '
a.strictEqual(typeof s.departurePlatform, 'string', msg + 'be a string')
a.ok(s.departurePlatform, msg + 'not be empty')
}
if (is(s.formerScheduledDeparturePlatform)) {
const msg = name + '.formerScheduledDeparturePlatform must '
a.strictEqual(typeof s.formerScheduledDeparturePlatform, 'string', msg + 'be a string')
a.ok(s.formerScheduledDeparturePlatform, msg + 'not be empty')
}
anyOf(['stop', 'station'], val, s.stop, name + '.stop')
}
return validateStopover
}
const validateTicket = (val, ti, name = 'ticket') => {
a.strictEqual(typeof ti.name, 'string', name + '.name must be a string')
a.ok(ti.name, name + '.name must not be empty')
if (is(ti.price)) {
a.strictEqual(typeof ti.price, 'number', name + '.price must be a number')
a.ok(ti.price > 0, name + '.price must be >0')
}
if (is(ti.amount)) {
a.strictEqual(typeof ti.amount, 'number', name + '.amount must be a number')
a.ok(ti.amount > 0, name + '.amount must be >0')
}
// todo: move to VBB tests
if ('bike' in ti) {
a.strictEqual(typeof ti.bike, 'boolean', name + '.bike must be a boolean')
}
if ('shortTrip' in ti) {
a.strictEqual(typeof ti.shortTrip, 'boolean', name + '.shortTrip must be a boolean')
}
if ('group' in ti) {
a.strictEqual(typeof ti.group, 'boolean', name + '.group must be a boolean')
}
if ('fullDay' in ti) {
a.strictEqual(typeof ti.fullDay, 'boolean', name + '.fullDay must be a boolean')
}
if ('tariff' in ti) {
a.strictEqual(typeof ti.tariff, 'string', name + '.tariff must be a string')
a.ok(ti.tariff, name + '.tariff must not be empty')
}
if ('coverage' in ti) {
a.strictEqual(typeof ti.coverage, 'string', name + '.coverage must be a string')
a.ok(ti.coverage, name + '.coverage must not be empty')
}
if ('variant' in ti) {
a.strictEqual(typeof ti.variant, 'string', name + '.variant must be a string')
a.ok(ti.variant, name + '.variant must not be empty')
}
}
const createValidateJourneyLeg = (cfg) => {
const validateJourneyLeg = (val, leg, name = 'journeyLeg') => {
const withFakeScheduleAndOperator = Object.assign({
schedule: 'foo', // todo: let hafas-client parse a schedule ID
operator: 'bar' // todo: let hafas-client parse the operator
}, leg)
defaultValidators.journeyLeg(val, withFakeScheduleAndOperator, name)
if (leg.arrival !== null) {
assertValidWhen(leg.arrival, cfg.when, name + '.arrival')
}
if (leg.departure !== null) {
assertValidWhen(leg.departure, cfg.when, name + '.departure')
}
// todo: leg.arrivalPlatform !== null
if (is(leg.arrivalPlatform)) {
const msg = name + '.arrivalPlatform must be a string'
a.strictEqual(typeof leg.arrivalPlatform, 'string', msg)
a.ok(leg.arrivalPlatform, name + '.arrivalPlatform must not be empty')
}
// todo: leg.departurePlatform !== null
if (is(leg.departurePlatform)) {
const msg = name + '.departurePlatform must be a string'
a.strictEqual(typeof leg.departurePlatform, 'string', msg)
a.ok(leg.departurePlatform, name + '.departurePlatform must not be empty')
}
if ('stopovers' in leg) {
a.ok(Array.isArray(leg.stopovers), name + '.stopovers must be an array')
a.ok(leg.stopovers.length > 0, name + '.stopovers must not be empty')
for (let i = 0; i < leg.stopovers.length; i++) {
val.stopover(val, leg.stopovers[i], name + `.stopovers[${i}]`)
}
}
if (leg.mode === 'walking') {
if (leg.distance !== null) {
const msg = name + '.distance must be '
a.strictEqual(typeof leg.distance, 'number', msg + 'a number')
a.ok(leg.distance > 0, msg + '> 0')
}
} else {
const msg = name + '.direction must be a string'
a.strictEqual(typeof leg.direction, 'string', msg)
a.ok(leg.direction, name + '.direction must not be empty')
}
// todo: validate polyline
}
return validateJourneyLeg
}
const validateJourney = (val, j, name = 'journey') => {
const withFakeId = Object.assign({
id: 'foo' // todo: let hafas-client parse a journey ID
}, j)
defaultValidators.journey(val, withFakeId, name)
if ('tickets' in j) {
a.ok(Array.isArray(j.tickets), name + '.tickets must be an array')
a.ok(j.tickets.length > 0, name + '.tickets must not be empty')
for (let i = 0; i < j.tickets.length; i++) {
val.ticket(val, j.tickets[i], name + `.tickets[${i}]`)
}
}
}
const validateJourneys = (val, js, name = 'journeys') => {
a.ok(Array.isArray(js), name + ' must be an array')
a.ok(js.length > 0, name + ' must not be empty')
for (let i = 0; i < js.length; i++) {
val.journey(val, js[i], name + `[${i}]`)
}
}
const createValidateArrivalOrDeparture = (cfg) => {
const validateArrivalOrDeparture = (val, dep, name = 'arrOrDep') => {
a.ok(isObj(dep), name + ' must be an object')
// todo: let hafas-client add a .type field
a.strictEqual(typeof dep.tripId, 'string', name + '.tripId must be a string')
a.ok(dep.tripId, name + '.tripId must not be empty')
a.strictEqual(typeof dep.trip, 'number', name + '.trip must be a number')
anyOf(['stop', 'station'], val, dep.stop, name + '.stop')
assertValidWhen(dep.when, cfg.when, name)
if (dep.delay !== null) {
const msg = name + '.delay must be a number'
a.strictEqual(typeof dep.delay, 'number', msg)
}
if (dep.platform !== null) {
const msg = name + '.platform must '
a.strictEqual(typeof dep.platform, 'string', msg + 'be a string')
a.ok(dep.platform, name + 'not be empty')
}
val.line(val, dep.line, name + '.line')
a.strictEqual(typeof dep.direction, 'string', name + '.direction must be a string')
a.ok(dep.direction, name + '.direction must not be empty')
}
return validateArrivalOrDeparture
}
const validateArrivals = (val, deps, name = 'arrivals') => {
a.ok(Array.isArray(deps), name + ' must be an array')
a.ok(deps.length > 0, name + ' must not be empty')
for (let i = 0; i < deps.length; i++) {
val.arrival(val, deps[i], name + `[${i}]`)
}
}
const validateDepartures = (val, deps, name = 'departures') => {
a.ok(Array.isArray(deps), name + ' must be an array')
a.ok(deps.length > 0, name + ' must not be empty')
for (let i = 0; i < deps.length; i++) {
val.departure(val, deps[i], name + `[${i}]`)
}
}
const validateMovement = (val, m, name = 'movement') => {
a.ok(isObj(m), name + ' must be an object')
// todo: let hafas-client add a .type field
val.line(val, m.line, name + '.line')
a.strictEqual(typeof m.direction, 'string', name + '.direction must be a string')
a.ok(m.direction, name + '.direction must not be empty')
const lName = name + '.location'
val.location(val, m.location, lName)
a.ok(m.location.latitude <= 55, lName + '.latitude is too small')
a.ok(m.location.latitude >= 45, lName + '.latitude is too large')
a.ok(m.location.longitude >= 9, lName + '.longitude is too small')
a.ok(m.location.longitude <= 15, lName + '.longitude is too small')
a.ok(Array.isArray(m.nextStops), name + '.nextStops must be an array')
for (let i = 0; i < m.nextStops.length; i++) {
const st = m.nextStops[i]
val.stopover(val, m.nextStops[i], name + `.nextStops[${i}]`)
}
a.ok(Array.isArray(m.frames), name + '.frames must be an array')
a.ok(m.frames.length > 0, name + '.frames must not be empty')
for (let i = 0; i < m.frames.length; i++) {
const f = m.frames[i]
const fName = name + `.frames[${i}]`
a.ok(isObj(f), fName + ' must be an object')
anyOf(['location', 'stop', 'station'], val, f.origin, fName + '.origin')
anyOf(['location', 'stop', 'station'], val, f.destination, fName + '.destination')
a.strictEqual(typeof f.t, 'number', fName + '.frames must be a number')
}
// todo: validate polyline
}
const validateMovements = (val, ms, name = 'movements') => {
a.ok(Array.isArray(ms), name + ' must be an array')
a.ok(ms.length > 0, name + ' must not be empty')
for (let i = 0; i < ms.length; i++) {
val.movement(val, ms[i], name + `[${i}]`)
}
}
module.exports = {
station: createValidateStation,
stop: () => validateStop,
location: () => validateLocation,
locations: () => validateLocations,
poi: () => validatePoi,
address: () => validateAddress,
line: createValidateLine,
stopover: createValidateStopover,
ticket: () => validateTicket,
journeyLeg: createValidateJourneyLeg,
journey: () => validateJourney,
journeys: () => validateJourneys,
arrival: createValidateArrivalOrDeparture,
departure: createValidateArrivalOrDeparture,
departures: () => validateDepartures,
arrivals: () => validateArrivals,
movement: () => validateMovement,
movements: () => validateMovements
}

View file

@ -0,0 +1,87 @@
'use strict'
const stations = require('vbb-stations-autocomplete')
const a = require('assert')
const shorten = require('vbb-short-station-name')
const products = require('../../p/bvg/products')
const {createWhen} = require('./util')
const {
station: createValidateStation,
line: createValidateLine,
journeyLeg: createValidateJourneyLeg,
departure: createValidateDeparture,
movement: _validateMovement
} = require('./validators')
const when = createWhen('Europe/Berlin', 'de-DE')
const cfg = {
when,
stationCoordsOptional: false,
products
}
const validateDirection = (dir, name) => {
if (!stations(dir, true, false)[0]) {
console.error(name + `: station "${dir}" is unknown`)
}
}
// todo: coordsOptional = false
const _validateStation = createValidateStation(cfg)
const validateStation = (validate, s, name) => {
_validateStation(validate, s, name)
// todo: find station by ID
a.equal(s.name, shorten(s.name), name + '.name must be shortened')
}
const _validateLine = createValidateLine(cfg)
const validateLine = (validate, l, name) => {
_validateLine(validate, l, name)
if (l.symbol !== null) {
a.strictEqual(typeof l.symbol, 'string', name + '.symbol must be a string')
a.ok(l.symbol, name + '.symbol must not be empty')
}
if (l.nr !== null) {
a.strictEqual(typeof l.nr, 'number', name + '.nr must be a string')
a.ok(l.nr, name + '.nr must not be empty')
}
if (l.metro !== null) {
a.strictEqual(typeof l.metro, 'boolean', name + '.metro must be a boolean')
}
if (l.express !== null) {
a.strictEqual(typeof l.express, 'boolean', name + '.express must be a boolean')
}
if (l.night !== null) {
a.strictEqual(typeof l.night, 'boolean', name + '.night must be a boolean')
}
}
const _validateJourneyLeg = createValidateJourneyLeg(cfg)
const validateJourneyLeg = (validate, l, name) => {
_validateJourneyLeg(validate, l, name)
if (l.mode !== 'walking') {
validateDirection(l.direction, name + '.direction')
}
}
const _validateDeparture = createValidateDeparture(cfg)
const validateDeparture = (validate, dep, name) => {
_validateDeparture(validate, dep, name)
validateDirection(dep.direction, name + '.direction')
}
const validateMovement = (validate, m, name) => {
_validateMovement(validate, m, name)
validateDirection(m.direction, name + '.direction')
}
module.exports = {
cfg,
validateStation,
validateLine,
validateJourneyLeg,
validateDeparture,
validateMovement
}

View file

@ -1,70 +1,52 @@
'use strict'
// todo
// const getStations = require('db-stations').full
const tapePromise = require('tape-promise').default
const tape = require('tape')
const isRoughlyEqual = require('is-roughly-equal')
const validateFptf = require('validate-fptf')
const validateLineWithoutMode = require('./validate-line-without-mode')
const co = require('./co')
const {createWhen} = require('./lib/util')
const co = require('./lib/co')
const createClient = require('..')
const nahshProfile = require('../p/nahsh')
const {allProducts} = require('../p/nahsh/products')
const products = require('../p/nahsh/products')
const {
assertValidStation,
assertValidPoi,
assertValidAddress,
assertValidLocation,
assertValidStopover,
hour, createWhen, assertValidWhen
} = require('./util.js')
line: createValidateLine,
station: createValidateStation
} = require('./lib/validators')
const createValidate = require('./lib/validate-fptf-with')
const testJourneysStationToStation = require('./lib/journeys-station-to-station')
const testJourneysStationToAddress = require('./lib/journeys-station-to-address')
const testJourneysStationToPoi = require('./lib/journeys-station-to-poi')
const testEarlierLaterJourneys = require('./lib/earlier-later-journeys')
const testRefreshJourney = require('./lib/refresh-journey')
const journeysFailsWithNoProduct = require('./lib/journeys-fails-with-no-product')
const testDepartures = require('./lib/departures')
const testDeparturesInDirection = require('./lib/departures-in-direction')
const testArrivals = require('./lib/arrivals')
const when = createWhen('Europe/Berlin', 'de-DE')
const assertValidStationProducts = (t, p) => {
t.ok(p)
t.equal(typeof p.nationalExp, 'boolean')
t.equal(typeof p.national, 'boolean')
t.equal(typeof p.interregional, '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.onCall, 'boolean')
const cfg = {
when,
stationCoordsOptional: false,
products
}
const isKielHbf = (s) => {
return s.type === 'station' &&
(s.id === '8000199') &&
s.name === 'Kiel Hbf' &&
s.location &&
isRoughlyEqual(s.location.latitude, 54.314982, .0005) &&
isRoughlyEqual(s.location.longitude, 10.131976, .0005)
}
const assertIsKielHbf = (t, s) => {
t.equal(s.type, 'station')
t.ok(s.id === '8000199', 'id should be 8000199')
t.equal(s.name, 'Kiel Hbf')
t.ok(s.location)
t.ok(isRoughlyEqual(s.location.latitude, 54.314982, .0005))
t.ok(isRoughlyEqual(s.location.longitude, 10.131976, .0005))
}
// todo: DRY with assertValidStationProducts
// todo: DRY with other tests
const assertValidProducts = (t, p) => {
for (let product of allProducts) {
product = product.product // wat
t.equal(typeof p[product], 'boolean', 'product ' + p + ' must be a boolean')
const _validateLine = createValidateLine(cfg)
const validateLine = (validate, l, name) => {
if (l && l.product === 'onCall') {
// skip line validation
// https://github.com/derhuerst/hafas-client/issues/8#issuecomment-355839965
l = Object.assign({}, l)
l.mode = 'taxi'
}
_validateLine(validate, l, name)
}
const validate = createValidate(cfg, {
line: validateLine
})
const assertValidPrice = (t, p) => {
t.ok(p)
if (p.amount !== null) {
@ -77,275 +59,218 @@ const assertValidPrice = (t, p) => {
}
}
const assertValidLine = (t, l) => { // with optional mode
const validators = Object.assign({}, validateFptf.defaultValidators, {
line: validateLineWithoutMode
})
const recurse = validateFptf.createRecurse(validators)
try {
recurse(['line'], l, 'line')
} catch (err) {
t.ifError(err)
}
}
const test = tapePromise(tape)
const client = createClient(nahshProfile)
const client = createClient(nahshProfile, 'public-transport/hafas-client:test')
const kielHbf = '8000199'
const flensburg = '8000103'
const luebeckHbf = '8000237'
const husum = '8000181'
const schleswig = '8005362'
const kielHbf = '9049079'
const flensburg = '9027253'
const luebeckHbf = '9057819'
const husum = '9044660'
const schleswig = '9081683'
const ellerbekerMarkt = '9049027'
const seefischmarkt = '9049245'
const kielRaeucherei = '9049217'
test('Kiel Hbf to Flensburg', co(function* (t) {
test('journeys  Kiel Hbf to Flensburg', co(function* (t) {
const journeys = yield client.journeys(kielHbf, flensburg, {
when, passedStations: true
results: 3,
departure: when,
stopovers: true
})
t.ok(Array.isArray(journeys))
t.ok(journeys.length > 0, 'no journeys')
for (let journey of journeys) {
t.equal(journey.type, 'journey')
yield testJourneysStationToStation({
test: t,
journeys,
validate,
fromId: kielHbf,
toId: flensburg
})
assertValidStation(t, journey.origin)
assertValidStationProducts(t, journey.origin.products)
// todo
// 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)
}
assertValidWhen(t, journey.departure, when)
assertValidStation(t, journey.destination)
assertValidStationProducts(t, journey.origin.products)
// todo
// 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)
}
assertValidWhen(t, journey.arrival, when)
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)
// todo
// if (!(yield findStation(leg.origin.id))) {
// console.error('unknown station', leg.origin.id, leg.origin.name)
// }
assertValidWhen(t, leg.departure, when)
t.equal(typeof leg.departurePlatform, 'string')
assertValidStation(t, leg.destination)
assertValidStationProducts(t, leg.origin.products)
// todo
// if (!(yield findStation(leg.destination.id))) {
// console.error('unknown station', leg.destination.id, leg.destination.name)
// }
assertValidWhen(t, leg.arrival, when)
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)
for (let i = 0; i < journeys.length; i++) {
const j = journeys[i]
// todo: find a journey where there pricing info is always available
if (j.price) assertValidPrice(t, j.price, `journeys[${i}].price`)
}
t.end()
}))
test('Kiel Hbf to Husum, Zingel 10', co(function* (t) {
const zingel = {
// todo: journeys, only one product
test('journeys  fails with no product', (t) => {
journeysFailsWithNoProduct({
test: t,
fetchJourneys: client.journeys,
fromId: kielHbf,
toId: flensburg,
when,
products
})
t.end()
})
test('Kiel Hbf to Berliner Str. 80, Husum', co(function* (t) {
const berlinerStr = {
type: 'location',
latitude: 54.475359,
longitude: 9.050798,
address: 'Husum, Zingel 10'
address: 'Husum, Berliner Straße 80',
latitude: 54.488995,
longitude: 9.056263
}
const journeys = yield client.journeys(kielHbf, berlinerStr, {
results: 3,
departure: when
})
const journeys = yield client.journeys(kielHbf, zingel, {when})
t.ok(Array.isArray(journeys))
t.ok(journeys.length >= 1, 'no journeys')
const journey = journeys[0]
const firstLeg = journey.legs[0]
const lastLeg = journey.legs[journey.legs.length - 1]
assertValidStation(t, firstLeg.origin)
assertValidStationProducts(t, firstLeg.origin.products)
// todo
// if (!(yield findStation(leg.origin.id))) {
// console.error('unknown station', leg.origin.id, leg.origin.name)
// }
if (firstLeg.origin.products) assertValidProducts(t, firstLeg.origin.products)
assertValidWhen(t, firstLeg.departure, when)
assertValidWhen(t, firstLeg.arrival, when)
assertValidWhen(t, lastLeg.departure, when)
assertValidWhen(t, lastLeg.arrival, when)
const d = lastLeg.destination
assertValidAddress(t, d)
t.equal(d.address, 'Husum, Zingel 10')
t.ok(isRoughlyEqual(.0001, d.latitude, 54.475359))
t.ok(isRoughlyEqual(.0001, d.longitude, 9.050798))
yield testJourneysStationToAddress({
test: t,
journeys,
validate,
fromId: kielHbf,
to: berlinerStr
})
t.end()
}))
test('Holstentor to Kiel Hbf', co(function* (t) {
test('Kiel Hbf to Holstentor', co(function* (t) {
const holstentor = {
type: 'location',
latitude: 53.866321,
longitude: 10.679976,
id: '970004303',
name: 'Hansestadt Lübeck, Holstentor (Denkmal)',
id: '970003547'
latitude: 53.866321,
longitude: 10.679976
}
const journeys = yield client.journeys(holstentor, kielHbf, {when})
t.ok(Array.isArray(journeys))
t.ok(journeys.length >= 1, 'no journeys')
const journey = journeys[0]
const firstLeg = journey.legs[0]
const lastLeg = journey.legs[journey.legs.length - 1]
const o = firstLeg.origin
assertValidPoi(t, o)
t.equal(o.name, 'Hansestadt Lübeck, Holstentor (Denkmal)')
t.ok(isRoughlyEqual(.0001, o.latitude, 53.866321))
t.ok(isRoughlyEqual(.0001, o.longitude, 10.679976))
assertValidWhen(t, firstLeg.departure, when)
assertValidWhen(t, firstLeg.arrival, when)
assertValidWhen(t, lastLeg.departure, when)
assertValidWhen(t, lastLeg.arrival, when)
assertValidStation(t, lastLeg.destination)
assertValidStationProducts(t, lastLeg.destination.products)
if (lastLeg.destination.products) assertValidProducts(t, lastLeg.destination.products)
// todo
// if (!(yield findStation(leg.destination.id))) {
// console.error('unknown station', leg.destination.id, leg.destination.name)
// }
const journeys = yield client.journeys(kielHbf, holstentor, {
results: 3,
departure: when
})
yield testJourneysStationToPoi({
test: t,
journeys,
validate,
fromId: kielHbf,
to: holstentor
})
t.end()
}))
test('Husum to Lübeck Hbf with stopover at Husum', co(function* (t) {
const [journey] = yield client.journeys(husum, luebeckHbf, {
test('Husum to Lübeck Hbf with stopover at Kiel Hbf', co(function* (t) {
const journeys = yield client.journeys(husum, luebeckHbf, {
via: kielHbf,
results: 1,
when
departure: when,
stopovers: true
})
const i1 = journey.legs.findIndex(leg => leg.destination.id === kielHbf)
t.ok(i1 >= 0, 'no leg with Kiel Hbf as destination')
validate(t, journeys, 'journeys', 'journeys')
const i2 = journey.legs.findIndex(leg => leg.origin.id === kielHbf)
t.ok(i2 >= 0, 'no leg with Kiel Hbf as origin')
t.ok(i2 > i1, 'leg with Kiel Hbf as origin must be after leg to it')
const leg = journeys[0].legs.some((leg) => {
return leg.stopovers && leg.stopovers.some((stopover) => {
const s = stopover.stop
return s.station && s.station.id === kielHbf || s.id === kielHbf
})
})
t.ok(leg, 'Kiel Hbf is not being passed')
t.end()
}))
test('earlier/later journeys, Kiel Hbf -> Flensburg', co(function* (t) {
const model = yield client.journeys(kielHbf, flensburg, {
results: 3, when
yield testEarlierLaterJourneys({
test: t,
fetchJourneys: client.journeys,
validate,
fromId: kielHbf,
toId: flensburg
})
t.equal(typeof model.earlierRef, 'string')
t.ok(model.earlierRef)
t.equal(typeof model.laterRef, 'string')
t.ok(model.laterRef)
// when and earlierThan/laterThan should be mutually exclusive
t.throws(() => {
client.journeys(kielHbf, flensburg, {
when, earlierThan: model.earlierRef
})
})
t.throws(() => {
client.journeys(kielHbf, flensburg, {
when, laterThan: model.laterRef
})
})
let earliestDep = Infinity, latestDep = -Infinity
for (let j of model) {
const dep = +new Date(j.departure)
if (dep < earliestDep) earliestDep = dep
else if (dep > latestDep) latestDep = dep
}
const earlier = yield client.journeys(kielHbf, flensburg, {
results: 3,
// todo: single journey ref?
earlierThan: model.earlierRef
})
for (let j of earlier) {
t.ok(new Date(j.departure) < earliestDep)
}
const later = yield client.journeys(kielHbf, flensburg, {
results: 3,
// todo: single journey ref?
laterThan: model.laterRef
})
for (let j of later) {
t.ok(new Date(j.departure) > latestDep)
}
t.end()
}))
test('leg details for Flensburg to Husum', co(function* (t) {
test('refreshJourney', co(function* (t) {
yield testRefreshJourney({
test: t,
fetchJourneys: client.journeys,
refreshJourney: client.refreshJourney,
validate,
fromId: kielHbf,
toId: flensburg,
when
})
t.end()
}))
// todo: with detour test
// todo: without detour test
test('trip details', co(function* (t) {
const journeys = yield client.journeys(flensburg, husum, {
results: 1, when
results: 1, departure: 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)
const trip = yield client.trip(p.id, p.line.name, {when})
validate(t, trip, 'journeyLeg', 'trip')
t.end()
}))
test('departures at Kiel Hbf', co(function* (t) {
const deps = yield client.departures(kielHbf, {
test('departures at Kiel Räucherei', co(function* (t) {
const departures = yield client.departures(kielRaeucherei, {
duration: 30, when
})
t.ok(Array.isArray(deps))
for (let dep of deps) {
assertValidStation(t, dep.station)
assertValidStationProducts(t, dep.station.products)
// todo
// 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)
assertValidWhen(t, dep.when, when)
}
yield testDepartures({
test: t,
departures,
validate,
id: kielRaeucherei
})
t.end()
}))
test('departures with station object', co(function* (t) {
const deps = yield client.departures({
type: 'station',
id: kielHbf,
name: 'Kiel Hbf',
location: {
type: 'location',
latitude: 1.23,
longitude: 2.34
}
}, {when})
validate(t, deps, 'departures', 'departures')
t.end()
}))
test('departures at Berlin Hbf in direction of Berlin Ostbahnhof', co(function* (t) {
yield testDeparturesInDirection({
test: t,
fetchDepartures: client.departures,
fetchTrip: client.trip,
id: ellerbekerMarkt,
directionIds: [seefischmarkt, '710102'],
when,
validate
})
t.end()
}))
test('arrivals at Kiel Räucherei', co(function* (t) {
const arrivals = yield client.arrivals(kielRaeucherei, {
duration: 30, when
})
yield testArrivals({
test: t,
arrivals,
validate,
id: kielRaeucherei
})
t.end()
}))
@ -359,94 +284,66 @@ test('nearby Kiel Hbf', co(function* (t) {
results: 2, distance: 400
})
validate(t, nearby, 'locations', 'nearby')
t.ok(Array.isArray(nearby))
t.equal(nearby.length, 2)
assertIsKielHbf(t, nearby[0])
t.ok(nearby[0].id === kielHbf || nearby[0].id === '8000199')
t.equal(nearby[0].name, 'Kiel Hbf')
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 Kiel', co(function* (t) {
const locations = yield client.locations('Kiel', {
results: 10
results: 20
})
t.ok(Array.isArray(locations))
t.ok(locations.length > 0)
t.ok(locations.length <= 10)
validate(t, locations, 'locations', 'locations')
t.ok(locations.length <= 20)
for (let l of locations) {
if (l.type === 'station') assertValidStation(t, l)
else assertValidLocation(t, l)
}
t.ok(locations.some(isKielHbf))
t.ok(locations.find(s => s.type === 'stop' || s.type === 'station'))
t.ok(locations.find(s => s.id && s.name)) // POIs
t.ok(locations.some(l => l.station && s.station.id === kielHbf || l.id === kielHbf))
t.end()
}))
test('location', co(function* (t) {
const loc = yield client.location(schleswig)
test('station', co(function* (t) {
const s = yield client.station(kielHbf)
assertValidStation(t, loc)
t.equal(loc.id, schleswig)
validate(t, s, ['stop', 'station'], 'station')
t.equal(s.id, kielHbf)
t.end()
}))
// todo: see #34
test.skip('radar Kiel', co(function* (t) {
const vehicles = yield client.radar(54.4, 10.0, 54.2, 10.2, {
test('radar', co(function* (t) {
const vehicles = yield client.radar({
north: 54.4,
west: 10.0,
south: 54.2,
east: 10.2
}, {
duration: 5 * 60, when
})
t.ok(Array.isArray(vehicles))
t.ok(vehicles.length > 0)
for (let v of vehicles) {
// todo
// t.ok(findStation(v.direction))
assertValidLine(t, v.line)
t.equal(typeof v.location.latitude, 'number')
t.ok(v.location.latitude <= 57, 'vehicle is too far away')
t.ok(v.location.latitude >= 51, 'vehicle is too far away')
t.equal(typeof v.location.longitude, 'number')
t.ok(v.location.longitude >= 7, 'vehicle is too far away')
t.ok(v.location.longitude <= 13, 'vehicle is too far away')
t.ok(Array.isArray(v.nextStops))
for (let st of v.nextStops) {
assertValidStopover(t, st, true)
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)
t.ok(isRoughlyEqual(14 * hour, +when, dep))
}
// todo: cfg.stationProductsOptional option
const allProducts = products.reduce((acc, p) => (acc[p.id] = true, acc), {})
const validateStation = createValidateStation(cfg)
const validate = createValidate(cfg, {
station: (validate, s, name) => {
s = Object.assign({
products: allProducts // todo: fix station.products
}, s)
if (!s.name) s.name = 'foo' // todo, see #34
validateStation(validate, s, name)
}
})
validate(t, vehicles, 'movements', 'vehicles')
t.ok(Array.isArray(v.frames))
for (let f of v.frames) {
assertValidStation(t, f.origin, true)
assertValidStationProducts(t, f.origin.products)
assertValidStation(t, f.destination, true)
assertValidStationProducts(t, f.destination.products)
t.equal(typeof f.t, 'number')
}
}
t.end()
}))

View file

@ -1,86 +1,42 @@
'use strict'
// todo
// const getStations = require('db-stations').full
const tapePromise = require('tape-promise').default
const tape = require('tape')
const isRoughlyEqual = require('is-roughly-equal')
const validateFptf = require('validate-fptf')
const validateLine = require('validate-fptf/line')
const validateLineWithoutMode = require('./validate-line-without-mode')
const co = require('./co')
const {createWhen} = require('./lib/util')
const co = require('./lib/co')
const createClient = require('..')
const oebbProfile = require('../p/oebb')
const {allProducts} = require('../p/oebb/products')
const products = require('../p/oebb/products')
const {
assertValidStation,
assertValidPoi,
assertValidAddress,
assertValidLocation,
assertValidStopover,
hour, createWhen, assertValidWhen
} = require('./util.js')
station: createValidateStation,
stop: validateStop
} = require('./lib/validators')
const createValidate = require('./lib/validate-fptf-with')
const testJourneysStationToStation = require('./lib/journeys-station-to-station')
const testJourneysStationToAddress = require('./lib/journeys-station-to-address')
const testJourneysStationToPoi = require('./lib/journeys-station-to-poi')
const testEarlierLaterJourneys = require('./lib/earlier-later-journeys')
const testRefreshJourney = require('./lib/refresh-journey')
const journeysFailsWithNoProduct = require('./lib/journeys-fails-with-no-product')
const testJourneysWithDetour = require('./lib/journeys-with-detour')
const testDeparturesInDirection = require('./lib/departures-in-direction')
const when = createWhen('Europe/Vienna', 'de-AT')
const assertValidStationProducts = (t, p) => {
t.ok(p)
t.equal(typeof p.nationalExp, 'boolean')
t.equal(typeof p.national, 'boolean')
t.equal(typeof p.interregional, '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.onCall, 'boolean')
const cfg = {
when,
stationCoordsOptional: false,
products
}
// todo
// 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)
// })
// todo validateDirection: search list of stations for direction
const isSalzburgHbf = (s) => {
return s.type === 'station' &&
(s.id === '008100002' || s.id === '8100002') &&
s.name === 'Salzburg Hbf' &&
s.location &&
isRoughlyEqual(s.location.latitude, 47.812851, .0005) &&
isRoughlyEqual(s.location.longitude, 13.045604, .0005)
}
const assertIsSalzburgHbf = (t, s) => {
t.equal(s.type, 'station')
t.ok(s.id === '008100002' || s.id === '8100002', 'id should be 8100002')
t.equal(s.name, 'Salzburg Hbf')
t.ok(s.location)
t.ok(isRoughlyEqual(s.location.latitude, 47.812851, .0005))
t.ok(isRoughlyEqual(s.location.longitude, 13.045604, .0005))
}
// todo: DRY with assertValidStationProducts
// todo: DRY with other tests
const assertValidProducts = (t, p) => {
for (let product of allProducts) {
product = product.product // wat
t.equal(typeof p[product], 'boolean', 'product ' + p + ' must be a boolean')
}
}
const validate = createValidate(cfg, {
line: validateLine // bypass line validator in lib/validators
})
const assertValidPrice = (t, p) => {
t.ok(p)
@ -94,167 +50,97 @@ const assertValidPrice = (t, p) => {
}
}
// todo: fix this upstream
// see https://github.com/public-transport/hafas-client/blob/c6e558be217667f1bcdac4a605898eb75ea80374/p/oebb/products.js#L71
const assertValidLine = (t, l) => { // with optional mode
const validators = Object.assign({}, validateFptf.defaultValidators, {
line: validateLineWithoutMode
})
const recurse = validateFptf.createRecurse(validators)
try {
recurse(['line'], l, 'line')
} catch (err) {
t.ifError(err)
}
}
const test = tapePromise(tape)
const client = createClient(oebbProfile)
const client = createClient(oebbProfile, 'public-transport/hafas-client:test')
const salzburgHbf = '8100002'
const wienWestbahnhof = '1291501'
const wienFickeystr = '911014'
const wien = '1190100'
const wienWestbahnhof = '1291501'
const klagenfurtHbf = '8100085'
const muenchenHbf = '8000261'
const grazHbf = '8100173'
const wienRenngasse = '1390186'
const wienKarlsplatz = '1390461'
const wienPilgramgasse = '1390562'
test('Salzburg Hbf to Wien Westbahnhof', co(function* (t) {
const journeys = yield client.journeys(salzburgHbf, wienWestbahnhof, {
when, passedStations: true
test.skip('journeys  Salzburg Hbf to Wien Westbahnhof', co(function* (t) {
const journeys = yield client.journeys(salzburgHbf, wienFickeystr, {
results: 3,
departure: when,
stopovers: true
})
t.ok(Array.isArray(journeys))
t.ok(journeys.length > 0, 'no journeys')
for (let journey of journeys) {
t.equal(journey.type, 'journey')
yield testJourneysStationToStation({
test: t,
journeys,
validate,
fromId: salzburgHbf,
toId: wienFickeystr
})
assertValidStation(t, journey.origin)
assertValidStationProducts(t, journey.origin.products)
// todo
// 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)
}
assertValidWhen(t, journey.departure, when)
assertValidStation(t, journey.destination)
assertValidStationProducts(t, journey.origin.products)
// todo
// 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)
}
assertValidWhen(t, journey.arrival, when)
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)
// todo
// if (!(yield findStation(leg.origin.id))) {
// console.error('unknown station', leg.origin.id, leg.origin.name)
// }
assertValidWhen(t, leg.departure, when)
t.equal(typeof leg.departurePlatform, 'string')
assertValidStation(t, leg.destination)
assertValidStationProducts(t, leg.origin.products)
// todo
// if (!(yield findStation(leg.destination.id))) {
// console.error('unknown station', leg.destination.id, leg.destination.name)
// }
assertValidWhen(t, leg.arrival, when)
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)
for (let i = 0; i < journeys.length; i++) {
const j = journeys[i]
if (j.price) assertValidPrice(t, j.price, `journeys[${i}].price`)
}
t.end()
}))
// todo: journeys, only one product
test('journeys  fails with no product', (t) => {
journeysFailsWithNoProduct({
test: t,
fetchJourneys: client.journeys,
fromId: salzburgHbf,
toId: wienFickeystr,
when,
products
})
t.end()
})
test('Salzburg Hbf to 1220 Wien, Wagramer Straße 5', co(function* (t) {
const wagramerStr = {
type: 'location',
address: '1220 Wien, Wagramer Straße 5',
latitude: 48.236216,
longitude: 16.425863,
address: '1220 Wien, Wagramer Straße 5'
longitude: 16.425863
}
const journeys = yield client.journeys(salzburgHbf, wagramerStr, {
results: 3,
departure: when
})
const journeys = yield client.journeys(salzburgHbf, wagramerStr, {when})
t.ok(Array.isArray(journeys))
t.ok(journeys.length >= 1, 'no journeys')
const journey = journeys[0]
const firstLeg = journey.legs[0]
const lastLeg = journey.legs[journey.legs.length - 1]
assertValidStation(t, firstLeg.origin)
assertValidStationProducts(t, firstLeg.origin.products)
// todo
// if (!(yield findStation(leg.origin.id))) {
// console.error('unknown station', leg.origin.id, leg.origin.name)
// }
if (firstLeg.origin.products) assertValidProducts(t, firstLeg.origin.products)
assertValidWhen(t, firstLeg.departure, when)
assertValidWhen(t, firstLeg.arrival, when)
assertValidWhen(t, lastLeg.departure, when)
assertValidWhen(t, lastLeg.arrival, when)
const d = lastLeg.destination
assertValidAddress(t, d)
t.equal(d.address, '1220 Wien, Wagramer Straße 5')
t.ok(isRoughlyEqual(.0001, d.latitude, 48.236216))
t.ok(isRoughlyEqual(.0001, d.longitude, 16.425863))
yield testJourneysStationToAddress({
test: t,
journeys,
validate,
fromId: salzburgHbf,
to: wagramerStr
})
t.end()
}))
test('Albertina to Salzburg Hbf', co(function* (t) {
test('Salzburg Hbf to Albertina', co(function* (t) {
const albertina = {
type: 'location',
latitude: 48.204699,
longitude: 16.368404,
id: '975900003',
name: 'Albertina',
id: '975900003'
latitude: 48.204699,
longitude: 16.368404
}
const journeys = yield client.journeys(albertina, salzburgHbf, {when})
t.ok(Array.isArray(journeys))
t.ok(journeys.length >= 1, 'no journeys')
const journey = journeys[0]
const firstLeg = journey.legs[0]
const lastLeg = journey.legs[journey.legs.length - 1]
const o = firstLeg.origin
assertValidPoi(t, o)
t.equal(o.name, 'Albertina')
t.ok(isRoughlyEqual(.0001, o.latitude, 48.204699))
t.ok(isRoughlyEqual(.0001, o.longitude, 16.368404))
assertValidWhen(t, firstLeg.departure, when)
assertValidWhen(t, firstLeg.arrival, when)
assertValidWhen(t, lastLeg.departure, when)
assertValidWhen(t, lastLeg.arrival, when)
assertValidStation(t, lastLeg.destination)
assertValidStationProducts(t, lastLeg.destination.products)
if (lastLeg.destination.products) assertValidProducts(t, lastLeg.destination.products)
// todo
// if (!(yield findStation(leg.destination.id))) {
// console.error('unknown station', leg.destination.id, leg.destination.name)
// }
const journeys = yield client.journeys(salzburgHbf, albertina, {
results: 3, departure: when
})
yield testJourneysStationToPoi({
test: t,
journeys,
validate,
fromId: salzburgHbf,
to: albertina
})
t.end()
}))
@ -265,237 +151,247 @@ test('journeys: via works with detour', co(function* (t) {
const schottenring = '001390163'
const donauinsel = '001392277'
const donauinselPassed = '922001'
const [journey] = yield client.journeys(stephansplatz, schottenring, {
const journeys = yield client.journeys(stephansplatz, schottenring, {
via: donauinsel,
results: 1,
when,
passedStations: true
departure: when,
stopovers: true
})
t.ok(journey)
const l = journey.legs.some(l => l.passed && l.passed.some(p => p.station.id === donauinselPassed))
t.ok(l, 'Donauinsel is not being passed')
yield testJourneysWithDetour({
test: t,
journeys,
validate,
detourIds: [donauinsel, donauinselPassed]
})
t.end()
}))
test('journeys: via works without detour', co(function* (t) {
// When going from Karlsplatz to Praterstern via Museumsquartier, there is *no need*
// to change trains / no need for a "detour".
// When going from Karlsplatz to Praterstern via Museumsquartier, there is
// *no need* to change trains / no need for a "detour".
const karlsplatz = '001390461'
const praterstern = '001290201'
const museumsquartier = '001390171'
const museumsquartierPassed = '901014'
const [journey] = yield client.journeys(karlsplatz, praterstern, {
const journeys = yield client.journeys(karlsplatz, praterstern, {
via: museumsquartier,
results: 1,
when,
passedStations: true
departure: when,
stopovers: true
})
t.ok(journey)
validate(t, journeys, 'journeys', 'journeys')
const l = journey.legs.some(l => l.passed && l.passed.some(p => p.station.id === museumsquartierPassed))
t.ok(l, 'Weihburggasse is not being passed')
const l1 = journeys[0].legs.some((leg) => {
return (
leg.destination.id === museumsquartier ||
leg.destination.id === museumsquartierPassed
)
})
t.notOk(l1, 'transfer at Museumsquartier')
const l2 = journeys[0].legs.some((leg) => {
return leg.stopovers && leg.stopovers.some((stopover) => {
return stopover.stop.id === museumsquartierPassed
})
})
t.ok(l2, 'Museumsquartier is not being passed')
t.end()
}))
test('earlier/later journeys, Salzburg Hbf -> Wien Westbahnhof', co(function* (t) {
const model = yield client.journeys(salzburgHbf, wienWestbahnhof, {
results: 3, when
yield testEarlierLaterJourneys({
test: t,
fetchJourneys: client.journeys,
validate,
fromId: salzburgHbf,
toId: wienWestbahnhof
})
t.equal(typeof model.earlierRef, 'string')
t.ok(model.earlierRef)
t.equal(typeof model.laterRef, 'string')
t.ok(model.laterRef)
// when and earlierThan/laterThan should be mutually exclusive
t.throws(() => {
client.journeys(salzburgHbf, wienWestbahnhof, {
when, earlierThan: model.earlierRef
})
})
t.throws(() => {
client.journeys(salzburgHbf, wienWestbahnhof, {
when, laterThan: model.laterRef
})
})
let earliestDep = Infinity, latestDep = -Infinity
for (let j of model) {
const dep = +new Date(j.departure)
if (dep < earliestDep) earliestDep = dep
else if (dep > latestDep) latestDep = dep
}
const earlier = yield client.journeys(salzburgHbf, wienWestbahnhof, {
results: 3,
// todo: single journey ref?
earlierThan: model.earlierRef
})
for (let j of earlier) {
t.ok(new Date(j.departure) < earliestDep)
}
const later = yield client.journeys(salzburgHbf, wienWestbahnhof, {
results: 3,
// todo: single journey ref?
laterThan: model.laterRef
})
for (let j of later) {
t.ok(new Date(j.departure) > latestDep)
}
t.end()
}))
test('leg details for Wien Westbahnhof to München Hbf', co(function* (t) {
test('refreshJourney', co(function* (t) {
yield testRefreshJourney({
test: t,
fetchJourneys: client.journeys,
refreshJourney: client.refreshJourney,
validate,
fromId: salzburgHbf,
toId: wienWestbahnhof,
when
})
t.end()
}))
test('trip details', co(function* (t) {
const journeys = yield client.journeys(wienWestbahnhof, muenchenHbf, {
results: 1, when
results: 1, departure: 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)
const trip = yield client.trip(p.id, p.line.name, {when})
validate(t, trip, 'journeyLeg', 'trip')
t.end()
}))
test('departures at Salzburg Hbf', co(function* (t) {
const deps = yield client.departures(salzburgHbf, {
duration: 5, when
test('departures at Wien Leibenfrostgasse', co(function* (t) {
const wienLeibenfrostgasse = '1390469'
const ids = [
wienLeibenfrostgasse, // station
'904029', // stop "Wien Leibenfrostgasse (Phorusgasse)s"
'904030' // stop "Wien Leibenfrostgasse (Ziegelofengasse)"
]
const deps = yield client.departures(wienLeibenfrostgasse, {
duration: 15, when
})
t.ok(Array.isArray(deps))
for (let dep of deps) {
assertValidStation(t, dep.station)
assertValidStationProducts(t, dep.station.products)
// todo
// 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)
assertValidWhen(t, dep.when, when)
validate(t, deps, 'departures', 'departures')
t.ok(deps.length > 0, 'must be >0 departures')
// todo: move into deps validator
t.deepEqual(deps, deps.sort((a, b) => t.when > b.when))
for (let i = 0; i < deps.length; i++) {
const dep = deps[i]
const msg = `deps[${i}].stop.id is invalid`
t.ok(ids.includes(dep.stop.id, msg))
}
t.end()
}))
test('departures with station object', co(function* (t) {
const deps = yield client.departures({
type: 'station',
id: salzburgHbf,
name: 'Salzburg Hbf',
location: {
type: 'location',
latitude: 1.23,
longitude: 2.34
}
}, {when})
validate(t, deps, 'departures', 'departures')
t.end()
}))
test('departures at Karlsplatz in direction of Pilgramgasse', co(function* (t) {
yield testDeparturesInDirection({
test: t,
fetchDepartures: client.departures,
fetchTrip: client.trip,
id: wienKarlsplatz,
directionIds: [wienPilgramgasse, '905002'],
when,
validate
})
t.end()
}))
// todo: arrivals
test('nearby Salzburg Hbf', co(function* (t) {
const salzburgHbfPosition = {
const nearby = yield client.nearby({
type: 'location',
longitude: 13.045604,
latitude: 47.812851
}
const nearby = yield client.nearby(salzburgHbfPosition, {
results: 2, distance: 400
}, {
results: 5, distance: 400
})
t.ok(Array.isArray(nearby))
t.equal(nearby.length, 2)
validate(t, nearby, 'locations', 'nearby')
t.equal(nearby.length, 5)
assertIsSalzburgHbf(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)
}
const s = nearby[0]
t.ok(s.id === '008100002' || s.id === '8100002', 'id should be 8100002')
t.equal(s.name, 'Salzburg Hbf')
t.ok(isRoughlyEqual(.0005, s.location.latitude, 47.812851))
t.ok(isRoughlyEqual(.0005, s.location.longitude, 13.045604))
t.ok(s.distance >= 0)
t.ok(s.distance <= 100)
t.end()
}))
test('locations named Salzburg', co(function* (t) {
const locations = yield client.locations('Salzburg', {
results: 10
results: 20
})
t.ok(Array.isArray(locations))
t.ok(locations.length > 0)
t.ok(locations.length <= 10)
validate(t, locations, 'locations', 'locations')
t.ok(locations.length <= 20)
for (let l of locations) {
if (l.type === 'station') assertValidStation(t, l)
else assertValidLocation(t, l)
}
t.ok(locations.some(isSalzburgHbf))
t.ok(locations.find(s => s.type === 'stop' || s.type === 'station'))
t.ok(locations.find(s => s.id && s.name)) // POIs
t.ok(locations.some((s) => {
// todo: trim IDs
if (s.station) {
if (s.station.id === '008100002' || s.station.id === '8100002') return true
}
return s.id === '008100002' || s.id === '8100002'
}))
t.end()
}))
test('location', co(function* (t) {
const loc = yield client.location(grazHbf)
test('station', co(function* (t) {
const loc = yield client.station(wienRenngasse)
assertValidStation(t, loc)
t.equal(loc.id, grazHbf)
// todo: find a way to always get products from the API
// todo: cfg.stationProductsOptional option
const allProducts = products.reduce((acc, p) => (acc[p.id] = true, acc), {})
const validateStation = createValidateStation(cfg)
const validate = createValidate(cfg, {
stop: (validate, s, name) => {
const withFakeProducts = Object.assign({products: allProducts}, s)
validateStop(validate, withFakeProducts, name)
},
station: (validate, s, name) => {
const withFakeProducts = Object.assign({products: allProducts}, s)
validateStation(validate, withFakeProducts, name)
}
})
validate(t, loc, ['stop', 'station'], 'station')
t.equal(loc.id, wienRenngasse)
t.end()
}))
test('radar Salzburg', co(function* (t) {
const vehicles = yield client.radar(47.827203, 13.001261, 47.773278, 13.07562, {
duration: 5 * 60, when
let vehicles = yield client.radar({
north: 47.827203,
west: 13.001261,
south: 47.773278,
east: 13.07562
}, {
duration: 5 * 60,
// when
})
t.ok(Array.isArray(vehicles))
t.ok(vehicles.length > 0)
for (let v of vehicles) {
// todo: find a way to always get frames from the API
vehicles = vehicles.filter(m => m.frames && m.frames.length > 0)
// todo
// t.ok(findStation(v.direction))
assertValidLine(t, v.line)
// todo: find a way to always get products from the API
// todo: cfg.stationProductsOptional option
const allProducts = products.reduce((acc, p) => (acc[p.id] = true, acc), {})
const validateStation = createValidateStation(cfg)
const validate = createValidate(cfg, {
station: (validate, s, name) => {
const withFakeProducts = Object.assign({products: allProducts}, s)
validateStation(validate, withFakeProducts, name)
},
line: validateLine
})
validate(t, vehicles, 'movements', 'vehicles')
t.equal(typeof v.location.latitude, 'number')
t.ok(v.location.latitude <= 52, 'vehicle is too far away')
t.ok(v.location.latitude >= 42, 'vehicle is too far away')
t.equal(typeof v.location.longitude, 'number')
t.ok(v.location.longitude >= 10, 'vehicle is too far away')
t.ok(v.location.longitude <= 16, 'vehicle is too far away')
t.ok(Array.isArray(v.nextStops))
for (let st of v.nextStops) {
assertValidStopover(t, st, true)
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)
t.ok(isRoughlyEqual(14 * hour, +when, dep))
}
}
t.ok(Array.isArray(v.frames))
for (let f of v.frames) {
assertValidStation(t, f.origin, true)
// can contain stations in germany which don't have a products property, would break
// assertValidStationProducts(t, f.origin.products)
assertValidStation(t, f.destination, true)
// can contain stations in germany which don't have a products property, would break
// assertValidStationProducts(t, f.destination.products)
t.equal(typeof f.t, 'number')
}
}
t.end()
}))

View file

@ -5,6 +5,7 @@ const test = require('tape')
const createThrottledClient = require('../throttle')
const vbbProfile = require('../p/vbb')
const userAgent = 'public-transport/hafas-client:test'
const spichernstr = '900000042101'
test('throttle works', (t) => {
@ -15,7 +16,7 @@ test('throttle works', (t) => {
}
const mockProfile = Object.assign({}, vbbProfile, {transformReqBody})
const client = createThrottledClient(mockProfile, 2, 1000)
const client = createThrottledClient(mockProfile, userAgent, 2, 1000)
for (let i = 0; i < 10; i++) client.departures(spichernstr, {duration: 1})
t.plan(3)

View file

@ -1,158 +0,0 @@
'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 createWhen = (timezone, locale) => {
return DateTime.fromMillis(Date.now(), {
zone: timezone,
locale,
}).startOf('week').plus({weeks: 1, hours: 10}).toJSDate()
}
const isValidWhen = (actual, expected) => {
const ts = +new Date(actual)
if (Number.isNaN(ts)) return false
return isRoughlyEqual(12 * hour, +expected, ts)
}
const assertValidWhen = (t, actual, expected) => {
t.ok(isValidWhen(actual, expected), '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, createWhen, isValidWhen, assertValidWhen,
assertValidTicket
}

View file

@ -1,38 +0,0 @@
'use strict'
const a = require('assert')
const is = require('@sindresorhus/is')
const validateItem = require('validate-fptf/lib/item')
const validateReference = require('validate-fptf/lib/reference')
// todo: this is copied code, DRY this up!
// see https://github.com/public-transport/validate-fptf/blob/373b4847ec9668c4a9ec9b0dbd50f8a70ffbe127/line.js
const validateLineWithoutMode = (validate, line, name) => {
validateItem(line, name)
a.strictEqual(line.type, 'line', name + '.type must be `line`')
validateReference(line.id, name + '.id')
a.strictEqual(typeof line.name, 'string', name + '.name must be a string')
a.ok(line.name.length > 0, name + '.name can\'t be empty')
// skipping line validation here
// see https://github.com/public-transport/hafas-client/issues/8#issuecomment-355839965
if (is.undefined(line.mode) || is.null(line.mode)) {
console.error(`ÖBB: Missing \`mode\` for line ${line.name} (at ${name}).`)
}
if (!is.undefined(line.subMode)) {
a.fail(name + '.subMode is reserved an should not be used for now')
}
// todo: routes
if (!is.null(line.operator) && !is.undefined(line.operator)) {
validate(['operator'], line.operator, name + '.operator')
}
}
module.exports = validateLineWithoutMode

View file

@ -1,125 +1,76 @@
'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 shorten = require('vbb-short-station-name')
const co = require('./co')
const co = require('./lib/co')
const createClient = require('..')
const vbbProfile = require('../p/vbb')
const products = require('../p/vbb/products')
const {
assertValidStation: _assertValidStation,
assertValidPoi,
assertValidAddress,
assertValidLocation,
assertValidLine: _assertValidLine,
assertValidStopover,
hour, createWhen,
assertValidWhen,
assertValidTicket
} = require('./util')
cfg,
validateStation,
validateLine,
validateJourneyLeg,
validateDeparture,
validateMovement
} = require('./lib/vbb-bvg-validators')
const createValidate = require('./lib/validate-fptf-with')
const testJourneysStationToStation = require('./lib/journeys-station-to-station')
const testJourneysStationToAddress = require('./lib/journeys-station-to-address')
const testJourneysStationToPoi = require('./lib/journeys-station-to-poi')
const testEarlierLaterJourneys = require('./lib/earlier-later-journeys')
const testRefreshJourney = require('./lib/refresh-journey')
const journeysFailsWithNoProduct = require('./lib/journeys-fails-with-no-product')
const testDepartures = require('./lib/departures')
const testDeparturesInDirection = require('./lib/departures-in-direction')
const testDeparturesWithoutRelatedStations = require('./lib/departures-without-related-stations')
const testArrivals = require('./lib/arrivals')
const testJourneysWithDetour = require('./lib/journeys-with-detour')
const when = createWhen('Europe/Berlin', 'de-DE')
const when = cfg.when
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')
}
const findStation = (query) => stations(query, true, false)[0]
const validate = createValidate(cfg, {
station: validateStation,
line: validateLine,
journeyLeg: validateJourneyLeg,
departure: validateDeparture,
movement: validateMovement
})
const test = tapePromise(tape)
const client = createClient(vbbProfile)
const client = createClient(vbbProfile, 'public-transport/hafas-client:test')
const amrumerStr = '900000009101'
const spichernstr = '900000042101'
const bismarckstr = '900000024201'
const westhafen = '900000001201'
const wedding = '900000009104'
const württembergallee = '900000026153'
test('journeys  station to station', co(function* (t) {
const journeys = yield client.journeys(spichernstr, amrumerStr, {
results: 3, when, passedStations: true
test('journeys  Spichernstr. to Bismarckstr.', co(function* (t) {
const journeys = yield client.journeys(spichernstr, bismarckstr, {
results: 3,
departure: when,
stopovers: true
})
t.ok(Array.isArray(journeys))
t.strictEqual(journeys.length, 3)
yield testJourneysStationToStation({
test: t,
journeys,
validate,
fromId: spichernstr,
toId: bismarckstr
})
// todo: find a journey where there ticket info is always available
for (let journey of journeys) {
t.equal(journey.type, 'journey')
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, when)
assertValidStation(t, journey.destination)
assertValidStationProducts(t, journey.destination.products)
t.strictEqual(journey.destination.id, amrumerStr)
assertValidWhen(t, journey.arrival, when)
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, when)
assertValidStation(t, leg.destination)
assertValidStationProducts(t, leg.destination.products)
t.strictEqual(leg.destination.id, amrumerStr)
assertValidWhen(t, leg.arrival, when)
assertValidLine(t, leg.line)
if (!findStation(leg.direction)) {
const err = new Error('unknown direction: ' + leg.direction)
err.stack = err.stack.split('\n').slice(0, 2).join('\n')
console.error(err)
}
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(function* (t) {
const journeys = yield client.journeys(spichernstr, bismarckstr, {
results: 20, when,
results: 20,
departure: when,
products: {
suburban: false,
subway: true,
@ -131,259 +82,160 @@ test('journeys  only subway', co(function* (t) {
}
})
t.ok(Array.isArray(journeys))
validate(t, journeys, 'journeys', 'journeys')
t.ok(journeys.length > 1)
for (let i = 0; i < journeys.length; i++) {
const journey = journeys[i]
for (let j = 0; j < journey.legs.length; j++) {
const leg = journey.legs[j]
for (let journey of journeys) {
for (let leg of journey.legs) {
const name = `journeys[${i}].legs[${i}].line`
if (leg.line) {
assertValidLine(t, leg.line)
t.equal(leg.line.mode, 'train')
t.equal(leg.line.product, 'subway')
t.equal(leg.line.mode, 'train', name + '.mode is invalid')
t.equal(leg.line.product, 'subway', name + '.product is invalid')
}
t.ok(journey.legs.some(l => l.line), name + '.legs has no subway leg')
}
}
t.end()
}))
test('journeys  fails with no product', co(function* (t) {
try {
client.journeys(spichernstr, bismarckstr, {
when,
products: {
suburban: false,
subway: false,
tram: false,
bus: false,
ferry: false,
express: false,
regional: false
}
})
// silence rejections, we're only interested in exceptions
.catch(() => {})
} catch (err) {
t.ok(err, 'error thrown')
t.end()
}
}))
// todo: journeys  with arrival time
test('journeys  with arrival time', co(function* (t) {
const journeys = yield client.journeys(spichernstr, bismarckstr, {
results: 3,
test('journeys  fails with no product', (t) => {
journeysFailsWithNoProduct({
test: t,
fetchJourneys: client.journeys,
fromId: spichernstr,
toId: bismarckstr,
when,
whenRepresents: 'arrival'
products
})
for (let j of journeys) {
const arr = +new Date(j.arrival)
t.ok(arr <= when)
}
t.end()
}))
})
test('earlier/later journeys', co(function* (t) {
const model = yield client.journeys(spichernstr, bismarckstr, {
results: 3, when
yield testEarlierLaterJourneys({
test: t,
fetchJourneys: client.journeys,
validate,
fromId: spichernstr,
toId: bismarckstr
})
t.equal(typeof model.earlierRef, 'string')
t.ok(model.earlierRef)
t.equal(typeof model.laterRef, 'string')
t.ok(model.laterRef)
// when and earlierThan/laterThan should be mutually exclusive
t.throws(() => {
client.journeys(spichernstr, bismarckstr, {
when, earlierThan: model.earlierRef
})
})
t.throws(() => {
client.journeys(spichernstr, bismarckstr, {
when, laterThan: model.laterRef
})
})
let earliestDep = Infinity, latestDep = -Infinity
for (let j of model) {
const dep = +new Date(j.departure)
if (dep < earliestDep) earliestDep = dep
else if (dep > latestDep) latestDep = dep
}
const earlier = yield client.journeys(spichernstr, bismarckstr, {
results: 3,
// todo: single journey ref?
earlierThan: model.earlierRef
})
for (let j of earlier) {
t.ok(new Date(j.departure) < earliestDep)
}
const later = yield client.journeys(spichernstr, bismarckstr, {
results: 3,
// todo: single journey ref?
laterThan: model.laterRef
})
for (let j of later) {
t.ok(new Date(j.departure) > latestDep)
}
t.end()
}))
test('journey leg details', co(function* (t) {
test('refreshJourney', co(function* (t) {
yield testRefreshJourney({
test: t,
fetchJourneys: client.journeys,
refreshJourney: client.refreshJourney,
validate,
fromId: spichernstr,
toId: bismarckstr,
when
})
t.end()
}))
test('trip details', co(function* (t) {
const journeys = yield client.journeys(spichernstr, amrumerStr, {
results: 1, when
results: 1, departure: 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)
const trip = yield client.trip(p.id, p.line.name, {when})
validate(t, trip, 'journeyLeg', 'trip')
t.end()
}))
test('journeys  station to address', co(function* (t) {
const journeys = yield client.journeys(spichernstr, {
const torfstr = {
type: 'location',
address: 'Torfstr. 17, Berlin',
latitude: 52.541797, longitude: 13.350042
}, {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, when)
const dest = leg.destination
assertValidAddress(t, dest)
t.strictEqual(dest.address, '13353 Berlin-Wedding, Torfstr. 17')
t.ok(isRoughlyEqual(.0001, dest.latitude, 52.541797))
t.ok(isRoughlyEqual(.0001, dest.longitude, 13.350042))
assertValidWhen(t, leg.arrival, when)
address: '13353 Berlin-Wedding, Torfstr. 17',
latitude: 52.541797,
longitude: 13.350042
}
const journeys = yield client.journeys(spichernstr, torfstr, {
results: 3,
departure: when
})
yield testJourneysStationToAddress({
test: t,
journeys,
validate,
fromId: spichernstr,
to: torfstr
})
t.end()
}))
test('journeys  station to POI', co(function* (t) {
const journeys = yield client.journeys(spichernstr, {
const atze = {
type: 'location',
id: '900980720',
name: 'Berlin, Atze Musiktheater für Kinder',
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, when)
const dest = leg.destination
assertValidPoi(t, dest)
t.strictEqual(dest.id, '900980720')
t.strictEqual(dest.name, 'Berlin, Atze Musiktheater für Kinder')
t.ok(isRoughlyEqual(.0001, dest.latitude, 52.543333))
t.ok(isRoughlyEqual(.0001, dest.longitude, 13.351686))
assertValidWhen(t, leg.arrival, when)
latitude: 52.543333,
longitude: 13.351686
}
const journeys = yield client.journeys(spichernstr, atze, {
results: 3,
departure: when
})
yield testJourneysStationToPoi({
test: t,
journeys,
validate,
fromId: spichernstr,
to: atze
})
t.end()
}))
test('journeys: via works with detour', co(function* (t) {
// Going from Westhafen to Wedding via Württembergalle without detour
// is currently impossible. We check if the routing engine computes a detour.
const westhafen = '900000001201'
const wedding = '900000009104'
const württembergallee = '900000026153'
const [journey] = yield client.journeys(westhafen, wedding, {
const journeys = yield client.journeys(westhafen, wedding, {
via: württembergallee,
results: 1,
when,
passedStations: true
departure: when,
stopovers: true
})
t.ok(journey)
const l = journey.legs.some(l => l.passed && l.passed.some(p => p.station.id === württembergallee))
t.ok(l, 'Württembergalle is not being passed')
yield testJourneysWithDetour({
test: t,
journeys,
validate,
detourIds: [württembergallee]
})
t.end()
}))
test('journeys: via works without detour', co(function* (t) {
// When going from Ruhleben to Zoo via Kastanienallee, there is *no need*
// to change trains / no need for a "detour".
const ruhleben = '900000025202'
const zoo = '900000023201'
const kastanienallee = '900000020152'
const [journey] = yield client.journeys(ruhleben, zoo, {
via: kastanienallee,
results: 1,
when,
passedStations: true
})
t.ok(journey)
const l = journey.legs.some(l => l.passed && l.passed.some(p => p.station.id === kastanienallee))
t.ok(l, 'Kastanienallee is not being passed')
t.end()
}))
// todo: without detour test
test('departures', co(function* (t) {
const deps = yield client.departures(spichernstr, {duration: 5, when})
const departures = 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, when)
if (!findStation(dep.direction)) {
const err = new Error('unknown direction: ' + dep.direction)
err.stack = err.stack.split('\n').slice(0, 2).join('\n')
console.error(err)
}
assertValidLine(t, dep.line)
}
yield testDepartures({
test: t,
departures,
validate,
id: spichernstr
})
t.end()
}))
test('departures with station object', co(function* (t) {
yield client.departures({
const deps = yield client.departures({
type: 'station',
id: spichernstr,
name: 'U Spichernstr',
@ -394,7 +246,20 @@ test('departures with station object', co(function* (t) {
}
}, {when})
t.ok('did not fail')
validate(t, deps, 'departures', 'departures')
t.end()
}))
test('departures at Spichernstr. in direction of Westhafen', co(function* (t) {
yield testDeparturesInDirection({
test: t,
fetchDepartures: client.departures,
fetchTrip: client.trip,
id: spichernstr,
directionIds: [westhafen],
when,
validate
})
t.end()
}))
@ -402,13 +267,39 @@ test('departures at 7-digit station', co(function* (t) {
const eisenach = '8010097' // see derhuerst/vbb-hafas#22
yield client.departures(eisenach, {when})
t.pass('did not fail')
t.end()
}))
test('departures without related stations', co(function* (t) {
yield testDeparturesWithoutRelatedStations({
test: t,
fetchDepartures: client.departures,
id: '900000024101', // Charlottenburg
when,
products: {bus: false, suburban: false, regional: false},
linesOfRelatedStations: ['U7']
})
t.end()
}))
test('arrivals', co(function* (t) {
const arrivals = yield client.arrivals(spichernstr, {
duration: 5, when
})
yield testArrivals({
test: t,
arrivals,
validate,
id: spichernstr
})
t.end()
}))
test('nearby', co(function* (t) {
const berlinerStr = '900000044201'
const landhausstr = '900000043252'
// Berliner Str./Bundesallee
const nearby = yield client.nearby({
type: 'location',
@ -416,18 +307,14 @@ test('nearby', co(function* (t) {
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)
}
validate(t, nearby, 'locations', 'nearby')
t.equal(nearby[0].id, '900000044201')
t.equal(nearby[0].id, berlinerStr)
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].id, landhausstr)
t.equal(nearby[1].name, 'Landhausstr.')
t.ok(nearby[1].distance > 100)
t.ok(nearby[1].distance < 200)
@ -435,93 +322,38 @@ test('nearby', co(function* (t) {
t.end()
}))
test('locations', co(function* (t) {
const locations = yield client.locations('Alexanderplatz', {results: 20})
t.ok(Array.isArray(locations))
t.ok(locations.length > 0)
validate(t, locations, 'locations', 'locations')
t.ok(locations.length <= 20)
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.type === 'stop' || 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('location', co(function* (t) {
const loc = yield client.location(spichernstr)
test('station', co(function* (t) {
const s = yield client.station(spichernstr)
assertValidStation(t, loc)
t.equal(loc.id, spichernstr)
t.ok(Array.isArray(loc.lines))
if (Array.isArray(loc.lines)) {
for (let line of loc.lines) assertValidLine(t, line)
}
validate(t, s, ['stop', 'station'], 'station')
t.equal(s.id, spichernstr)
t.end()
}))
test('radar', co(function* (t) {
const vehicles = yield client.radar(52.52411, 13.41002, 52.51942, 13.41709, {
const vehicles = yield client.radar({
north: 52.52411,
west: 13.41002,
south: 52.51942,
east: 13.41709
}, {
duration: 5 * 60, when
})
t.ok(Array.isArray(vehicles))
t.ok(vehicles.length > 0)
for (let v of vehicles) {
if (!findStation(v.direction)) {
const err = new Error('unknown direction: ' + v.direction)
err.stack = err.stack.split('\n').slice(0, 2).join('\n')
console.error(err)
}
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')
}
}
validate(t, vehicles, 'movements', 'vehicles')
t.end()
}))

270
test/vbn.js Normal file
View file

@ -0,0 +1,270 @@
'use strict'
const tapePromise = require('tape-promise').default
const tape = require('tape')
const isRoughlyEqual = require('is-roughly-equal')
const {createWhen} = require('./lib/util')
const co = require('./lib/co')
const createClient = require('..')
const vbnProfile = require('../p/vbn')
const products = require('../p/vbn/products')
const createValidate = require('./lib/validate-fptf-with')
const testJourneysStationToStation = require('./lib/journeys-station-to-station')
const testJourneysStationToAddress = require('./lib/journeys-station-to-address')
const testJourneysStationToPoi = require('./lib/journeys-station-to-poi')
const testEarlierLaterJourneys = require('./lib/earlier-later-journeys')
const testRefreshJourney = require('./lib/refresh-journey')
const journeysFailsWithNoProduct = require('./lib/journeys-fails-with-no-product')
const testDepartures = require('./lib/departures')
const testArrivals = require('./lib/arrivals')
const testJourneysWithDetour = require('./lib/journeys-with-detour')
const when = createWhen('Europe/Berlin', 'de-DE')
const cfg = {
when,
stationCoordsOptional: false,
products
}
const validate = createValidate(cfg, {})
const test = tapePromise(tape)
const client = createClient(vbnProfile, 'public-transport/hafas-client:test')
const bremenHbf = '8000050'
const bremerhavenHbf = '8000051'
test.only('journeys  Bremen Hbf to Bremerhaven Hbf', co(function* (t) {
const journeys = yield client.journeys(bremenHbf, bremerhavenHbf, {
results: 3,
departure: when,
stopovers: true
})
yield testJourneysStationToStation({
test: t,
journeys,
validate,
fromId: bremenHbf,
toId: bremerhavenHbf
})
t.end()
}))
// todo: journeys, only one product
test.skip('journeys  fails with no product', (t) => {
journeysFailsWithNoProduct({
test: t,
fetchJourneys: client.journeys,
fromId: bremenHbf,
toId: bremerhavenHbf,
when,
products
})
t.end()
})
test.skip('Magdeburg Hbf to 39104 Magdeburg, Sternstr. 10', co(function*(t) {
const sternStr = {
type: 'location',
address: 'Magdeburg - Altenstadt, Sternstraße 10',
latitude: 52.118414,
longitude: 11.422332
}
const journeys = yield client.journeys(bremenHbf, sternStr, {
results: 3,
departure: when
})
yield testJourneysStationToAddress({
test: t,
journeys,
validate,
fromId: bremenHbf,
to: sternStr
})
t.end()
}))
test.skip('Magdeburg Hbf to Kloster Unser Lieben Frauen', co(function*(t) {
const kloster = {
type: 'location',
id: '970012223',
name: 'Magdeburg, Kloster Unser Lieben Frauen (Denkmal)',
latitude: 52.127601,
longitude: 11.636437
}
const journeys = yield client.journeys(bremenHbf, kloster, {
results: 3,
departure: when
})
yield testJourneysStationToPoi({
test: t,
journeys,
validate,
fromId: bremenHbf,
to: kloster
})
t.end()
}))
test.skip('journeys: via works with detour', co(function* (t) {
// Going from Magdeburg, Hasselbachplatz (Sternstr.) (Tram/Bus) to Stendal
// via Dessau without detour is currently impossible. We check if the routing
// engine computes a detour.
const journeys = yield client.journeys(hasselbachplatzSternstrasse, stendal, {
via: dessau,
results: 1,
departure: when,
stopovers: true
})
yield testJourneysWithDetour({
test: t,
journeys,
validate,
detourIds: ['8010077', dessau] // todo: trim IDs
})
t.end()
}))
// todo: without detour
test.skip('earlier/later journeys', co(function* (t) {
yield testEarlierLaterJourneys({
test: t,
fetchJourneys: client.journeys,
validate,
fromId: bremenHbf,
toId: bremerhavenHbf
})
t.end()
}))
test.skip('refreshJourney', co(function* (t) {
yield testRefreshJourney({
test: t,
fetchJourneys: client.journeys,
refreshJourney: client.refreshJourney,
validate,
fromId: bremenHbf,
toId: bremerhavenHbf,
when
})
t.end()
}))
test.skip('trip details', co(function* (t) {
const journeys = yield client.journeys(bremenHbf, bremerhavenHbf, {
results: 1, departure: when
})
const p = journeys[0].legs[0]
t.ok(p.tripId, 'precondition failed')
t.ok(p.line.name, 'precondition failed')
const trip = yield client.trip(p.tripId, p.line.name, {when})
validate(t, trip, 'journeyLeg', 'trip')
t.end()
}))
test.skip('departures at Magdeburg Leiterstr.', co(function*(t) {
const departures = yield client.departures(leiterstr, {
duration: 5, when
})
yield testDepartures({
test: t,
departures,
validate,
id: leiterstr
})
t.end()
}))
test.skip('departures with station object', co(function* (t) {
const deps = yield client.departures({
type: 'station',
id: bremenHbf,
name: 'Magdeburg Hbf',
location: {
type: 'location',
latitude: 1.23,
longitude: 2.34
}
}, {when})
validate(t, deps, 'departures', 'departures')
t.end()
}))
test.skip('arrivals at Magdeburg Leiterstr.', co(function*(t) {
const arrivals = yield client.arrivals(leiterstr, {
duration: 5, when
})
yield testArrivals({
test: t,
arrivals,
validate,
id: leiterstr
})
t.end()
}))
// todo: nearby
test.skip('locations named Magdeburg', co(function*(t) {
const locations = yield client.locations('Magdeburg', {
results: 20
})
validate(t, locations, 'locations', 'locations')
t.ok(locations.length <= 20)
t.ok(locations.find(s => s.type === 'stop' || s.type === 'station'))
t.ok(locations.find(s => s.id && s.name)) // POIs
t.ok(locations.some((loc) => {
// todo: trim IDs
if (l.station) {
if (l.station.id === '008010224' || l.station.id === bremenHbf) return true
}
return l.id === '008010224' || l.id === bremenHbf
}))
t.end()
}))
test.skip('station Magdeburg-Buckau', co(function* (t) {
const s = yield client.station(bremerhavenHbf)
validate(t, s, ['stop', 'station'], 'station')
t.equal(s.id, bremerhavenHbf)
t.end()
}))
test.skip('radar', co(function* (t) {
const vehicles = yield client.radar({
north: 52.148364,
west: 11.600826,
south: 52.108486,
east: 11.651451
}, {
duration: 5 * 60, when, results: 10
})
const customCfg = Object.assign({}, cfg, {
stationCoordsOptional: true, // see #28
})
const validate = createValidate(customCfg, {})
validate(t, vehicles, 'movements', 'vehicles')
t.end()
}))

View file

@ -5,9 +5,9 @@ const throttle = require('p-throttle')
const request = require('./lib/request')
const createClient = require('.')
const createThrottledClient = (profile, limit = 5, interval = 1000) => {
const createThrottledClient = (profile, userAgent, limit = 5, interval = 1000) => {
const throttledRequest = throttle(request, limit, interval)
return createClient(profile, throttledRequest)
return createClient(profile, userAgent, throttledRequest)
}
module.exports = createThrottledClient