mirror of
https://github.com/public-transport/db-vendo-client.git
synced 2026-02-01 22:05:34 +02:00
merge next into master
This commit is contained in:
commit
abf3661edc
100 changed files with 5025 additions and 3092 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -5,4 +5,6 @@ Thumbs.db
|
|||
node_modules
|
||||
npm-debug.log
|
||||
|
||||
package-lock.json
|
||||
|
||||
/id.json
|
||||
|
|
|
|||
3
docs/arrivals.md
Normal file
3
docs/arrivals.md
Normal 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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
151
docs/journeys.md
151
docs/journeys.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
71
docs/migrating-to-3.md
Normal 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
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
44
docs/profile-boilerplate.js
Normal file
44
docs/profile-boilerplate.js
Normal 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
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
38
docs/refresh-journey.md
Normal 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()`.
|
||||
|
|
@ -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
187
docs/trip.md
Normal 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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
37
format/products-filter.js
Normal 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
244
index.js
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
6
lib/generate-install-id.js
Executable 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')
|
||||
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
37
p/bvg/example.js
Normal 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
112
p/bvg/index.js
Normal 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
60
p/bvg/products.js
Normal 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
21
p/bvg/readme.md
Normal 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 `⟲`
|
||||
|
|
@ -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}))
|
||||
|
|
|
|||
282
p/db/index.js
282
p/db/index.js
|
|
@ -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
|
||||
|
|
|
|||
108
p/db/modes.js
108
p/db/modes.js
|
|
@ -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
85
p/db/products.js
Normal 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
|
||||
}
|
||||
]
|
||||
|
|
@ -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')
|
||||
```
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
```
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
```
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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}))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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}))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
112
p/vbb/modes.js
112
p/vbb/modes.js
|
|
@ -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
60
p/vbb/products.js
Normal 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
|
||||
}
|
||||
]
|
||||
|
|
@ -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')
|
||||
```
|
||||
|
||||
|
||||
|
|
|
|||
14
package.json
14
package.json
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
68
parse/arrival-or-departure.js
Normal file
68
parse/arrival-or-departure.js
Normal 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
10
parse/arrival.js
Normal 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
|
||||
|
|
@ -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]) {
|
||||
|
|
|
|||
|
|
@ -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
17
parse/find-remark.js
Normal 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
248
parse/hint.js
Normal 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
|
||||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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 || {}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
53
parse/polyline.js
Normal 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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
// todo: is passing in profile necessary?
|
||||
const parseRemark = (profile, r) => {
|
||||
return null // todo
|
||||
}
|
||||
|
||||
module.exports = parseRemark
|
||||
|
|
@ -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
42
parse/warning.js
Normal 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
|
||||
13
readme.md
13
readme.md
|
|
@ -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)
|
||||
|
||||
[](https://www.npmjs.com/package/hafas-client)
|
||||
[](https://travis-ci.org/public-transport/hafas-client)
|
||||
[](https://travis-ci.org/public-transport/hafas-client)
|
||||

|
||||
[](https://gitter.im/public-transport/Lobby)
|
||||
[](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
369
test/bvg.js
Normal 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()
|
||||
}))
|
||||
510
test/db.js
510
test/db.js
|
|
@ -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()
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
require('./db')
|
||||
require('./vbb')
|
||||
require('./bvg')
|
||||
require('./oebb')
|
||||
require('./insa')
|
||||
require('./nahsh')
|
||||
|
|
|
|||
481
test/insa.js
481
test/insa.js
|
|
@ -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
25
test/lib/arrivals.js
Normal 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
|
||||
|
|
@ -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 () {
|
||||
38
test/lib/departures-in-direction.js
Normal file
38
test/lib/departures-in-direction.js
Normal 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
|
||||
41
test/lib/departures-without-related-stations.js
Normal file
41
test/lib/departures-without-related-stations.js
Normal 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
25
test/lib/departures.js
Normal 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
|
||||
82
test/lib/earlier-later-journeys.js
Normal file
82
test/lib/earlier-later-journeys.js
Normal 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
|
||||
23
test/lib/journeys-fails-with-no-product.js
Normal file
23
test/lib/journeys-fails-with-no-product.js
Normal 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
|
||||
30
test/lib/journeys-station-to-address.js
Normal file
30
test/lib/journeys-station-to-address.js
Normal 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
|
||||
39
test/lib/journeys-station-to-poi.js
Normal file
39
test/lib/journeys-station-to-poi.js
Normal 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
|
||||
22
test/lib/journeys-station-to-station.js
Normal file
22
test/lib/journeys-station-to-station.js
Normal 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
|
||||
22
test/lib/journeys-with-detour.js
Normal file
22
test/lib/journeys-with-detour.js
Normal 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
|
||||
51
test/lib/refresh-journey.js
Normal file
51
test/lib/refresh-journey.js
Normal 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
28
test/lib/util.js
Normal 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
|
||||
}
|
||||
30
test/lib/validate-fptf-with.js
Normal file
30
test/lib/validate-fptf-with.js
Normal 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
364
test/lib/validators.js
Normal 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
|
||||
}
|
||||
87
test/lib/vbb-bvg-validators.js
Normal file
87
test/lib/vbb-bvg-validators.js
Normal 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
|
||||
}
|
||||
547
test/nahsh.js
547
test/nahsh.js
|
|
@ -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()
|
||||
}))
|
||||
|
|
|
|||
610
test/oebb.js
610
test/oebb.js
|
|
@ -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()
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
158
test/util.js
158
test/util.js
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
566
test/vbb.js
566
test/vbb.js
|
|
@ -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
270
test/vbn.js
Normal 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()
|
||||
}))
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue