added all files to project

This commit is contained in:
2022-03-10 10:36:59 +01:00
parent 09dd957b33
commit 46a936d7de
25351 changed files with 3883356 additions and 0 deletions
@@ -0,0 +1,3 @@
# `arrivals(station, [opt])`
Just like [`departures(station, [opt])`](departures.md), except that it gives arrival times instead of departure times.
@@ -0,0 +1,882 @@
# Changelog
## `5.21.0`
- 97b6a76e `parseTrip()`: expose `realtimeDataUpdatedAt` ✅
- 3453cbe1 `parseArrival()`/`parseDeparture()`: expose `stbStop.{rem,msg}L` as `remarks[]` as well
- 69ddf5fb/6941e7a4 BVG: parse occupancy ✅
- c270eed9 `nationalExp` -> `nationalExpress` 🐛
- 4492b3a3 use correct `HttpsAgent` option for `LOCAL_ADDRESS` environment variable 🐛
[🏷 `5.21.0`](https://github.com/public-transport/hafas-client/releases/tag/5.21.0), 2021-11-01
## `5.20.2`
- 84c7582a `journeys()`: fix empty `viaLocL[]` 🐛 (#247)
- dd5e4368 fix `departures()`/`arrivals()` without `opt.direction` 🐛
- 3f75e075 BVG: update API endpoint
- 1f6e6810 DB: update `.ext`
- 39d3807c minor tweaks 📝
- 8d4f8a83 E2E/integration tests: update fixtures ✅
[🏷 `5.20.2`](https://github.com/public-transport/hafas-client/releases/tag/5.20.2), 2021-10-26
## `5.20.1`
- 46fb44d0 SNCB: update CA chain 🐛
- f0d33564 `parseTrip()`: handle missing `stopL[]` (on-demand trips) 🐛✅
- fd6a349b `tripsByName()`: more options, add to debug CLI, skipped E2E test ✅📝
- 39ca7ede `tripsByName()`: support some journey filters
- 22a7f16e remove SBB integration/E2E test ✅
- 102c4bf2 BVG/VBB: fix `nearby()` integration/E2E test ✅
- 959e894d E2E/integration tests: un-skip tests, update mocked `when`, update fixtures ✅
[🏷 `5.20.1`](https://github.com/public-transport/hafas-client/releases/tag/5.20.1), 2021-10-24
## `5.20.0`
- 0a096a13 `parseJourneyLeg()`/`parseTrip()`: expose `currentLocation` ✅📝
- b10c1ce6 Rejseplanen `trip()` test: update fixtures ✅
- 6507d5a7 VBB `departures()` test: update fixtures ✅
- c1ee557c `parseArrivalOrDeparture()`: expose `currentTripLocation` ✅📝
[🏷 `5.20.0`](https://github.com/public-transport/hafas-client/releases/tag/5.20.0), 2021-10-18
## `5.19.1`
- 042668ff add [*DART* profile](../p/dart)
- ca75c440 adapt *VBB* profile to server changes
[🏷 `5.19.1`](https://github.com/public-transport/hafas-client/releases/tag/5.19.1), 2021-09-21
## `5.19.0`
- c10f3181 `refreshJourney()`: expose `realtimeDataFrom`
- c3bdcc88 expose `prodCtx.catOut` as `line.productName`
- 21610276 first/last walking leg: handle `dTZOffset`/`aTZOffset` of `0` 🐛
- 6de2dc7b/dd52c4ad DB: fix price parsing 🐛
- 0114f587 adapt E2E tests to latest data ✅
[🏷 `5.19.0`](https://github.com/public-transport/hafas-client/releases/tag/5.19.0), 2021-08-24
## `5.18.0`
- e293223c add [*VVV* profile](../p/vvv)
- f20466c2 add [*IVB* profile](../p/ivb)
- 0ae13b09 add [*STV* profile](../p/stv)
- 649a7ec0 add [*OÖVV* profile](../p/ooevv)
- db2cbfdc add [*VOR* profile](../p/vor)
- 56bd16b5 add [Salzburg profile](../p/salzburg)
- ce828176/aab7babb/0995696c [*DB* profile](../p/db): add [`journeysFromTrip()`](journeys-from-trips.md)
[🏷 `5.18.0`](https://github.com/public-transport/hafas-client/releases/tag/5.18.0), 2021-08-05
## `5.17.0`
- 24c2cc6e add [*BLS* profile](../p/bls)
- 33dab455 add [*TPG* profile](../p/tpg)
[🏷 `5.17.0`](https://github.com/public-transport/hafas-client/releases/tag/5.17.0), 2021-07-28
## `5.16.0`
- 7cb62108 `lib/request`: validate response `content-type` 🐛
- 96b4d55f (re-)upgrade profiles, adapt feature flags
- 62843f79 update profiles' examples 📝
- e9701648 add Rejseplanen profile to the list 📝
- 4557e336 update the "writing a profile" guide 📝
96b4d55f might break your code. Eventually I would have been forced to upgrade the HAFAS protocol version anyways though, so I included this change in the `5.16` minor release.
[🏷 `5.16.0`](https://github.com/public-transport/hafas-client/releases/tag/5.16.0), 2021-05-01
## `5.15.2`
- 7025d3bc ÖBB: fix profile by using `https:` 🐛
- ebe7c595 `lib/request`: fix `DEBUG` env var switch 🐛
- 6f56f152 DB: enable usage of 1st class BahnCard 🐛
- 68d8bf9f/9bfd4566/15be4a0b/b6ad9ba0/d9de0e00 fix readme/docs 📝
[🏷 `5.15.2`](https://github.com/public-transport/hafas-client/releases/tag/5.15.2), 2021-03-26
## `5.15.1`
- 7025d3bc ÖBB: enable `remarks()`
- cb8d92be use `LOCAL_ADDRESS` environment variable to pick network interface address
- 92f1831c `departures()`/`arrivals()`: document `opt.products` 📝
- 7025d3bc/c6fb9661 ÖBB: change `ver` to `1.33` 🐛
- 78bbf9b6 VKG, VVT `departures()`: disable `getPasslist` & `stbFltrEquiv` 🐛
[🏷 `5.15.1`](https://github.com/public-transport/hafas-client/releases/tag/5.15.1), 2021-02-12
## `5.15.0`
- 7106d24a add [*VOS* profile](../p/vos)
- 2ae6a9a4 add [*AVV* profile](../p/avv)
- f47343df add [*BART* profile](../p/bart)
- 2853fb04 add [*VVT* profile](../p/vvt)
- 0690724d add [*VKG* profile](../p/vkg)
- d69d2530 add `profile.remarksGetPolyline` flag
- 51af991e `lib/request`: add `profile.(auth|client|ext|ver)` to request
- 4ee062a1 `lib/request`: allow string `profile.salt`
- c260e34f DB: parse `gridL[].itemL[].remL[]` 🐛
- 33f398bd `parseWarning`: use `fromLocations[0]`
[🏷 `5.15.0`](https://github.com/public-transport/hafas-client/releases/tag/5.15.0), 2021-01-26
## `5.14.0`
- ad6cfd22/3407ad6b/d017e627/02af67e2 add [*mobil.nrw* profile](../p/mobil-nrw)
- 174ed807 `remarks()`: support missing `res.msgL[]` 🐛
- 4efff792 `parseJourney`: use `j.recon.ctx` as `refreshToken` too 🐛
- 86bf3b46 docs: remove "migrating to 4" guide, fix profile examples, minor tweaks 📝
[🏷 `5.14.0`](https://github.com/public-transport/hafas-client/releases/tag/5.14.0), 2021-01-19
## `5.13.0`
- 7444e08/bbf024d/6815c9e add [*SBB* (Switzerland) profile](../p/sbb)
- 17e08ac `parseJourneyLeg` → `parseAlternative`: handle missing `stopL[]` 🐛, add tests ✅
- 850ec94 *mobiliteit.lu*: fix endpoint, upgrade to version `1.25`
- 54b7d28 *mobiliteit.lu*: fix `national-train` product bitmasks 🐛
[🏷 `5.13.0`](https://github.com/public-transport/hafas-client/releases/tag/5.13.0), 2020-12-27
## `5.12.0`
- 3e6d6d9 add [`serverInfo()` method](server-info.md)
- ed48971/731d9b8 add [`remarks()` method](remarks.md)
- 9d8260b/53e10f7/1a0d97d add [`lines()` method](lines.md)
- e6bc8c6 `departures()`/`arrivals()`: add `line` option
[🏷 `5.12.0`](https://github.com/public-transport/hafas-client/releases/tag/5.12.0), 2020-12-09
## `5.11.0`
- 259fcd7 add [*VRN* (south-west Germany) profile](../p/vrn)
[🏷 `5.11.0`](https://github.com/public-transport/hafas-client/releases/tag/5.11.0), 2020-11-26
## `5.10.1`
- 6d4f29a `nearby()`: support `opt.products`
- 66ff661 `parseJourneyLeg` → `applyRemarks`: handle legs without `stopovers[]` 🐛
[🏷 `5.10.1`](https://github.com/public-transport/hafas-client/releases/tag/5.10.1), 2020-11-15
## `5.10.0`
- 013ab2d add [*mobiliteit.lu* (Luxembourg) profile](../p/mobiliteit-lu)
- 11ca3b1 add [`tripsByName()` method](trips-by-name.md)
- 92fb29d `parseTrip`: handle `stopL[]` items without `idx` 🐛
[🏷 `5.10.0`](https://github.com/public-transport/hafas-client/releases/tag/5.10.0), 2020-11-01
## `5.9.0`
- 8ed218f add [*Irish Rail* profile](../p/irish-rail)
- de86391 support HTTP proxies via `HTTPS_PROXY` & `HTTP_PROXY` environment variables
[🏷 `5.9.0`](https://github.com/public-transport/hafas-client/releases/tag/5.9.0), 2020-09-24
## `5.8.0`
- 4d06057/82de740/c17bd5a add [*Rejseplanen* profile](../p/rejseplanen)
- 9848dfa RMV: fix product bitmasks 🐛 (by [Adwirawien](https://github.com/Adwirawien))
- 25fb25c `parseLeg`: use remarks without `fIdx`/`tIdx` 🐛
- 68aaad1 *S-Bahn München*: switch to `1.21` protocol
- a621fd6 minor tweaks
- 2d139c8/c9f8cc6/b2a3ce4/e6f25a6/c17bd5a improve/update E2E & integration tests ✅
[🏷 `5.8.0`](https://github.com/public-transport/hafas-client/releases/tag/5.8.0), 2020-09-15
## `5.7.1`
- 2612494 fix platform parsing with some profiles 🐛 (by [em0lar](https://em0lar.de))
[🏷 `5.7.1`](https://github.com/public-transport/hafas-client/releases/tag/5.7.1), 2020-09-09
## `5.7.0`
- b2b1b75/3f4c05d/097557c add [*ZVV* profile](../p/zvv)
- 4fc4c3b fix `H9360` error message 🐛
[🏷 `5.7.0`](https://github.com/public-transport/hafas-client/releases/tag/5.7.0), 2020-08-01
## `5.6.3`
- 71db75d `journeys()`: expose realtime data timestamp
- d2314e0 `journeys()`: don't send `outDate`/`outTime` & `ctxScr`
- f9bfd69 `parseJourneyLeg`: parse `jny.poly` 🐛
- 51f4a66/2c04e2f `journeys()`: remove collection of results
- 6b27517 `parseMovement`: skip invalid `stopL[]` items 🐛
[🏷 `5.6.3`](https://github.com/public-transport/hafas-client/releases/tag/5.6.3), 2020-07-26
## `5.6.2`
- de896b1 `parseCommon`: respect `opt.polyline` 🐛
- fc2e214 ÖBB: add `trip()` test ✅
- dce42bf move trip parsing into `parse/trip`
[🏷 `5.6.2`](https://github.com/public-transport/hafas-client/releases/tag/5.6.2), 2020-06-13
## `5.6.1`
- 542aa8c parse `DEVI` journey legs (#175)
- 3ca4a0c/57fc610 `arrivals()`: add `provenance` field (#180)
- ee94c65 ÖBB: improve `onCall` product name
- a8a9303 `nearby()`: return at most `opt.results` results
[🏷 `5.6.1`](https://github.com/public-transport/hafas-client/releases/tag/5.6.1), 2020-06-10
## `5.6.0`
- 07c77f8/76e3102/1abafb5/d92eb15/0251e31 parse stop/station entrances & sub-stops (#153)
- 9e75f42/0251e31/322004b DB: parse *Reisezentrum* opening hours & station facilities (#153)
[🏷 `5.6.0`](https://github.com/public-transport/hafas-client/releases/tag/5.6.0), 2020-05-21
## `5.5.1`
- 3c888a0 `refreshJourney()`: actually throw the error 🐛, add error code
- e02a20b readme: update links 📝
- b302ba7 minor readme/documentation tweaks 📝
[🏷 `5.5.1`](https://github.com/public-transport/hafas-client/releases/tag/5.5.1), 2020-05-21
## `5.5.0`
- fa3146d/9c4189a add [*SVV* profile](../p/svv)
- e032ec1 "invalid response" error: add `isHafasError: true` flag
- 0699d4d `departures()`/`arrivals()`: let `results` option default to `null`
- 1b01331 use `object-scan@13` ⚡️
[🏷 `5.5.0`](https://github.com/public-transport/hafas-client/releases/tag/5.5.0), 2020-04-09
## `5.4.0`
- 01b3693/17031f3/7d3107e add [*SNCB*/*NMBS* profile](../p/sncb)
- ae74bb4 `departures()`/`arrivals()`: add `results` option
[🏷 `5.4.0`](https://github.com/public-transport/hafas-client/releases/tag/5.4.0), 2020-03-29
## `5.3.1`
- 916ac30 PKP: trim `-` from stop names
- a939090 INSA: `ver` `1.21` -> `1.18` 🐛
- 2cb6a0c `parseIcon()`, `parseHint()`, `parseLocation()`: handle more edge cases 🐛
- 0dceb41 `parseJourneyLeg()`: parse isRchbl correctly 🐛
- 78487d9 `journeys()`: default `earlierRef` & `laterRef` to `null` 🐛
- cda96b6 improve docs 📝
[🏷 `5.3.1`](https://github.com/public-transport/hafas-client/releases/tag/5.3.1), 2020-03-18
## `5.3.0`
- 1c790e1/299b5ac add [*INVG* profile](../p/invg)
- d5116c2/c2b15fa add [*PKP* profile](../p/pkp)
- 682f9f9/8540f5f add [*VBN* profile](../p/vbn)
- 3a9e548/0ea2c01 add [*RMV* profile](../p/rmv)
- 84637b2/522248b add [*RSAG* profile](../p/rsag)
- 86ddf2c add [*VMT* profile](../p/vmt)
[🏷 `5.3.0`](https://github.com/public-transport/hafas-client/releases/tag/5.3.0), 2020-03-12
## `5.2.0`
- 1b03b2e INSA: protocol version `1.21`, enable [`reachableFrom()`](reachable-from.md)
- 2a24137/3ea9380 `parseLocation()`: parse foreign stop IDs
- 3ea9380 `parseLocation()`: parse fare zone, transit authority
- 8c7f164 `parseLine()`: expose admin code
- b9d5c85 add DB & INSA `stop()` tests
[🏷 `5.2.0`](https://github.com/public-transport/hafas-client/releases/tag/5.2.0), 2020-03-08
## `5.1.2`
- e5abe3d DB: fix journey leg loadFactor parsing 🐛
- bc30309 fix undefined variables 🐛
- db94a62/c072a70/df010fc/9874292 add linting
[🏷 `5.1.2`](https://github.com/public-transport/hafas-client/releases/tag/5.1.2), 2020-03-02
## `5.1.1`
- 8cb7d80 improve `findInTree` performance (#152) ⚡️
- 940519b make readme more helpful 📝
- 9522e92 `object-scan@11`
[🏷 `5.1.1`](https://github.com/public-transport/hafas-client/releases/tag/5.1.1), 2020-02-22
## `5.1.0`
- 542a9ee/1c67350/738354d add [*VSN* profile](../p/vsn)
- dfff999 `request()`: add resonse ID to error objects
- c1beb28 `Error` -> `TypeError`
[🏷 `5.1.0`](https://github.com/public-transport/hafas-client/releases/tag/5.1.0), 2020-02-08
## `5.0.4`
- db9287f [`@mapbox/polyline`](https://www.npmjs.com/package/@mapbox/polyline) -> [`google-polyline`](https://www.npmjs.com/package/google-polyline)
- 9b0e55c VBB: accept station IDs with an unknown length 🐛
- ea4912a debug CLI: accept JS objects
[🏷 `5.0.4`](https://github.com/public-transport/hafas-client/releases/tag/5.0.4), 2020-02-03
## `5.0.3`
- 8c6a8d8 `findInTree`: improved performance ⚡️
- c080f32 `vbb-translate-ids@4` 🐛
[🏷 `5.0.3`](https://github.com/public-transport/hafas-client/releases/tag/5.0.3), 2020-01-29
## `5.0.2`
- e049aa3 `parseWarning()`: fix `parseMsgEvent()` 🐛
[🏷 `5.0.2`](https://github.com/public-transport/hafas-client/releases/tag/5.0.2), 2020-01-15
## `5.0.1`
- 51b1e68 `throttle.js`, `retry.js`: use default profile 🐛
[🏷 `5.0.1`](https://github.com/public-transport/hafas-client/releases/tag/5.0.1), 2020-01-15
## `5.0.0`
Note that this version is not backwords-compatible with `4.*`. Check out [the migration guide](migrating-to-5.md).
### breaking changes 💥
- 2f8f82f require Node `>=10`
- 29a2cf3/2b9280e add `plannedArrival`/`plannedDeparture`/`plannedWhen`, `scheduled*` -> `planned*`/`prognosed*`
- 938a6f2/2d1d482 add `plannedArrivalPlatform`/`plannedDeparturePlatform`/`plannedPlatform`, `scheduled*` -> `planned*`/`prognosed*`
- 35e44d4 `parseWarning()`/`parseHint()`: change signature to `(profile, raw, data) => …`
- 4162328 `createClient()`: change signature to `(profile, userAgent, opt = {}) => …`
- fb7a565/252ce5b/9fc6664/2cfee22/e2567ef change parse fns signature to `({profile, opt, res, common}) => (rawData) => …`
- baff692 `journeys()`: don't request nr of results by default
- b8496be DB `journeys()`: let `journey.price` default to `null`
- 6d5c608 call `request()` via `profile`
### features
- f8210c5/9c47a39/0c145d3/9a89cd0 `journeys()`: add `walkingSpeed` option
- a40006f/1afe4ca BVG: support *BerlKönig*, add E2E test
- 352fa2e parse more warning fields
- 8b2a5a8 `parseIcon()`: use `.txt` & `.txtS` as text fallback
- 39a6267 request formatters (e.g. `formatTripReq()`) via `profile`
### bugfixes 🐛
- 5ea22f7 `parseHint()`: parse `.code` & `.text` properly
- 29d7bd4 `parseJourney()`: fix `journey.scheduledDays` year
- 9a6bc2d `parseWarning()`: call `parseDateTime()` via `profile`
- 7b7293e `request()`: use *transformed* `req`
[🏷 `5.0.0`](https://github.com/public-transport/hafas-client/releases/tag/5.0.0), 2020-01-05
## `4.8.0`
- 56dee66/46eadcf/1611635 add [*DB Busradar NRW* profile](../p/db-busradar-nrw)
[🏷 `4.8.0`](https://github.com/public-transport/hafas-client/releases/tag/4.8.0), 2019-12-29
## `4.7.0`
- fceaf86 parse `Q` hints
- c883d96 documentation for `mgate.exe` endpoints
[🏷 `4.7.0`](https://github.com/public-transport/hafas-client/releases/tag/4.7.0), 2019-12-26
## `4.6.2`
- 105c18b DB: always use `rtMode: HYBRID`
[🏷 `4.6.2`](https://github.com/public-transport/hafas-client/releases/tag/4.6.2), 2019-11-18
## `4.6.1`
- 43b4a6e handle `H_UNKNOWN` error
- 1cc453b parseArrOrDep, parseLocation: bugfixes 🐛
[🏷 `4.6.1`](https://github.com/public-transport/hafas-client/releases/tag/4.6.1), 2019-10-28
## `3.10.3`
- c9ceeca put deprecation note
[🏷 `3.10.3`](https://github.com/public-transport/hafas-client/releases/tag/3.10.3), 2019-10-28
## `2.10.4`
- 096f8a0 put deprecation note
[🏷 `2.10.4`](https://github.com/public-transport/hafas-client/releases/tag/2.10.4), 2019-10-28
## `4.6.0`
- 73ca349/19c3ee6 NVV profile
[🏷 `4.6.0`](https://github.com/public-transport/hafas-client/releases/tag/4.6.0), 2019-08-16
## `4.5.2`
- 2e88e96 install-unique client ID via `postinstall` step -> generate process-unique ID
[🏷 `4.5.2`](https://github.com/public-transport/hafas-client/releases/tag/4.5.2), 2019-08-16
## `3.10.2`
- 1babfbf `parseWarning`: handle missing summary/text 🐛
[🏷 `3.10.2`](https://github.com/public-transport/hafas-client/releases/tag/3.10.2), 2019-08-12
## `4.5.1`
- bd7d5bb `parseWarning`: handle missing `summary`/`text` 🐛
- 92c842b DB: enable `radar()`
[🏷 `4.5.1`](https://github.com/public-transport/hafas-client/releases/tag/4.5.1), 2019-07-20
## `4.5.0`
- b144dd5/b57c212 return nice error messages & error codes
[🏷 `4.5.0`](https://github.com/public-transport/hafas-client/releases/tag/4.5.0), 2019-07-08
## `4.4.0`
- e46d6cd `parseLocation`: expose `stop.isMeta`
[🏷 `4.4.0`](https://github.com/public-transport/hafas-client/releases/tag/4.4.0), 2019-06-30
## `4.3.0`
- 1e0182f `parseLint`: use `addName`
- d0f7ca1 follow HTTP redirects, accept `br` encoding
[🏷 `4.3.0`](https://github.com/public-transport/hafas-client/releases/tag/4.3.0), 2019-06-25
## `4.2.2`
- 64f797e `parseProductsBitmask`: fix bitmask handling 🐛
- 707fd29 `p-retry@4`, `p-throttle@3`
[🏷 `4.2.2`](https://github.com/public-transport/hafas-client/releases/tag/4.2.2), 2019-06-25
## `4.2.1`
- 9078d2d fix `leg.reachable`, which was breaking all walking legs 🐛
[🏷 `4.2.1`](https://github.com/public-transport/hafas-client/releases/tag/4.2.1), 2019-06-08
## `4.2.0`
- 6da1e80 add `leg.reachable`
[🏷 `4.2.0`](https://github.com/public-transport/hafas-client/releases/tag/4.2.0), 2019-06-07
## `4.1.1`
- 875ea18 parse scheduled/actual platform information on legs, fixes #116 🐛
- f92e933 [DB](../p/db) departures/arrivals: parse load factor #112
[🏷 `4.1.1`](https://github.com/public-transport/hafas-client/releases/tag/4.1.1), 2019-05-29
## `4.1.0`
- 831bcaf ISO date+time: suppress milliseconds if 0
- 3e01303/75432fc CFG profile
- 820f2ab `parseWarning`: parse products
- 3ab099b/57c7186 HVV profile
[🏷 `4.1.0`](https://github.com/public-transport/hafas-client/releases/tag/4.1.0), 2019-05-27
## `4.0.3`
- 6aa57d4 `parseJourneyLeg`/`parseMovement`/`parseArrival`/`parseDeparture`: handle missing `dirTxt` 🐛
[🏷 `4.0.3`](https://github.com/public-transport/hafas-client/releases/tag/4.0.3), 2019-04-01
## `4.0.2`
- 133cee9 `parseWarning`: expose `warning.id` 🐛
[🏷 `4.0.2`](https://github.com/public-transport/hafas-client/releases/tag/4.0.2), 2019-03-27
## `4.0.1`
- 5d49fd0 `parseDateTime`: fix `tzOffset` & `daysOffset` 🐛
[🏷 `4.0.1`](https://github.com/public-transport/hafas-client/releases/tag/4.0.1), 2019-03-19
## `4.0.0`
This version is not fully backwords-compatible. Check out [the migration guide](migrating-to-4.md).
### breaking changes 💥
- 1e13cf1/b99ceb2 `parseLocation`: strip leading zeros from IDs
- a9fd9ff `parseDateTime`: return ISO string/timestamp
- ca1105f `parseDateTime`: parse timezone offset if given
- bf3c4c5 require Node `>=8.3.0`
- bbff1f4 `movement.nextStops` -> `movement.nextStopovers`
- bad0af8/8b87868/2e12206 rename `station(id)` -> `stop(id)`
- 96ff59d/0daa1c5/88c78c2 `leg.id` -> `leg.tripId`
- 3bc2eff `locations()`: default `opt.results` to `5`
- a1ffad3/cb535cd `parseLine`: remove `line.class` & `line.productCode`
- fcc53b5/b2b2d11/a1c40ad `journeys()`: return object with `journeys`, `earlierRef`, `laterRef`
- 61e7d14 `journeys()`: default `opt.transfers` to `-1`
- d7e439b debugging: `NODE_DEBUG` -> `DEBUG`
- 8f9b22e `locations()`, `nearby()`: `opt.stations` -> `opt.stops`
- a972dad `departures()`/`arrivals()`, `locations()`, `nearby()`, `stop()`: `opt.stationLines` -> `opt.linesOfStops`
- 0e1fcb0/0e1fcb0 `leg.mode: 'walking'` -> `leg.walking: true`
- 567cc98 DB, INSA, Nah.SH, ÖBB: `nationalExp` -> `nationalExpress`
- 9c44995 remove `arrival.trip`/`departure.trip` & `movement.trip`
- eb3ffba/eab850e mark POIs objects with `poi: true`
- 748f8ce `createThrottledClient` -> `withThrottling`
- fbde6a1 `createClientWithRetry` -> `withRetrying`
- 1646173 throw `Error`s -> `TypeError`s
- 7e39a2f/3b0740d `formerScheduled…` -> `scheduled…`
### bugfixes 🐛
- fcc2a23 ÖBB `journeys()`: fix `opt.results`
[🏷 `4.0.0`](https://github.com/public-transport/hafas-client/releases/tag/4.0.0), 2019-02-28
## `3.10.1`
- dafc96a update CMTA credentials
- 46e7729 remove `console.error` call 🐛
[🏷 `3.10.1`](https://github.com/public-transport/hafas-client/releases/tag/3.10.1), 2019-02-28
## `3.10.0`
- d797333/1e16a10 [DB](../p/db): parse additional line names
[🏷 `3.10.0`](https://github.com/public-transport/hafas-client/releases/tag/3.10.0), 2019-02-13
## `3.9.1`
- a145fea extend default retrying options 🐛
[🏷 `3.9.1`](https://github.com/public-transport/hafas-client/releases/tag/3.9.1), 2019-02-08
## `3.9.0`
- b0f786c support for retrying failed requests ✨ – [docs](readme.md#retrying-failed-requests)
[🏷 `3.9.0`](https://github.com/public-transport/hafas-client/releases/tag/3.9.0), 2019-02-08
## `3.8.1`
- 3f58d84 handle `stop` objects as input 🐛
[🏷 `3.8.1`](https://github.com/public-transport/hafas-client/releases/tag/3.8.1), 2019-02-06
## `3.8.0`
- 5d0096c `departures()`: profile flag for `getPasslist` & `stbFilterEquiv`
- #99 [Saarfahrplan profile](p/saarfahrplan) – Thanks @ialokim & @juliuste!
[🏷 `3.8.0`](https://github.com/public-transport/hafas-client/releases/tag/3.8.0), 2018-12-31
## `3.7.0`
- e867dac/f097022 `opt.stopovers`, `departure.nextStopovers`/`arrival.previousStopovers`
[🏷 `3.7.0`](https://github.com/public-transport/hafas-client/releases/tag/3.7.0), 2018-12-28
## `3.6.3`
- cb2d298 `stop`s/`station`s: default `id` of `null` 🐛
[🏷 `3.6.3`](https://github.com/public-transport/hafas-client/releases/tag/3.6.3), 2018-12-28
## `3.6.2`
- 5beff47 `radar()`: fix `polylines` option 🐛
- 48424cf `p-throttle` as normal dependency 🐛
[🏷 `3.6.2`](https://github.com/public-transport/hafas-client/releases/tag/3.6.2), 2018-12-16
## `3.6.1`
- b809281 fix error parsing 🐛
- bcbc366/ae2007c/e1f1d0d ÖBB `radar()`: fix filtering of `movement.nextStops` 🐛
[🏷 `3.6.1`](https://github.com/public-transport/hafas-client/releases/tag/3.6.1), 2018-12-10
## `3.6.0`
- 4b56f66 parse `journey.cycle` if returned by HAFAS
- 17b8f14 `journeyLeg.cycle`: parse `nr` field if returned by HAFAS
- 8fac5fc `journeyLeg.alternatives`: parse `direction`, `delay`, `tripId`
[🏷 `3.6.0`](https://github.com/public-transport/hafas-client/releases/tag/3.6.0), 2018-12-03
## `3.5.0`
- 9d96902 `readableFrom()`: make `opt.maxDuration` optional
- 02e0e51 parse scheduled days of a `journey`
[🏷 `3.5.0`](https://github.com/public-transport/hafas-client/releases/tag/3.5.0), 2018-11-13
## `3.4.3`
- 9936466 `p-throttle@2`, `tape-promise@4`
[🏷 `3.4.3`](https://github.com/public-transport/hafas-client/releases/tag/3.4.3), 2018-10-24
## `3.4.2`
- 2a6b0dc speed up date+time formatting ⚡️
[🏷 `3.4.2`](https://github.com/public-transport/hafas-client/releases/tag/3.4.2), 2018-09-24
## `3.4.1`
- 582c9de speed up date+time parsing ⚡️
[🏷 `3.4.1`](https://github.com/public-transport/hafas-client/releases/tag/3.4.1), 2018-09-22
## `3.4.0`
- #81 [S-Bahn München profile](p/sbahn-muenchen) – Thanks @flori-uni!
[🏷 `3.4.0`](https://github.com/public-transport/hafas-client/releases/tag/3.4.0), 2018-09-20
## `3.3.1`
- 035877c `reachableFrom()` retry 🐛
[🏷 `3.3.1`](https://github.com/public-transport/hafas-client/releases/tag/3.3.1), 2018-09-03
## `3.3.0`
- #80/b36ccda `reachableFrom()` method – [docs](reachable-from.md)
[🏷 `3.3.0`](https://github.com/public-transport/hafas-client/releases/tag/3.3.0), 2018-09-03
## `3.2.1`
- 044a5ee `arrivals()`: return a `direction` of `null` :bug:
- b37bedb parse `line.id` if possible
[🏷 `3.2.1`](https://github.com/public-transport/hafas-client/releases/tag/3.2.1), 2018-09-03
## `3.2.0`
- #79 [CapMetro/CMTA profile](p/cmta) – Thanks @nickturskyi!
[🏷 `3.2.0`](https://github.com/public-transport/hafas-client/releases/tag/3.2.0), 2018-08-26
## `3.1.2`
- f796337 handle warnings without schedule `sDate`/`eDate`/`lModDate` 🐛
[🏷 `3.1.2`](https://github.com/public-transport/hafas-client/releases/tag/3.1.2), 2018-08-24
## `3.1.1`
- 39cc2f3 fix install on Windows 🐛
[🏷 `3.1.1`](https://github.com/public-transport/hafas-client/releases/tag/3.1.1), 2018-08-23
## `3.1.0`
- 9257d3a parse `line.fahrtNr`
[🏷 `3.1.0`](https://github.com/public-transport/hafas-client/releases/tag/3.1.0), 2018-08-22
## `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 🐛
[🏷 `3.0.0`](https://github.com/public-transport/hafas-client/releases/tag/3.0.0), 2018-08-17
## `2.10.3`
- 50bd440 better `User-Agent` randomization
[🏷 `2.10.3`](https://github.com/public-transport/hafas-client/releases/tag/2.10.3), 2018-08-08
## `2.10.2`
- d54c26d randomize `User-Agent`
[🏷 `2.10.2`](https://github.com/public-transport/hafas-client/releases/tag/2.10.2), 2018-07-25
## `2.10.1`
- 04d550f parse `TRSF` legs as `walking` 🐛
[🏷 `2.10.1`](https://github.com/public-transport/hafas-client/releases/tag/2.10.1), 2018-07-02
## `2.10.0`
- 4da8689 journey legs with `type: 'walking'` now have a `distance` in meters
- c1bdade `departures()`: parse & expose platforms
- fccd3d0 `journeys()`: `startWithWalking` option
[🏷 `2.10.0`](https://github.com/public-transport/hafas-client/releases/tag/2.10.0), 2018-06-30
## `2.9.1`
- a952b08 notes on how to use `hafas-client` with react-native 📝
- 38a3749 `parseStopover`: fix first/last canceled stopovers 🐛
## `2.9.0`
- 49186ae journey leg passed stations: add `arrivalDelay` & `departureDelay`
- deb8829 [`journeys()`](journeys.md): new `whenRepresents` option
- f3d8304 let the `insa` and `nahsh` profiles use HTTPS
[🏷 `2.9.0`](https://github.com/public-transport/hafas-client/releases/tag/2.9.0), 2018-06-20
## `2.8.1`
- 769f2e3 send `Accept: application/json`
[🏷 `2.8.1`](https://github.com/public-transport/hafas-client/releases/tag/2.8.1), 2018-06-07
## `2.8.0`
- 16c3f01 enable [`journeyLeg()`](journey-leg.md) for [DB](../p/db)
[🏷 `2.8.0`](https://github.com/public-transport/hafas-client/releases/tag/2.8.0), 2018-05-24
## `2.7.5`
- 908d531 [DB](../p/db) [`journeys()`](journeys.md): fix polylines parsing 🐛
[🏷 `2.7.5`](https://github.com/public-transport/hafas-client/releases/tag/2.7.5), 2018-05-24
## `2.7.4`
- 709b7b4 update dependencies
[🏷 `2.7.4`](https://github.com/public-transport/hafas-client/releases/tag/2.7.4), 2018-05-24
## `2.7.3`
- 48f2cef each movement from `radar()` now has a `journeyId` field
[🏷 `2.7.3`](https://github.com/public-transport/hafas-client/releases/tag/2.7.3), 2018-05-21
## `2.7.2`
- a97e0d3 fix polylines parsing 🐛
[🏷 `2.7.2`](https://github.com/public-transport/hafas-client/releases/tag/2.7.2), 2018-05-16
## `2.7.1`
- aa480e0 fix polylines parsing 🐛
[🏷 `2.7.1`](https://github.com/public-transport/hafas-client/releases/tag/2.7.1), 2018-05-16
## `2.7.0`
- `journeys()`: `polylines` option
- `journeyLeg()`: `polyline` option
- `radar()`: `polylines` option
[🏷 `2.7.0`](https://github.com/public-transport/hafas-client/releases/tag/2.7.0), 2018-05-15
## `2.6.0`
- 5d10d76 journey legs: parse cycle
[🏷 `2.6.0`](https://github.com/public-transport/hafas-client/releases/tag/2.6.0), 2018-04-29
## `2.5.3`
- d676b84 fix parsing for journey leg alternatives 🐛
[🏷 `2.5.3`](https://github.com/public-transport/hafas-client/releases/tag/2.5.3), 2018-04-29
## `2.5.2`
- 16e6dd6 departure docs: fix method 📝
- c60213a DB: tram mode should be `train` 🐛
[🏷 `2.5.2`](https://github.com/public-transport/hafas-client/releases/tag/2.5.2), 2018-04-24
## `2.5.1`
- afc0124 fix stopover parsing 🐛
[🏷 `2.5.1`](https://github.com/public-transport/hafas-client/releases/tag/2.5.1), 2018-04-05
## `2.5.0`
- new [Schleswig-Holstein (NAH.SH)](https://de.wikipedia.org/wiki/Nahverkehrsverbund_Schleswig-Holstein) [profile](../p/nahsh)
- new [*writing a profile* guide](./writing-a-profile.md)
[🏷 `2.5.0`](https://github.com/public-transport/hafas-client/releases/tag/2.5.0), 2018-03-18
## `2.4.2`
- `parseStopover`: expose canceled arrivals & departures 🐛
[🏷 `2.4.2`](https://github.com/public-transport/hafas-client/releases/tag/2.4.2), 2018-03-17
## `2.4.1`
- new [*writing a profile* guide](./writing-a-profile.md)
- `parseMovement`: use `parseStopover` 🐛
- `parseStopover`: use `parseStationName` 🐛
[🏷 `2.4.1`](https://github.com/public-transport/hafas-client/releases/tag/2.4.1), 2018-03-17
## `2.4.0`
- new [Nahverkehr Sachsen-Anhalt (NASA)](https://de.wikipedia.org/wiki/Nahverkehrsservice_Sachsen-Anhalt)/[INSA](https://insa.de) profile
- new `earlierRef`/`laterRef` feature to query earlier/later journeys (pagination)
- former scheduled date & time for canceled departures & journeys
[🏷 `2.4.0`](https://github.com/public-transport/hafas-client/releases/tag/2.4.0), 2018-03-14
@@ -0,0 +1,198 @@
# `departures(station, [opt])`
`station` must be in one of these formats:
```js
// a station ID, in a format compatible to the profile you use
'900000013102'
// an FPTF `station` object
{
type: 'station',
id: '900000013102',
name: 'foo station',
location: {
type: 'location',
latitude: 1.23,
longitude: 3.21
}
}
```
With `opt`, you can override the default options, which look like this:
```js
{
when: new Date(),
direction: null, // only show departures heading to this station
line: null, // filter by line ID
duration: 10, // show departures for the next n minutes
results: null, // max. number of results; `null` means "whatever HAFAS wants"
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
linesOfStops: false, // parse & expose lines at the stop/station?
remarks: true, // parse & expose hints & warnings?
stopovers: false, // fetch & parse previous/next stopovers?
// departures at related stations
// e.g. those that belong together on the metro map.
includeRelatedStations: true,
language: 'en' // language to get results in
}
```
If you pass an object `opt.products`, its fields will partially override the default products defined in the profile. An example with the [BVG profile](../p/bvg):
```js
const createClient = require('hafas-client')
const vbbProfile = require('hafas-client/p/vbb')
const client = createClient(vbbProfile, 'my-awesome-program')
// will query with these products: suburban, subway, bus, express, regional
client.departures('900000024101', {products: {tram: false, ferry: false}})
```
## Response
*Note:* As stated in the [*Friendly Public Transport Format* v2 draft spec](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md), the `when` field includes the current delay. The `delay` field, if present, expresses how much the former differs from the schedule.
You may pass 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):
```js
const createClient = require('hafas-client')
const vbbProfile = require('hafas-client/p/vbb')
const client = createClient(vbbProfile, 'my-awesome-program')
// S Charlottenburg
client.departures('900000024101', {duration: 3})
.then(console.log)
.catch(console.error)
```
The response may look like this:
```js
[ {
tripId: '1|31431|28|86|17122017',
trip: 31431,
direction: 'S Spandau',
line: {
type: 'line',
id: '18299',
fahrtNr: '12345',
mode: 'train',
product: 'suburban',
public: true,
name: 'S9',
symbol: 'S',
nr: 9,
metro: false,
express: false,
night: false,
operator: {
type: 'operator',
id: 's-bahn-berlin-gmbh',
name: 'S-Bahn Berlin GmbH'
}
},
currentTripPosition: {
type: 'location',
latitude: 52.500851,
longitude: 13.283755,
},
stop: {
type: 'station',
id: '900000024101',
name: 'S Charlottenburg',
location: {
type: 'location',
latitude: 52.504806,
longitude: 13.303846
},
products: {
suburban: true,
subway: false,
tram: false,
bus: true,
ferry: false,
express: false,
regional: true
}
},
when: '2017-12-17T19:32:00+01:00',
plannedWhen: '2017-12-17T19:32:00+01:00',
delay: null,
platform: '2',
plannedPlatform: '2'
}, {
cancelled: true,
tripId: '1|30977|8|86|17122017',
trip: 30977,
direction: 'S Westkreuz',
line: {
type: 'line',
id: '16441',
fahrtNr: '54321',
mode: 'train',
product: 'suburban',
public: true,
name: 'S5',
symbol: 'S',
nr: 5,
metro: false,
express: false,
night: false,
operator: { /* … */ }
},
currentTripPosition: {
type: 'location',
latitude: 52.505004,
longitude: 13.322391,
},
stop: { /* … */ },
when: null,
plannedWhen: '2017-12-17T19:33:00+01:00'
delay: null,
platform: null,
plannedPlatform: '2',
prognosedPlatform: '2'
}, {
tripId: '1|28671|4|86|17122017',
trip: 28671,
direction: 'U Rudow',
line: {
type: 'line',
id: '19494',
fahrtNr: '11111',
mode: 'train',
product: 'subway',
public: true,
name: 'U7',
symbol: 'U',
nr: 7,
metro: false,
express: false,
night: false,
operator: { /* … */ }
},
currentTripPosition: {
type: 'location',
latitude: 52.49864,
longitude: 13.307622,
},
stop: { /* … */ },
when: '2017-12-17T19:35:00+01:00',
plannedWhen: '2017-12-17T19:35:00+01:00',
delay: 0,
platform: null,
plannedPlatform: null
} ]
```
@@ -0,0 +1,160 @@
# HAFAS `mgate.exe` protocol
The protocol of `mgate.exe` HAFAS endpoints is not openly (and freely) documented. The following documentation is based on general observations and reverse-engineering.
*Note:* There are also `rest.exe` (a.k.a. "open API", a.k.a. "REST API") endpoints. This documentation is *not* about them.
## date & time format
Dates are encoded as `YYYYMMDD`, time strings as `HHMMSS`. These are in the timezone configured on the HAFAS/server side, *per endpoint*.
Whenever HAFAS returns a time string that exceeds the day the response describes, it will add a "day offset". As an example, when you query departures at `2019-12-12T23:50+01:00` for the next 30 minutes, it will encode the departure at `2019-12-13T00:13+01:00` as `20191212` & `01001300`.
For working code, check out [`parseDateTime()`](../parse/date-time.js).
## coordinate format
All endpoints I've seen so far use [WGS84](http://wiki.gis.com/wiki/index.php/WGS84). Values are multiplied by `10^6` though, so you would encode `{latitude: 1.23, longitude: -2.34}` as `{Y: 1230000: X: -2340000}`. There's an optional parameter `z` with the elevation.
For working code, check out [`formatAddress()`](../format/address.js).
## querying the API
In many aspects, the API looks and feels like [RPCs](https://en.wikipedia.org/wiki/Remote_procedure_call). You must send queries via HTTP `POST`, with the minimal JSON body looking like this:
```js
{
"auth": {
"type": "AID",
"aid": "…" // endpoint-specific authentication token, e.g. `1Rxs112shyHLatUX4fofnmdxK`
},
"ver": "…", // endpoint-specific string, e.g. `1.15`
"ext": "…", // endpoint-specific string, e.g. `BVG.1`
"client": {
"type": "IPA", // might also be `IPH` for "iPhone" or `WEB` for "web client"
"id": "…", // endpoint-specific string, e.g. `BVG`
"name": "…", // endpoint-specific string, e.g. `FahrInfo`
"v": "…" // endpoint-specific string, e.g. `4070700`
},
"lang": "…", // language, sometimes 2-digit (e.g. `de`), sometimes 3-digit (e.g. `deu`)
"svcReqL": [
{
"meth": "…", // name of the API call, supported values depend on the endpoint
"req": {
// actual request parameters…
}
// some endpoints also require this:
"cfg": {
"cfgGrpL": [],
"cfgHash": "…" // endpoint-specific string
}
}
]
}
```
- The data in `client` must be correct, otherwise HAFAS will reject your request.
- HAFAS will return slightly different response formats (and slightly different levels of detail) for different `ver`, `ext` and `client.v` values.
- All endpoints known support JSON & UTF-8, so make sure to send `Accept: application/json` & `Accept-Charset: utf-8` headers.
- Most endpoints support at least GZIP compression, so make sure to send a `Accept-Encoding: gzip` header.
For working code, check out [`request()`](lib/request.js).
## Authentication
There are three known types of authentication used among `mgate.exe` endpoints.
For working code, check out [`hafas-client`'s `request()`](lib/request.js), [`public-transport-enabler`'s Java implementation](https://github.com/schildbach/public-transport-enabler/blob/69614c87af627e2feafc576882f2ccccdbf4b7e6/src/de/schildbach/pte/AbstractHafasClientInterfaceProvider.java#L845-L860), [`TripKit`'s Swift implementation](https://github.com/alexander-albers/tripkit/blob/724b6cd8c258c9c61e7443c81e914618b79393cb/TripKit/AbstractHafasClientInterfaceProvider.swift#L1473-L1495) or [`marudor.de`'s TypeScript implementation](https://github.com/marudor/BahnhofsAbfahrten/blob/cf64d53c6902981ec529d3952253b2c83bff9221/src/server/HAFAS/profiles.ts#L30-L54).
### unprotected endpoints
You can just query these, as long as you send a formally correct request.
### endpoints using the `checksum` query parameter
`checksum` is a [message authentication code](https://en.wikipedia.org/wiki/Message_authentication_code): You can compute it by [hashing](https://en.wikipedia.org/wiki/Hash_function) the request body and a secret *salt*.
This secret can be read from the config file inside the accompanying client app. There is no guide for this yet, so please [open an issue](https://github.com/public-transport/hafas-client/issues/new).
### endpoints using the `mic` & `mac` query parameters
`mic` is a [message integrity code](https://en.wikipedia.org/wiki/Message_authentication_code), the [hash](https://en.wikipedia.org/wiki/Hash_function) of the request body.
`mac` is a [message authentication code](https://en.wikipedia.org/wiki/Message_authentication_code), the hash of `mic` and a secret *salt*.
This secret can be read from the config file inside the accompanying client app. There is no guide for this yet, so please [open an issue](https://github.com/public-transport/hafas-client/issues/new).
## API responses
A minimal valid response looks like this:
```js
{
"ver": "…", // endpoint-specific string, e.g. `1.15`
"lang": "…", // language
"ext": "…", // endpoint-specific string, e.g. `BVG.1`
"id": "…", // unique ID for each response?
"svcResL": [
{
"meth": "StationBoard",
"err": "OK",
"res": {
// result of the API call
}
}
]
}
```
For working code, check out [`request()`](lib/request.js).
### parse error
todo: generic server error
```js
{
"ver": "…", // endpoint-specific string, e.g. `1.15`
"lang": "…", // language, sometimes 2-digit (e.g. `de`), sometimes 3-digit (e.g. `deu`)
"err": "PARSE", // error code
"errTxt": "…", // error message, not always present
"svcResL": []
}
```
### authentication error
```js
{
"ver": "…", // endpoint-specific string, e.g. `1.15`
"lang": "…", // language
"ext": "…", // endpoint-specific string, e.g. `BVG.1`
"err": "AUTH", // error code
"errTxt": "…", // error message, not always present
"svcResL": []
}
```
### API-call-specific error
```js
{
"ver": "…", // endpoint-specific string, e.g. `1.15`
"lang": "…", // language
"ext": "…", // endpoint-specific string, e.g. `BVG.1`
"svcResL": [
{
"meth": "StationBoard",
"err": "…", // error code, e.g. `H9300`
"errTxt": "…", // error message, e.g. `Unknown arrival station`
"res": {}
}
]
}
```
## more ressources
- [@Nakaner's `strecken.info` API docs](https://github.com/Nakaner/bahnstoerungen/tree/62a72b1e0f0255668500b438187ff65aef39242a/api-doc/db-strecken-info)
- [unfinished HAFAS glossary](https://gist.github.com/derhuerst/74b703e2a0fc64e4a0fa8fbb1f3a61b4)
- [various `mgate.exe` HTTP traffic recordings](https://gist.github.com/search?q=post+mgate.exe&ref=searchresults)
@@ -0,0 +1,45 @@
# `journeysFromTrip(tripId, previousStopover, to, [opt])`
`to` must be an [*Friendly Public Transport Format* (FPTF) `stop`](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md#stop) or [`station`](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md#station). See [`journeys()`](journeys.md) for details.
With `opt`, you can override the default options, which look like this:
```js
{
accessibility: 'none', // 'none', 'partial' or 'complete'
stopovers: false, // return stations on the way?
polylines: false, // return leg shapes?
transferTime: 0, // minimum time for a single transfer in minutes
tickets: false, // return tickets?
remarks: true // parse & expose hints & warnings?
}
```
## Response
*Note:* The returned `departure` and `arrival` times include the current delay. The `departureDelay`/`arrivalDelay` fields express how much they differ from `plannedDeparture`/`plannedArrival`, respectively.
As an example, we're going to use the [*Deutsche Bahn* profile](../p/db):
```js
const createClient = require('hafas-client')
const dbProfile = require('hafas-client/p/db')
const berlinSüdkreuz = '8011113'
const münchenHbf = '8000261'
const kölnHbf = '8000207'
const client = createClient(dbProfile, 'my-awesome-program')
// find any journey from Berlin Südkreuz to München Hbf
const [journey] = await client.journeys(berlinSüdkreuz, münchenHbf, {results: 1, stopovers: true})
// find the ICE leg
const leg = journey.legs.find(l => l.line.product === 'nationalExpress')
// find the stopover at the stop you've just passed
const previousStopover = leg.stopovers.find(st => st.departure && new Date(st.departure) < Date.now())
// find journeys from the ICE train to Köln Hbf
const journeys = await client.journeysFromTrip(leg.id, previousStopover, kölnHbf)
```
`journeys` will be an array of [FPTF `journey`s](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md#journey), as documented in [`journeys()`](journeys.md).
@@ -0,0 +1,357 @@
# `journeys(from, to, [opt])`
`from` and `to` each must be in one of these formats:
```js
// a station ID, in a format compatible to the profile you use
'900000013102'
// an FPTF `station` object
{
type: 'station',
id: '900000013102',
name: 'foo station',
location: {
type: 'location',
latitude: 1.23,
longitude: 3.21
}
}
// a point of interest, which is an FPTF `location` object
{
type: 'location',
id: '123',
poi: true,
name: 'foo restaurant',
latitude: 1.23,
longitude: 3.21
}
// an address, which is an FTPF `location` object
{
type: 'location',
address: 'foo street 1',
latitude: 1.23,
longitude: 3.21
}
```
With `opt`, you can override the default options, which look like this:
```js
{
// Use either `departure` or `arrival` to specify a date/time.
departure: new Date(),
arrival: null,
earlierThan: null, // ref to get journeys earlier than the last query
laterThan: null, // ref to get journeys later than the last query
results: null, // number of journeys – `null` means "whatever HAFAS returns"
via: null, // let journeys pass this station
stopovers: false, // return stations on the way?
transfers: -1, // Maximum nr of transfers. Default: Let HAFAS decide.
transferTime: 0, // minimum time for a single transfer in minutes
accessibility: 'none', // 'none', 'partial' or 'complete'
bike: false, // only bike-friendly journeys
walkingSpeed: 'normal', // 'slow', 'normal', 'fast'
// Consider walking to nearby stations at the beginning of a journey?
startWithWalking: true,
products: {
// these entries may vary from profile to profile
suburban: true,
subway: true,
tram: true,
bus: true,
ferry: true,
express: true,
regional: true
},
tickets: false, // return tickets? only available with some profiles
polylines: false, // return a shape for each leg?
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
remarks: true, // parse & expose hints & warnings?
scheduledDays: false, // parse which days each journey is valid on
language: 'en', // language to get results in
}
```
## Response
*Note:* As stated in the [*Friendly Public Transport Format* v2 draft spec](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md), the returned `departure` and `arrival` times include the current delay. The `departureDelay`/`arrivalDelay` fields express how much they differ from the schedule.
As an example, we're going to use the [VBB profile](../p/vbb):
```js
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,
stopovers: true
})
.then(console.log)
.catch(console.error)
```
The `Promise` returned by `journeys()` will resolve with an object with the `journeys` & `earlierRef`/`laterRef` fields. It might look like this:
```js
{
journeys: [ {
legs: [ {
tripId: '1|32615|6|86|10072018',
direction: 'S Ahrensfelde',
line: {
type: 'line',
id: '16845',
fahrtNr: '12345',
name: 'S7',
public: true,
mode: 'train',
product: 'suburban',
operator: {
type: 'operator',
id: 's-bahn-berlin-gmbh',
name: 'S-Bahn Berlin GmbH'
},
symbol: 'S',
nr: 7,
metro: false,
express: false,
night: false
},
currentLocation: {
type: 'location',
latitude: 52.51384,
longitude: 13.526806,
},
origin: {
type: 'station',
id: '900000003201',
name: 'S+U Berlin Hauptbahnhof',
location: {
type: 'location',
latitude: 52.52585,
longitude: 13.368928
},
products: {
suburban: true,
subway: true,
tram: true,
bus: true,
ferry: false,
express: true,
regional: true
}
},
departure: '2018-07-10T23:54:00+02:00',
plannedDeparture: '2018-07-10T23:53:00+02:00',
departureDelay: 60,
departurePlatform: '15',
plannedDeparturePlatform: '14',
destination: {
type: 'station',
id: '900000100004',
name: 'S+U Jannowitzbrücke',
location: {
type: 'location',
latitude: 52.504806,
longitude: 13.303846
},
products: { /* … */ }
},
arrival: '2018-07-11T00:02:00+02:00',
plannedArrival: '2018-07-11T00:01:00+02:00',
arrivalDelay: 60,
arrivalPlatform: '3',
plannedArrivalPlatform: '3',
stopovers: [ {
stop: {
type: 'station',
id: '900000003201',
name: 'S+U Berlin Hauptbahnhof',
/* … */
},
arrival: null,
plannedArrival: null,
arrivalPlatform: null,
plannedArrivalPlatform: null,
departure: null,
plannedDeparture: null,
departurePlatform: null,
plannedDeparturePlatform: null,
remarks: [
{type: 'hint', code: 'bf', text: 'barrier-free'},
{type: 'hint', code: 'FB', text: 'Bicycle conveyance'}
]
}, {
stop: {
type: 'station',
id: '900000100001',
name: 'S+U Friedrichstr.',
/* … */
},
cancelled: true,
arrival: null,
plannedArrival: '2018-07-10T23:55:00+02:00',
prognosedArrival: '2018-07-10T23:56:00+02:00',
arrivalDelay: 60,
arrivalPlatform: null,
plannedArrivalPlatform: null,
departure: null,
plannedDeparture: '2018-07-10T23:56:00+02:00',
prognosedDeparture: '2018-07-10T23:57:00+02:00',
departureDelay: 60,
departurePlatform: null,
plannedDeparturePlatform: null,
remarks: [ /* … */ ]
},
/* … */
{
stop: {
type: 'station',
id: '900000100004',
name: 'S+U Jannowitzbrücke',
/* … */
},
arrival: '2018-07-11T00:02:00+02:00',
plannedArrival: '2018-07-11T00:01:00+02:00',
arrivalDelay: 60,
arrivalPlatform: null,
plannedArrivalPlatform: null,
departure: '2018-07-11T00:02:00+02:00',
plannedDeparture: '2018-07-11T00:02:00+02:00',
departureDelay: null,
departurePlatform: null,
plannedDeparturePlatform: null,
remarks: [ /* … */ ]
} ]
}, {
public: true,
walking: true,
distance: 558,
origin: {
type: 'station',
id: '900000100004',
name: 'S+U Jannowitzbrücke',
location: { /* … */ },
products: { /* … */ }
},
departure: '2018-07-11T00:01:00+02:00',
destination: {
type: 'station',
id: '900000100008',
name: 'U Heinrich-Heine-Str.',
location: { /* … */ },
products: { /* … */ }
},
arrival: '2018-07-11T00:10:00+02:00'
} ]
} ],
earlierRef: '…', // use with the `earlierThan` option
laterRef: '…' // use with the `laterThan` option
}
```
Some [profiles](../p) are able to parse the ticket information, if returned by the API. For example, if you pass `tickets: true` with the [VBB profile](../p/vbb), each `journey` will have a tickets array that looks like this:
```js
[ {
name: 'Berlin Tarifgebiet A-B: Einzelfahrausweis – Regeltarif',
price: 2.8,
tariff: 'Berlin',
coverage: 'AB',
variant: 'adult',
amount: 1
}, {
name: 'Berlin Tarifgebiet A-B: Einzelfahrausweis – Ermäßigungstarif',
price: 1.7,
tariff: 'Berlin',
coverage: 'AB',
variant: 'reduced',
amount: 1,
reduced: true
}, /* … */ {
name: 'Berlin Tarifgebiet A-B: Tageskarte – Ermäßigungstarif',
price: 4.7,
tariff: 'Berlin',
coverage: 'AB',
variant: '1 day, reduced',
amount: 1,
reduced: true,
fullDay: true
}, /* … */ {
name: 'Berlin Tarifgebiet A-B: 4-Fahrten-Karte – Regeltarif',
price: 9,
tariff: 'Berlin',
coverage: 'AB',
variant: '4x adult',
amount: 4
} ]
```
If a journey leg has been cancelled, a `cancelled: true` will be added. Also, `departure`/`departureDelay`/`departurePlatform` and `arrival`/`arrivalDelay`/`arrivalPlatform` will be `null`.
To get more journeys earlier/later than the current set of results, pass `earlierRef`/`laterRef` into `opt.earlierThan`/`opt.laterThan`. For example, query *later* journeys as follows:
```js
const hbf = '900000003201'
const heinrichHeineStr = '900000100008'
client.journeys(hbf, heinrichHeineStr)
.then((res) => {
const lastJourney = res.journeys[res.journeys.length - 1]
console.log('departure of last journey', lastJourney.legs[0].departure)
// get later journeys
return client.journeys(hbf, heinrichHeineStr, {
laterThan: res.laterRef
})
})
.then((laterRes) => {
const firstLaterJourney = laterRes.journeys[laterRes.journeys.length - 1]
console.log('departure of first (later) journey', firstLaterJourney.legs[0].departure)
})
.catch(console.error)
```
```
departure of last journey 2017-12-17T19:07:00+01:00
departure of first (later) journey 2017-12-17T19:19:00+01:00
```
If you pass `polylines: true`, each journey leg will have a `polyline` field. Refer to [the section in the `trip()` docs](trip.md#polyline-option) for details.
If you pass `scheduledDays: true`, each journey will have a `scheduledDays` object looking like this:
```js
{
'2018-01-01': true,
'2018-01-02': false,
// …
'2018-10-12': true,
'2018-10-13': true,
// …
'2019-01-02': false,
'2019-01-03': false
}
```
@@ -0,0 +1,62 @@
# `lines([opt])`
**Fetches all lines known to the HAFAS endpoint**, e.g. warnings about disruptions, planned construction work, and general notices about the operating situation.
## Example
As an example, we're going to use the [SVV profile](../p/svv):
```js
const createClient = require('hafas-client')
const svvProfile = require('hafas-client/p/svv')
const client = createClient(svvProfile, 'my-awesome-program')
console.log(await client.lines('S1'))
```
```js
[
{
"id": "obb-1-S1-V-j20-1",
"type": "line",
"name": "S1",
"public": true,
"mode": "train",
"product": "bahn-s-bahn",
"operator": {
"type": "operator",
"id": "montafonerbahn-ag",
"name": "Montafonerbahn AG"
},
"directions": [
"Bludenz Bahnhof",
"Bregenz Hafen Bahnhof",
"Lindau Hbf",
"Bregenz Bahnhof",
"Schruns Bahnhof",
"Lochau Bahnhof"
],
},
// …
{
"id": "svv-42-50-j20-2",
"type": "line",
"name": "S1",
"public": true,
"mode": "train",
"product": "bahn-s-bahn",
"operator": {
"type": "operator",
"id": "salzburg-ag-salzburger-lokalbahn",
"name": "Salzburg AG - Salzburger Lokalbahn"
},
"directions": [
"Lamprechtshausen Bahnhof",
"Salzburg Hauptbahnhof",
"Acharting S-Bahn",
"Weitwörth-Nussdorf Bahnhof"
],
},
]
```
@@ -0,0 +1,72 @@
# `locations(query, [opt])`
`query` must be an string (e.g. `'Alexanderplatz'`).
With `opt`, you can override the default options, which look like this:
```js
{
fuzzy: true // find only exact matches?
, results: 5 // how many search results?
, stops: true // return stops/stations?
, addresses: true
, poi: true // points of interest
, subStops: true // parse & expose sub-stops of stations?
, entrances: true // parse & expose entrances of stops/stations?
, linesOfStops: false // parse & expose lines at each stop/station?
, language: 'en' // language to get results in
}
```
## Response
As an example, we're going to use the [VBB profile](../p/vbb):
```js
const createClient = require('hafas-client')
const vbbProfile = require('hafas-client/p/vbb')
const client = createClient(vbbProfile, 'my-awesome-program')
client.locations('Alexanderplatz', {results: 3})
.then(console.log)
.catch(console.error)
```
The response may look like this:
```js
[ {
type: 'stop',
id: '900000100003',
name: 'S+U Alexanderplatz',
location: {
type: 'location',
latitude: 52.521508,
longitude: 13.411267
},
products: {
suburban: true,
subway: true,
tram: true,
bus: true,
ferry: false,
express: false,
regional: true
}
}, { // point of interest
type: 'location',
id: '900980709',
poi: true,
name: 'Berlin, Holiday Inn Centre Alexanderplatz****',
latitude: 52.523549,
longitude: 13.418441
}, { // point of interest
type: 'location',
id: '900980176',
poi: true,
name: 'Berlin, Hotel Agon am Alexanderplatz',
latitude: 52.524556,
longitude: 13.420266
} ]
```
@@ -0,0 +1,108 @@
# Migrating to `hafas-client@5`
## If you use Node `8` ("Carbon")…
…migrate to Node `10` ("Dubnium"), sorry. [Node `8` is out of maintenance now](https://nodejs.org/en/about/releases/). 83f43c6
## new fields for departure/arrival time & delays
An arrival/departure now looks like this:
```js
{
when: null, // realtime/prognosed
plannedWhen: '2019-10-10T10:10+10:00',
platform: '3', // realtime/prognosed
plannedPlatform: '4'
}
```
A stopover/journey leg now will look like this:
```js
{
arrival: null, // realtime/prognosed
plannedArrival: '2019-10-10T10:10+10:00',
arrivalDelay: null,
arrivalPlatform: '3', // realtime/prognosed
plannedArrivalPlatform: '4',
departure: '2019-10-10T10:12+10:00', // realtime/prognosed
plannedDeparture: '2019-10-10T10:10+10:00',
departureDelay: 120, // seconds
departurePlatform: null, // realtime/prognosed
plannedDeparturePlatform: null
}
```
If the same stopover/journey leg is `cancelled: true`, it will look like this:
```js
{
arrival: null,
prognosedArrival: null,
plannedArrival: '2019-10-10T10:10+10:00',
arrivalDelay: null,
arrivalPlatform: null,
prognosedArrivalPlatform: '3',
plannedArrivalPlatform: '4',
departure: null,
prognosedDeparture: '2019-10-10T10:12+10:00',
plannedDeparture: '2019-10-10T10:10+10:00',
departureDelay: 120, // seconds
departurePlatform: null,
prognosedDeparturePlatform: null,
plannedDeparturePlatform: null
}
```
## If you use `journeys()`…
…with the `walkingSpeed` option and a custom profile, add `journeysWalkingSpeed` to your profile. 937583e
…without the `results` option, but *expect* a certain number of results, you must pass `results` now. 0045587
## If you use `departures()`/`arrivals()` with the [BVG profile](../p/bvg)…
With the latest protocol version, the BVG endpoint doesn't support these options anymore:
- `stopovers` – Fetch & parse previous/next stopovers? Default: `false`
- `includeRelatedStations` – Fepartures at related stations, e.g. those that belong together on the metro map? Default: `true`
2d72391
## If you use a custom profile…
Let's assume you have parse function looking like this:
```js
const createParseLine = (profile, opt, data) => (rawLine) => {
const operator = data.operators[rawLine.oprX] || null
if (operator && operator.name === 'foo') {
return {
type: 'line',
name: 'really special tram line',
mode: 'tram',
product: 'special-tram',
operator
}
}
return defaultParseLine(rawLine)
}
```
Adapt your parse function like this:
```diff
const createParseLine = (profile, opt, _) => (rawLine) => {
- const operator = data.operators[rawLine.oprX] || null
+ const operator = rawLine.operator || null
```
See also [`#127`](https://github.com/public-transport/hafas-client/pull/127).
If you use `icons` in `parseWarning`/`parseHint`, adapt the function(s) to take an object `data` as the first argument. You can access the list of *parsed* icons via `data.icons`, *parsed* warnings via `data.warnings`, etc. a229205 b36f0e3
## Other breaking changes
- `journey.price` will be `null` if there's no pricing data returned by the endpoint, instead of `{amount: null}`. 8fe277d
@@ -0,0 +1,86 @@
# `nearby(location, [opt])`
This method can be used to find stations close to a location. Note that it is not supported by every profile/endpoint.
`location` must be an [*FPTF* `location` object](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md#location-objects).
With `opt`, you can override the default options, which look like this:
```js
{
results: 8, // maximum number of results
distance: null, // maximum walking distance in meters
poi: false, // return points of interest?
stops: true, // return stops/stations?
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
linesOfStops: false, // parse & expose lines at each stop/station?
language: 'en' // language to get results in
}
```
## Response
As an example, we're going to use the [VBB profile](../p/vbb):
```js
const createClient = require('hafas-client')
const vbbProfile = require('hafas-client/p/vbb')
const client = createClient(vbbProfile, 'my-awesome-program')
client.nearby({
type: 'location',
latitude: 52.5137344,
longitude: 13.4744798
}, {distance: 400})
.then(console.log)
.catch(console.error)
```
The response may look like this:
```js
[ {
type: 'stop',
id: '900000120001',
name: 'S+U Frankfurter Allee',
location: {
type: 'location',
latitude: 52.513616,
longitude: 13.475298
},
products: {
suburban: true,
subway: true,
tram: true,
bus: true,
ferry: false,
express: false,
regional: false
},
distance: 56
}, {
type: 'stop',
id: '900000120540',
name: 'Scharnweberstr./Weichselstr.',
location: {
type: 'location',
latitude: 52.512339,
longitude: 13.470174
},
products: { /* … */ },
distance: 330
}, {
type: 'stop',
id: '900000160544',
name: 'Rathaus Lichtenberg',
location: {
type: 'location',
latitude: 52.515908,
longitude: 13.479073
},
products: { /* … */ },
distance: 394
} ]
```
@@ -0,0 +1,44 @@
'use strict'
// Refer to the the ./writing-a-profile.md guide.
const products = [
{
id: 'commuterTrain',
mode: 'train',
bitmasks: [16],
name: 'ACME Commuter Rail',
short: 'CR',
default: true
},
{
id: 'metro',
mode: 'train',
bitmasks: [8],
name: 'Foo Bar Metro',
short: 'M',
default: true
}
]
const transformReqBody = (body) => {
// get these from the recorded app requests
// body.client = { … }
// body.ver = …
// body.auth = { … }
// body.lang = …
return body
}
const insaProfile = {
// locale: …,
// timezone: …,
// endpoint: …,
transformReqBody,
products: products,
trip: false,
radar: false
}
module.exports = insaProfile
@@ -0,0 +1,187 @@
# `radar({north, west, south, east}, [opt])`
Use this method to find all vehicles currently in an area. Note that it is not supported by every profile/endpoint.
`north`, `west`, `south` and `eath` must be numbers (e.g. `52.52411`). Together, they form a [bounding box](https://en.wikipedia.org/wiki/Minimum_bounding_box).
With `opt`, you can override the default options, which look like this:
```js
{
results: 256, // maximum number of vehicles
duration: 30, // compute frames for the next n seconds
frames: 3, // nr of frames to compute
polylines: true, // return a track shape for each vehicle?
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
language: 'en' // language to get results in
}
```
## Response
*Note:* As stated in the [*Friendly Public Transport Format* v2 draft spec](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md), the returned `departure` and `arrival` times include the current delay. The `departureDelay`/`arrivalDelay` fields express how much they differ from the schedule.
As an example, we're going to use the [VBB profile](../p/vbb):
```js
const createClient = require('hafas-client')
const vbbProfile = require('hafas-client/p/vbb')
const client = createClient(vbbProfile, 'my-awesome-program')
client.radar({
north: 52.52411,
west: 13.41002,
south: 52.51942,
east: 13.41709
}, {results: 5})
.then(console.log)
.catch(console.error)
```
The response may look like this:
```js
[ {
location: {
type: 'location',
latitude: 52.521508,
longitude: 13.411267
},
line: {
type: 'line',
id: 's9',
fahrtNr: '12345',
name: 'S9',
public: true,
mode: 'train',
product: 'suburban',
symbol: 'S',
nr: 9,
metro: false,
express: false,
night: false,
operator: {
type: 'operator',
id: 's-bahn-berlin-gmbh',
name: 'S-Bahn Berlin GmbH'
}
},
direction: 'S Flughafen Berlin-Schönefeld',
trip: 31463, // todo: outdated, should be tripId!
nextStopovers: [ {
stop: {
type: 'stop',
id: '900000029101',
name: 'S Spandau',
location: {
type: 'location',
latitude: 52.534794,
longitude: 13.197477
},
products: {
suburban: true,
subway: false,
tram: false,
bus: true,
ferry: false,
express: true,
regional: true
}
},
arrival: null,
plannedArrival: null,
arrivalDelay: null,
arrivalPlatform: null,
plannedArrivalPlatform: null,
departure: null,
plannedDeparture: '2017-12-17T19:16:00+01:00',
departureDelay: null,
departurePlatform: null,
plannedDeparturePlatform: '1'
} /* … */ ],
frames: [ {
origin: {
type: 'stop',
id: '900000100003',
name: 'S+U Alexanderplatz',
location: { /* … */ },
products: { /* … */ }
},
destination: {
type: 'stop',
id: '900000100004',
name: 'S+U Jannowitzbrücke',
location: { /* … */ },
products: { /* … */ }
},
t: 0
}, /* … */ {
origin: { /* Alexanderplatz */ },
destination: { /* Jannowitzbrücke */ },
t: 30000
} ]
}, {
location: {
type: 'location',
latitude: 52.523297,
longitude: 13.411151
},
line: {
type: 'line',
id: 'm2',
fahrtNr: '54321',
name: 'M2',
public: true,
mode: 'train',
product: 'tram',
symbol: 'M',
nr: 2,
metro: true,
express: false,
night: false,
operator: {
type: 'operator',
id: 'berliner-verkehrsbetriebe',
name: 'Berliner Verkehrsbetriebe'
}
},
direction: 'Heinersdorf',
trip: 26321,
nextStopovers: [ {
stop: { /* S+U Alexanderplatz/Dircksenstr. */ },
arrival: null,
plannedArrival: null,
arrivalDelay: null,
departure: null,
plannedAeparture: '2017-12-17T19:52:00+01:00',
departureDelay: null
}, {
stop: { /* Memhardstr. */ },
arrival: null,
plannedArrival: '2017-12-17T19:54:00+01:00',
arrivalDelay: null,
arrivalPlatform: null,
plannedArrivalPlatform: null,
departure: null,
plannedDeparture: '2017-12-17T19:54:00+01:00',
departureDelay: null,
departurePlatform: null,
plannedDeparturePlatform: '1'
}, /* … */ ],
frames: [ {
origin: { /* S+U Alexanderplatz/Dircksenstr. */ },
destination: { /* Memhardstr. */ },
t: 0
}, /* … */ {
origin: { /* Memhardstr. */ },
destination: { /* Mollstr./Prenzlauer Allee */ },
t: 30000
} ]
}, /* … */ ]
```
If you pass `polylines: true`, each movement will have a `polyline` field, as documented in [the corresponding section in the `trip()` docs](trip.md#polyline-option), with the exception that station info is missing.
@@ -0,0 +1,94 @@
# `reachableFrom(address, [opt])`
This method can be used to get stations reachable within a certain time from an address. This concept is called [isochrone diagram](https://en.wikipedia.org/wiki/Isochrone_map#Transportation_planning).
*Note*: It appears that HAFAS cannot generate actual isochrones, but only the list of reachable stations, which you can estimate the isochrone(s) from.
`address` must be an [*FPTF* `location` object](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md#location-objects).
With `opt`, you can override the default options, which look like this:
```js
{
when: new Date(),
maxTransfers: 5, // maximum of 5 transfers
maxDuration: 20, // maximum travel duration in minutes, pass `null` for infinite
products: {
// These entries may vary from profile to profile!
suburban: true,
subway: true
// …
},
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
}
```
## Response
`reachableFrom(address, [opt])` returns an array, in which each item has a `duration` and an array of [*Friendly Public Transport Format* `station`s](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md#station).
As an example, we're going to use the [VBB profile](../p/vbb):
```js
const createClient = require('hafas-client')
const vbbProfile = require('hafas-client/p/vbb')
const client = createClient(vbbProfile, 'my-awesome-program')
client.reachableFrom({
type: 'location',
address: '13353 Berlin-Wedding, Torfstr. 17',
latitude: 52.541797,
longitude: 13.350042
}, {
maxDuration: 10 // minutes
})
.then(console.log)
.catch(console.error)
```
The response may look like this:
```js
[
{
duration: 2,
stations: [
{
type: 'stop',
id: '900000009101',
name: 'U Amrumer Str.',
location: {type: 'location', latitude: 52.542201, longitude: 13.34953},
products: { /* … */ }
}
]
}, {
duration: 3,
stations: [
{
type: 'stop',
id: '900000001201',
name: 'S+U Westhafen',
location: {type: 'location', latitude: 52.536179, longitude: 13.343839},
products: { /* … */ }
}
// …
]
},
// …
{
duration: 10,
stations: [
{
type: 'stop',
id: '900000001203',
name: 'Döberitzer Str.',
location: {type: 'location', latitude: 52.530668, longitude: 13.36811},
products: { /* … */ }
}
// …
]
}
]
```
@@ -0,0 +1,85 @@
# API documentation
- [`journeys(from, to, [opt])`](journeys.md) – get journeys between locations
- [`refreshJourney(refreshToken, [opt])`](refresh-journey.md) – fetch up-to-date/more details of a `journey`
- [`journeysFromTrip(tripId, previousStopover, to, [opt])`](journeys-from-trip.md) – get journeys from a trip to a location
- [`trip(id, lineName, [opt])`](trip.md) – get details for a trip
- [`tripsByName(lineNameOrFahrtNr, [opt])`](trips-by-name.md) – get all trips matching a name
- [`departures(station, [opt])`](departures.md) – query the next departures at a station
- [`arrivals(station, [opt])`](arrivals.md) – query the next arrivals at a station
- [`locations(query, [opt])`](locations.md) – find stations, POIs and addresses
- [`stop(id, [opt])`](stop.md) – get details about a stop/station
- [`nearby(location, [opt])`](nearby.md) – show stations & POIs around
- [`radar(north, west, south, east, [opt])`](radar.md) – find all vehicles currently in a certain area
- [`reachableFrom(address, [opt])`](reachable-from.md) – get all stations reachable from an address within `n` minutes
- [`remarks([opt])`](remarks.md) – get all remarks
- [`lines(query, [opt])`](lines.md) – get all lines matching a name
- [`serverInfo([opt])`](server-info.md) – fetch meta information from HAFAS
## Migrating from an old `hafas-client` version
- [`4` → `5` migration guide](migrating-to-5.md)
## Throttling requests
There's opt-in support for throttling requests to the endpoint.
```js
const createClient = require('hafas-client')
const withThrottling = require('hafas-client/throttle')
const dbProfile = require('hafas-client/p/db')
// create a throttled HAFAS client with Deutsche Bahn profile
const client = createClient(withThrottling(dbProfile), 'my-awesome-program')
// Berlin Jungfernheide to München Hbf
client.journeys('8011167', '8000261', {results: 1})
.then(console.log)
.catch(console.error)
```
You can pass custom values for the nr of requests (`limit`) per interval into `withThrottling`:
```js
// 2 requests per second
const throttledDbProfile = withThrottling(dbProfile, 2, 1000)
const client = createClient(throttledDbProfile, 'my-awesome-program')
```
## Retrying failed requests
There's opt-in support for retrying failed requests to the endpoint.
```js
const createClient = require('hafas-client')
const withRetrying = require('hafas-client/retry')
const dbProfile = require('hafas-client/p/db')
// create a client with Deutsche Bahn profile that will retry on HAFAS errors
const client = createClient(withRetrying(dbProfile), 'my-awesome-program')
// Berlin Jungfernheide to München Hbf
client.journeys('8011167', '8000261', {results: 1})
.then(console.log)
.catch(console.error)
```
You can pass custom options into `withRetrying`. They will be passed into [`retry`](https://github.com/tim-kos/node-retry#tutorial).
```js
// retry 2 times, after 10 seconds & 30 seconds
const retryingDbProfile = withRetrying(dbProfile, {
retries: 2,
minTimeout: 10 * 1000,
factor: 3
})
const client = createClient(retryingDbProfile, 'my-awesome-program')
```
## Writing a profile
Check [the guide](writing-a-profile.md).
## General documentation for `mgate.exe` APIs
[`hafas-mgate-api.md`](hafas-mgate-api.md)
@@ -0,0 +1,40 @@
# `refreshJourney(refreshToken, [opt])`
`refreshToken` must be a string, taken from `journey.refreshToken`.
With `opt`, you can override the default options, which look like this:
```js
{
stopovers: false, // return stations on the way?
polylines: false, // return a shape for each leg?
tickets: false, // return tickets? only available with some profiles
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
remarks: true, // parse & expose hints & warnings?
language: 'en' // language to get results in
}
```
## Response
As an example, we're going to use the [VBB profile](../p/vbb):
```js
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* v2 draft](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md) `journey`, in the same format as with `journeys()`.
@@ -0,0 +1,142 @@
# `remarks([opt])`
**Fetches all remarks known to the HAFAS endpoint**, e.g. warnings about disruptions, planned construction work, and general notices about the operating situation.
With `opt`, you can override the default options, which look like this:
```js
{
results: 100, // maximum number of remarks
// filter by time
from: Date.now(),
to: null,
products: null, // filter by affected products
language: 'en', // depends on the profile
}
```
## Example
As an example, we're going to use the [SVV profile](../p/svv):
```js
const createClient = require('hafas-client')
const svvProfile = require('hafas-client/p/svv')
const client = createClient(svvProfile, 'my-awesome-program')
console.log(await client.remarks())
```
```js
[
{
id: 'HIM_FREETEXT_110342',
type: 'warning',
summary: 'Bus will be running at different times',
text: 'Due to operational changes, this bus will be running at different times. We apologise for any inconvenience this may cause.',
priority: 50,
company: 'KGÖVV',
validFrom: '2020-07-04T00:00:00+02:00',
validUntil: '2020-08-09T23:59:00+02:00',
modified: '2020-07-01T14:39:12+02:00',
products: {
'bahn-s-bahn': true,
'u-bahn': true,
strassenbahn: true,
fernbus: true,
regionalbus: true,
stadtbus: true,
'seilbahn-zahnradbahn': true,
schiff: true,
},
categories: [1],
icon: {type: 'HIM1', title: null},
},
// …
{
id: 'HIM_FREETEXT_110235',
type: 'warning',
summary: 'Linie 7 - Umleitungen',
text: 'Aufgrund einer Baustelle gibt es bei der Linie 7 umfangreiche Umleitungen.',
priority: 100,
company: 'VOR',
validFrom: '2020-07-13T00:00:00+02:00',
validUntil: '2020-08-31T23:59:00+02:00',
modified: '2020-06-30T12:37:38+02:00',
affectedLines: [{
type: 'line',
id: '7',
name: '7',
public: true,
}],
products: {
'bahn-s-bahn': true,
'u-bahn': true,
strassenbahn: true,
fernbus: true,
regionalbus: true,
stadtbus: true,
'seilbahn-zahnradbahn': false,
schiff: true,
},
categories: [1],
icon: {type: 'HIM1', title: null},
},
// …
{
id: 'HIM_FREETEXT_106619',
type: 'warning',
summary: 'Stop Bad Hall Bahnhofstraße can not be approached',
text: 'The stop at Bad Hall Bahnhofstraße can not be approached during 21.04.-24.07.2020. Please use alternatively the stop at Bad Hall Busterminal (Abzw Bahnhofstraße).',
priority: 100,
company: 'OÖVG',
validFrom: '2020-04-21T00:00:00+02:00',
validUntil: '2020-07-24T23:59:00+02:00',
modified: '2020-07-08T12:52:13+02:00',
affectedLines: [{
type: 'line',
id: '452',
name: '452',
public: true,
}],
products: {
'bahn-s-bahn': false,
'u-bahn': false,
strassenbahn: false,
fernbus: false,
regionalbus: true,
stadtbus: false,
'seilbahn-zahnradbahn': false,
schiff: false
},
categories: [1],
icon: {type: 'HIM1', title: null},
},
// …
{
id: 'HIM_FREETEXT_106671',
type: 'warning',
summary: 'Neue Haltestellennamen',
text: 'Im Zuge der Neuordnung der Regionalbusverkehre werden mit 6.7.2020 neue Fahrpläne und Liniennummern wirksam und dadurch können sich mitunter die Haltestellennamen verändern.',
priority: 100,
company: 'VOR',
validFrom: '2020-04-21T00:00:00+02:00',
validUntil: '2020-09-30T23:59:00+02:00',
modified: '2020-04-21T13:20:41+02:00',
products: {
'bahn-s-bahn': true,
'u-bahn': true,
strassenbahn: true,
fernbus: true,
regionalbus: true,
stadtbus: true,
'seilbahn-zahnradbahn': false,
schiff: true,
},
categories: [4],
icon: {type: 'HIM4', title: null},
},
// …
]
```
@@ -0,0 +1,33 @@
# `serverInfo([opt])`
**Fetches meta information from the HAFAS endpoint.**
With `opt`, you can override the default options, which look like this:
```js
{
language: 'en', // depends on the profile
}
```
## Example
As an example, we're going to use the [SVV profile](../p/svv):
```js
const createClient = require('hafas-client')
const svvProfile = require('hafas-client/p/svv')
const client = createClient(svvProfile, 'my-awesome-program')
console.log(await client.serverInfo())
```
```js
{
timetableStart: '20200517',
timetableEnd: '20201212',
serverTime: '2020-07-19T21:32:12+02:00',
realtimeDataUpdatedAt: 1595187102,
}
```
+107
View File
@@ -0,0 +1,107 @@
# `stop(id, [opt])`
`id` must be in one of these formats:
```js
// a stop/station ID, in a format compatible with the profile you use
'900000123456'
// an FPTF `stop`/`station` object
{
type: 'station',
id: '900000123456',
name: 'foo station',
location: {
type: 'location',
latitude: 1.23,
longitude: 3.21
}
}
```
With `opt`, you can override the default options, which look like this:
```js
{
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
linesOfStops: false, // parse & expose lines at the stop/station?
language: 'en' // language to get results in
}
```
## Response
As an example, we're going to use the [VBB profile](../p/vbb):
```js
const createClient = require('hafas-client')
const vbbProfile = require('hafas-client/p/vbb')
const client = createClient(vbbProfile, 'my-awesome-program')
client.stop('900000042101') // U Spichernstr.
.then(console.log)
.catch(console.error)
```
The response may look like this:
```js
{
type: 'stop',
id: '900000042101',
name: 'U Spichernstr.',
location: {
type: 'location',
latitude: 52.496581,
longitude: 13.330616
},
products: {
suburban: false,
subway: true,
tram: false,
bus: true,
ferry: false,
express: false,
regional: false
},
lines: [ {
type: 'line',
id: 'u1',
mode: 'train',
product: 'subway',
public: true,
name: 'U1',
symbol: 'U',
nr: 1,
metro: false,
express: false,
night: false
},
// …
{
type: 'line',
id: 'n9',
mode: 'bus',
product: 'bus',
public: true,
name: 'N9',
symbol: 'N',
nr: 9,
metro: false,
express: false,
night: true
} ]
}
```
If the endpoint returns a list of entrances for a station, the resulting station object will have an `entrances` array looking similar to this:
```js
[
{type: 'location', latitude: 47.411069, longitude: 10.277412},
{type: 'location', latitude: 47.410493, longitude: 10.277223},
{type: 'location', latitude: 47.410754, longitude: 10.278023}
]
```
+202
View File
@@ -0,0 +1,202 @@
# `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.tripId`, 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.tripId, leg.line.name)
})
.then(console.log)
.catch(console.error)
```
With `opt`, you can override the default options, which look like this:
```js
{
stopovers: true, // return stations on the way?
polyline: false, // return a shape for the trip?
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
remarks: true, // parse & expose hints & warnings?
language: 'en' // language to get results in
}
```
## Response
*Note:* As stated in the [*Friendly Public Transport Format* v2 draft spec](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md), the returned `departure` and `arrival` times include the current delay. The `departureDelay`/`arrivalDelay` fields express how much they differ from the schedule.
As an example, we're going to use the [VBB profile](../p/vbb):
```js
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',
direction: 'S Spandau',
line: {
type: 'line',
id: '18299',
fahrtNr: '12345',
name: 'S9',
public: true,
mode: 'train',
product: 'suburban',
symbol: 'S',
nr: 9,
metro: false,
express: false,
night: false,
operator: {
type: 'operator',
id: 's-bahn-berlin-gmbh',
name: 'S-Bahn Berlin GmbH'
}
},
currentLocation: {
type: 'location',
latitude: 52.447455,
longitude: 13.522464,
},
origin: {
type: 'station',
id: '900000260005',
name: 'S Flughafen Berlin-Schönefeld',
location: {
type: 'location',
latitude: 52.390796,
longitude: 13.51352
},
products: {
suburban: true,
subway: false,
tram: false,
bus: true,
ferry: false,
express: false,
regional: true
}
},
departure: '2017-12-17T18:37:00+01:00',
plannedDeparture: '2017-12-17T18:37:00+01:00',
departureDelay: null,
departurePlatform: '13',
plannedDeparturePlatform: '13',
destination: {
type: 'station',
id: '900000029101',
name: 'S Spandau',
location: {
type: 'location',
latitude: 52.534794,
longitude: 13.197477
},
products: {
suburban: true,
subway: false,
tram: false,
bus: true,
ferry: false,
express: true,
regional: true
}
},
arrival: '2017-12-17T19:50:30+01:00',
plannedArrival: '2017-12-17T19:49:00+01:00',
arrivalDelay: 90,
arrivalPlatform: '3a',
plannedArrivalPlatform: '2',
stopovers: [ /* … */ ]
}
```
### `polyline` option
If you pass `polyline: true`, the trip will have a `polyline` field, containing a [GeoJSON](http://geojson.org) [`FeatureCollection`](https://tools.ietf.org/html/rfc7946#section-3.3) of [`Point`s](https://tools.ietf.org/html/rfc7946#appendix-A.1). Every `Point` next to a station will have `properties` containing the station's metadata.
We'll look at an example for *U6* from *Alt-Mariendorf* to *Alt-Tegel*, taken from the [VBB profile](../p/vbb):
```js
{
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: {
type: 'station',
id: '900000070301',
name: 'U Alt-Mariendorf',
/* … */
},
geometry: {
type: 'Point',
coordinates: [13.3875, 52.43993] // longitude, latitude
}
},
/* … */
{
type: 'Feature',
properties: {
type: 'station',
id: '900000017101',
name: 'U Mehringdamm',
/* … */
},
geometry: {
type: 'Point',
coordinates: [13.38892, 52.49448] // longitude, latitude
}
},
/* … */
{
// intermediate point, without associated station
type: 'Feature',
properties: {},
geometry: {
type: 'Point',
coordinates: [13.28599, 52.58742] // longitude, latitude
}
},
{
type: 'Feature',
properties: {
type: 'station',
id: '900000089301',
name: 'U Alt-Tegel',
/* … */
},
geometry: {
type: 'Point',
coordinates: [13.28406, 52.58915] // longitude, latitude
}
}
]
}
```
@@ -0,0 +1,108 @@
# `tripsByName([lineNameOrFahrtNr], [opt])`
Get all trips matching one or more criteria, e.g. a specific name.
## Response
As an example, we're going to use the [VBB profile](../p/vbb):
```js
const createClient = require('hafas-client')
const vbbProfile = require('hafas-client/p/vbb')
const client = createClient(vbbProfile, 'my-awesome-program')
console.log(await client.tripsByName('S1'))
```
With `opt`, you can override the default options, which look like this:
```js
{
// use either this
when: null,
// or these
fromWhen: null, untilWhen: null,
onlyCurrentlyRunning: true,
products: {
// these entries may vary from profile to profile
suburban: true,
subway: true,
tram: true,
bus: true,
ferry: true,
express: true,
regional: true,
},
currentlyStoppingAt: null, // only show trips currently stopping at a stop/station, string
lineName: null, // only show trips with this line name, string
operatorNames: null, // only show trips with these operator names, array of strings
}
```
The response may look like this:
```js
[
{
id: '1|1214|0|86|16092020'
direction: null,
line: {
type: 'line',
id: 's1',
fahrtNr: '325',
name: 'S1',
mode: 'train',
product: 'suburban',
// …
},
origin: {
type: 'stop',
id: '900000550239',
name: 'Warnemünde, Bhf',
location: { /* … */ },
products: { /* … */ },
},
departure: '2020-09-16T04:03:00+02:00',
plannedDeparture: '2020-09-16T04:03:00+02:00',
departureDelay: null,
departurePlatform: null,
plannedDeparturePlatform: null,
destination: {
type: 'stop',
id: '900000550002',
name: 'Rostock, Hbf',
location: { /* … */ },
products: { /* … */ },
},
arrival: '2020-09-16T04:24:00+02:00',
plannedArrival: '2020-09-16T04:24:00+02:00',
arrivalDelay: null,
arrivalPlatform: null,
plannedArrivalPlatform: null,
},
// …
{
id: '1|62554|0|86|16092020'
direction: null,
line: {
type: 'line',
id: 's1',
fahrtNr: '2001',
name: 'S1',
public: true,
mode: 'train',
product: 'suburban',
// …
},
origin: { /* … */ },
destination: { /* … */ },
// …
}
]
```
@@ -0,0 +1,163 @@
# Writing a profile
**Per HAFAS endpoint, `hafas-client` has an endpoint-specific customisation called *profile*.** A profile may, for example, do the following:
- handle the additional requirements of the endpoint (e.g. authentication),
- extract additional information from the data provided by the endpoint,
- guard against triggering bugs of certain endpoints (e.g. time limits).
This guide is about writing such a profile. If you just want to use an already supported endpoint, refer to the [main readme](../readme.md) instead.
*Note*: **If you get stuck, ask for help by [creating an issue](https://github.com/public-transport/hafas-client/issues/new)**; We're happy to help you expand the scope of this library!
## 0. How do the profiles work?
A profile may consist of three things:
- **mandatory details about the HAFAS endpoint**
- `endpoint`: The protocol, host and path of the endpoint.
- `locale`: The [BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag) [locale](https://en.wikipedia.org/wiki/Locale_(computer_software)) of your endpoint (or the area that your endpoint covers).
- `timezone`: An [IANA-time-zone](https://www.iana.org/time-zones)-compatible [timezone](https://en.wikipedia.org/wiki/Time_zone) of your endpoint.
- **flags indicating which features are supported by the endpoint** – e.g. `trip`
- **methods overriding the [default profile](../lib/default-profile.js)**
Let's use a fictional endpoint for [Austria](https://en.wikipedia.org/wiki/Austria) as an example:
```js
const myProfile = {
endpoint: 'https://example.org/bin/mgate.exe',
locale: 'de-AT',
timezone: 'Europe/Vienna'
}
```
Assuming their HAFAS endpoint returns all line names prefixed with `foo `, we can adapt our profile to clean them:
```js
// get the default line parser
const parseLine = require('hafas-client/parse/line')
// wrapper function with additional logic
const parseLineWithoutFoo = (ctx, rawLine) => {
const line = parseLine(ctx, rawLine)
line.name = line.name.replace(/foo /g, '')
return line
}
myProfile.parseLine = parseLineWithoutFoo
```
If you pass this profile into `hafas-client`, the `parseLine` method will override [the default one](../parse/line.js).
You can also use the `parseHook` helper to reduce boilerplate:
```js
const {parseHook} = require('hafas-client/lib/profile-hooks')
const removeFoo = (ctx, rawLine) => ({
...ctx.parsed,
name: line.name.replace(/foo /g, '')
})
myProfile.parseLine = parseHook(parseLine, removeFoo)
```
## 1. Setup
*Note*: There are many ways to find the required values. This way is rather easy and works with most endpoints by now.
1. **Find the journey planning webapp** corresponding to the API endpoint; Usually, you can find it on the public transport provider's website.
2. **Open your [browser's devtools](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_are_browser_developer_tools)**, switch to the "Network" tab, and **inspect the requests to the HAFAS API**.
If you can't find the webapp or your public transport provider doesn't have one, you can inspect their mobile app's traffic instead:
1. Get an iOS or Android device and **download the "official" app.**
2. **Configure a [man-in-the-middle HTTP proxy](https://docs.mitmproxy.org/stable/concepts-howmitmproxyworks/)** like [mitmproxy](https://mitmproxy.org).
- Configure your device to trust the self-signed SSL certificate, [as outlined in the mitmproxy docs](https://docs.mitmproxy.org/stable/concepts-certificates/).
- *Note*: This method does not work if the app uses [public key pinning](https://en.wikipedia.org/wiki/HTTP_Public_Key_Pinning). In this case (the app won't be able to query data), please [create an issue](https://github.com/public-transport/hafas-client/issues/new), so we can discuss other techniques.
3. **Record requests of the app.**
- [There's a video showing this step](https://stuff.jannisr.de/how-to-record-hafas-requests.mp4).
- Make sure to cover all relevant sections of the app, e.g. "journeys", "departures", "live map". Better record more than less!
- To help others in the future, post the requests (in their entirety!) on GitHub, e.g. in as format like [this](https://gist.github.com/derhuerst/5fa86ed5aec63645e5ae37e23e555886). This will also let us help you if you have any questions.
## 2. Basic profile
*Note:* You should have read the [general documentation on `mgate.exe` APIs](hafas-mgate-api.md) to make sense of the terminology used below.
You may want to start with the [profile boilerplate](profile-boilerplate.js).
- **Identify the `endpoint`.** The protocol, host and path of the endpoint, *but not* the query string.
- *Note*: **`hafas-client` for now only supports the interface providing JSON** (generated from XML), which is being used by the corresponding iOS/Android apps. It supports neither the JSONP, nor the XML, nor the HTML interface. If the endpoint does not end in `mgate.exe`, it mostly likely won't work.
- **Identify the `locale`.** Basically guess work; Use the date & time formats as an indicator.
- **Identify the `timezone`.** This may be tricky, a for example [Deutsche Bahn](https://en.wikipedia.org/wiki/Deutsche_Bahn) returns departures for Moscow as `+01:00` instead of `+03:00`.
- **Copy the authentication** and other meta fields, namely `ver`, `ext`, `client` and `lang`.
- You can find these fields in the root of each request JSON. Check [a HVV request](https://gist.github.com/derhuerst/5a9d29a556b54182f9d30202f7244bfd#file-journeys-http-L11-L54) and [the corresponding HVV profile](https://github.com/public-transport/hafas-client/blob/99142acf8b156599daa69f2e1470901088827982/p/hvv/index.js#L5-L23) for an example.
- Add a function `transformReqBody(ctx, body)` to your profile, which adds the fields to `body`. todo: adapt this
- Some profiles have a `checksum` parameter (like [here](https://gist.github.com/derhuerst/2a735268bd82a0a6779633f15dceba33#file-journey-details-1-http-L1)) or two `mic` & `mac` parameters (like [here](https://gist.github.com/derhuerst/5fa86ed5aec63645e5ae37e23e555886#file-1-http-L1)). If you see one of them in your requests, jump to the [*Authentication* section of the `mgate.exe` docs](hafas-mgate-api.md#authentication). Unfortunately, this is necessary to get the profile working.
## 3. Products
In `hafas-client`, there's a distinction between the `mode` and the `product` fields:
- The `mode` field describes the mode of transport in general. [Standardised by the *Friendly Public Transport Format*](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md#modes), it is on purpose limited to a very small number of possible values, e.g. `train` or `bus`.
- The value for `product` relates to how a means of transport "works" *in local context*. Example: Even though [*S-Bahn*](https://en.wikipedia.org/wiki/Berlin_S-Bahn) and [*U-Bahn*](https://en.wikipedia.org/wiki/Berlin_U-Bahn) in Berlin are both `train`s, they have different operators, service patterns, stations and look different. Therefore, they are two distinct `product`s `subway` and `suburban`.
**Specify `product`s that appear in the app** you recorded requests of. For a fictional transit network, this may look like this:
```js
const products = [
{
id: 'commuterTrain',
mode: 'train',
bitmasks: [16],
name: 'ACME Commuter Rail',
short: 'CR',
default: true
},
{
id: 'metro',
mode: 'train',
bitmasks: [8],
name: 'Foo Bar Metro',
short: 'M',
default: true
}
]
```
Let's break this down:
- `id`: A sensible, [camelCased](https://en.wikipedia.org/wiki/Camel_case#Variations_and_synonyms), alphanumeric identifier. Use it for the key in the `products` array as well.
- `mode`: A [valid *Friendly Public Transport Format* mode](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md#modes).
- `bitmasks`: HAFAS endpoints work with a [bitmask](https://en.wikipedia.org/wiki/Mask_(computing)#Arguments_to_functions) that toggles the individual products. It should be an array of values that toggle the appropriate bit(s) in the bitmask (see below).
- `name`: A short, but distinct name for the means of transport, *just precise enough in local context*, and in the local language. In Berlin, `S-Bahn-Schnellzug` would be too much, because everyone knows what `S-Bahn` means.
- `short`: The shortest possible symbol that identifies the product.
- `default`: Should the product be used for queries (e.g. journeys) by default?
If you want, you can now **verify that the profile works**; We've prepared [a script](https://runkit.com/derhuerst/hafas-client-profile-example/0.2.1) for that. Alternatively, [submit a Pull Request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/) and we will help you out with testing and improvements.
### Finding the right values for the `bitmasks` field
As shown in [the video](https://stuff.jannisr.de/how-to-record-hafas-requests.mp4), search for a journey and toggle off one product at a time, recording the requests. After extracting the products bitmask ([example](https://gist.github.com/derhuerst/193ef489f8aa50c2343f8bf1f2a22069#file-via-http-L34)) you will end up with values looking like these:
```
toggles value binary subtraction bit(s)
all products 31 11111 31 - 0
all but ACME Commuter Rail 15 01111 31 - 2^4 2^4
all but Foo Bar Metro 23 10111 31 - 2^3 2^3
all but product E 25 11001 31 - 2^2 - 2^1 2^2, 2^1
all but product F 30 11110 31 - 2^0 2^0
```
## 4. Additional info
We consider these improvements to be *optional*:
- **Check if the endpoint supports the `trip()` call.**
- In the app, check if you can re-fetch details for the status of a single journey leg. It should load realtime delays and the current progress.
- If this feature is supported, add `trip: true` to the profile.
- **Check if the endpoint supports the live map call.** Does the app have a "live map" showing all vehicles within an area? If so, add `radar: true` to the profile.
- **Consider transforming station & line names** into the formats that's most suitable for *local users*. This is just an optimal optimisation that makes it easier for users of the profile to use the data. Some examples:
- `M13 (Tram)` -> `M13`. With Berlin context, it is obvious that `M13` is a tram.
- `Berlin Jungfernheide Bhf` -> `Berlin Jungfernheide`. With local context, it's obvious that *Jungfernheide* is a train station.
- **Check if the endpoint has non-obvious limitations** and let use know about these. Examples:
- Some endpoints have a time limit, after which they won't return more departures, but silently discard them.
@@ -0,0 +1,25 @@
'use strict'
const formatLocationIdentifier = require('./location-identifier')
const formatCoord = require('./coord')
const formatAddress = (a) => {
if (a.type !== 'location' || !a.latitude || !a.longitude || !a.address) {
throw new TypeError('invalid address')
}
const data = {
A: '2', // address?
O: a.address,
X: formatCoord(a.longitude),
Y: formatCoord(a.latitude)
}
if (a.id) data.L = a.id
return {
type: 'A', // address
name: a.address,
lid: formatLocationIdentifier(data)
}
}
module.exports = formatAddress
@@ -0,0 +1,5 @@
'use strict'
const formatCoord = x => Math.round(x * 1000000)
module.exports = formatCoord
@@ -0,0 +1,22 @@
'use strict'
const {DateTime, IANAZone} = require('luxon')
const timezones = new WeakMap()
// todo: change to `(profile) => (when) => {}`
const formatDate = (profile, when) => {
let timezone
if (timezones.has(profile)) timezone = timezones.get(profile)
else {
timezone = new IANAZone(profile.timezone)
timezones.set(profile, timezone)
}
return DateTime.fromMillis(+when, {
locale: profile.locale,
zone: timezone
}).toFormat('yyyyMMdd')
}
module.exports = formatDate
@@ -0,0 +1,11 @@
'use strict'
const bike = {type: 'BC', mode: 'INC'}
const accessibility = {
none: {type: 'META', mode: 'INC', meta: 'notBarrierfree'},
partial: {type: 'META', mode: 'INC', meta: 'limitedBarrierfree'},
complete: {type: 'META', mode: 'INC', meta: 'completeBarrierfree'}
}
module.exports = {bike, accessibility}
@@ -0,0 +1,13 @@
'use strict'
module.exports = {
date: require('./date'),
time: require('./time'),
filters: require('./filters'),
station: require('./station'),
address: require('./address'),
poi: require('./poi'),
location: require('./location'),
locationFilter: require('./location-filter'),
rectangle: require('./rectangle')
}
@@ -0,0 +1,12 @@
'use strict'
const formatLinesReq = (ctx, query) => {
return {
meth: 'LineMatch',
req: {
input: query,
}
}
}
module.exports = formatLinesReq
@@ -0,0 +1,8 @@
'use strict'
const formatLocationFilter = (stops, addresses, poi) => {
if (stops && addresses && poi) return 'ALL'
return (stops ? 'S' : '') + (addresses ? 'A' : '') + (poi ? 'P' : '')
}
module.exports = formatLocationFilter
@@ -0,0 +1,16 @@
'use strict'
const sep = '@'
const formatLocationIdentifier = (data) => {
let str = ''
for (let key in data) {
if (!Object.prototype.hasOwnProperty.call(data, key)) continue
str += key + '=' + data[key] + sep // todo: escape, but how?
}
return str
}
module.exports = formatLocationIdentifier
@@ -0,0 +1,17 @@
'use strict'
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' || l.type === 'stop') {
return profile.formatStation(l.id)
}
if (l.poi) return profile.formatPoi(l)
if ('string' === typeof l.address) return profile.formatAddress(l)
if (!l.type) throw new TypeError(`missing ${name}.type`)
throw new TypeError(`invalid ${name}.type: ${l.type}`)
}
throw new TypeError(name + ': valid station, address or poi required.')
}
module.exports = formatLocation
@@ -0,0 +1,20 @@
'use strict'
const formatLocationsReq = (ctx, query) => {
const {profile, opt} = ctx
return {
cfg: {polyEnc: 'GPA'},
meth: 'LocMatch',
req: {input: {
loc: {
type: profile.formatLocationFilter(opt.stops, opt.addresses, opt.poi),
name: opt.fuzzy ? query + '?' : query
},
maxLoc: opt.results,
field: 'S' // todo: what is this?
}}
}
}
module.exports = formatLocationsReq
@@ -0,0 +1,28 @@
'use strict'
const nearbyReq = (ctx, location) => {
const {profile, opt} = ctx
return {
cfg: {polyEnc: 'GPA'},
meth: 'LocGeoPos',
req: {
ring: {
cCrd: {
x: profile.formatCoord(location.longitude),
y: profile.formatCoord(location.latitude)
},
maxDist: opt.distance || -1,
minDist: 0
},
locFltrL: [
profile.formatProductsFilter(ctx, opt.products || {}),
],
getPOIs: !!opt.poi,
getStops: !!opt.stops,
maxLoc: opt.results
}
}
}
module.exports = nearbyReq
@@ -0,0 +1,24 @@
'use strict'
const formatLocationIdentifier = require('./location-identifier')
const formatCoord = require('./coord')
const formatPoi = (p) => {
if (p.type !== 'location' || !p.latitude || !p.longitude || !p.id || !p.name) {
throw new TypeError('invalid POI')
}
return {
type: 'P', // POI
name: p.name,
lid: formatLocationIdentifier({
A: '4', // POI?
O: p.name,
L: p.id,
X: formatCoord(p.longitude),
Y: formatCoord(p.latitude)
})
}
}
module.exports = formatPoi
@@ -0,0 +1,35 @@
'use strict'
const isObj = require('lodash/isObject')
const hasProp = (o, k) => Object.prototype.hasOwnProperty.call(o, k)
const formatProductsFilter = (ctx, filter) => {
if (!isObj(filter)) throw new TypeError('products filter must be an object')
const {profile} = ctx
const byProduct = {}
const defaultProducts = {}
for (let product of profile.products) {
byProduct[product.id] = product
defaultProducts[product.id] = product.default
}
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 TypeError('unknown product ' + product)
products++
for (let bitmask of byProduct[product].bitmasks) res = res | bitmask
}
if (products === 0) throw new Error('no products used')
return {
type: 'PROD',
mode: 'INC',
value: res + ''
}
}
module.exports = formatProductsFilter
@@ -0,0 +1,26 @@
'use strict'
const formatRadarReq = (ctx, north, west, south, east) => {
const {profile, opt} = ctx
return {
meth: 'JourneyGeoPos',
req: {
maxJny: opt.results,
onlyRT: false, // todo: does this mean "only realtime"?
date: profile.formatDate(profile, opt.when),
time: profile.formatTime(profile, opt.when),
// todo: would a ring work here as well?
rect: profile.formatRectangle(profile, north, west, south, east),
perSize: opt.duration * 1000,
perStep: Math.round(opt.duration / Math.max(opt.frames, 1) * 1000),
ageOfReport: true, // todo: what is this?
jnyFltrL: [
profile.formatProductsFilter(ctx, opt.products || {})
],
trainPosMode: 'CALC' // todo: what is this? what about realtime?
}
}
}
module.exports = formatRadarReq
@@ -0,0 +1,22 @@
'use strict'
const formatReachableFromReq = (ctx, address) => {
const {profile, opt} = ctx
return {
meth: 'LocGeoReach',
req: {
loc: profile.formatLocation(profile, address, 'address'),
maxDur: opt.maxDuration === null ? -1 : opt.maxDuration,
maxChg: opt.maxTransfers,
date: profile.formatDate(profile, opt.when),
time: profile.formatTime(profile, opt.when),
period: 120, // todo: what is this?
jnyFltrL: [
profile.formatProductsFilter(ctx, opt.products || {})
]
}
}
}
module.exports = formatReachableFromReq
@@ -0,0 +1,16 @@
'use strict'
const formatRectangle = (profile, north, west, south, east) => {
return {
llCrd: {
x: profile.formatCoord(west),
y: profile.formatCoord(south)
},
urCrd: {
x: profile.formatCoord(east),
y: profile.formatCoord(north)
}
}
}
module.exports = formatRectangle
@@ -0,0 +1,25 @@
'use strict'
const formatRefreshJourneyReq = (ctx, refreshToken) => {
// eslint-disable-next-line no-unused-vars
const {profile, opt} = ctx
const req = {
getIST: true, // todo: make an option
getPasslist: !!opt.stopovers,
getPolyline: !!opt.polylines,
getTariff: !!opt.tickets
}
if (profile.refreshJourneyUseOutReconL) {
req.outReconL = [{ctx: refreshToken}]
} else {
req.ctxRecon = refreshToken
}
return {
meth: 'Reconstruction',
req,
}
}
module.exports = formatRefreshJourneyReq
@@ -0,0 +1,34 @@
'use strict'
const formatRemarksReq = (ctx) => {
const {profile, opt} = ctx
const himFltrL = []
// todo: https://github.com/marudor/BahnhofsAbfahrten/blob/95fef0217d01344642dd423457473fe9b8b6056e/src/types/HAFAS/index.ts#L76-L91
if (opt.products) {
himFltrL.push(profile.formatProductsFilter(ctx, opt.products))
}
const req = {
himFltrL,
}
if (profile.remarksGetPolyline) req.getPolyline = !!opt.polylines
// todo: stLoc, dirLoc
// todo: comp, dept, onlyHimId, onlyToday
// todo: dailyB, dailyE
// see https://github.com/marudor/BahnhofsAbfahrten/blob/46a74957d68edc15713112df44e1a25150f5a178/src/types/HAFAS/HimSearch.ts#L3-L18
if (opt.results !== null) req.maxNum = opt.results
if (opt.from !== null) {
req.dateB = profile.formatDate(profile, opt.from)
req.timeB = profile.formatTime(profile, opt.from)
}
if (opt.to !== null) {
req.dateE = profile.formatDate(profile, opt.to)
req.timeE = profile.formatTime(profile, opt.to)
}
return {meth: 'HimSearch', req}
}
module.exports = formatRemarksReq
@@ -0,0 +1,34 @@
'use strict'
const formatStationBoardReq = (ctx, station, type) => {
const {profile, opt} = ctx
const jnyFltrL = [
profile.formatProductsFilter(ctx, opt.products || {})
]
if (opt.line !== null) {
jnyFltrL.push({type: 'LINEID', mode: 'INC', value: opt.line})
}
const req = {
type,
date: profile.formatDate(profile, opt.when),
time: profile.formatTime(profile, opt.when),
stbLoc: station,
dirLoc: opt.direction ? profile.formatStation(opt.direction) : undefined,
jnyFltrL,
dur: opt.duration
}
if (opt.results !== null) {
req.maxJny = opt.results === Infinity ? 10000 : opt.results
}
if (profile.departuresGetPasslist) req.getPasslist = !!opt.stopovers
if (profile.departuresStbFltrEquiv) req.stbFltrEquiv = !opt.includeRelatedStations
return {
meth: 'StationBoard',
req
}
}
module.exports = formatStationBoardReq
@@ -0,0 +1,16 @@
'use strict'
const formatLocationIdentifier = require('./location-identifier')
const formatStation = (id) => {
return {
type: 'S', // station
// todo: name necessary?
lid: formatLocationIdentifier({
A: '1', // station?
L: id
})
}
}
module.exports = formatStation
@@ -0,0 +1,13 @@
'use strict'
const formatStopReq = (ctx, stopRef) => {
return {
// todo: there's also `StationDetails`, are there differences?
meth: 'LocDetails',
req: {
locL: [stopRef]
}
}
}
module.exports = formatStopReq
@@ -0,0 +1,22 @@
'use strict'
const {DateTime, IANAZone} = require('luxon')
const timezones = new WeakMap()
// todo: change to `(profile) => (when) => {}`
const formatTime = (profile, when) => {
let timezone
if (timezones.has(profile)) timezone = timezones.get(profile)
else {
timezone = new IANAZone(profile.timezone)
timezones.set(profile, timezone)
}
return DateTime.fromMillis(+when, {
locale: profile.locale,
zone: timezone
}).toFormat('HHmmss')
}
module.exports = formatTime
@@ -0,0 +1,19 @@
'use strict'
const formatTripReq = ({opt}, id, lineName) => {
return {
cfg: {polyEnc: 'GPA'},
meth: 'JourneyDetails',
req: {
// todo: getTrainComposition
jid: id,
name: lineName,
// HAFAS apparently ignores the date in the trip ID and uses the `date` field.
// Thus, it will find a different trip if you pass the wrong date via `opt.when`.
// date: profile.formatDate(profile, opt.when),
getPolyline: !!opt.polyline
}
}
}
module.exports = formatTripReq
+772
View File
@@ -0,0 +1,772 @@
'use strict'
const isObj = require('lodash/isObject')
const sortBy = require('lodash/sortBy')
const pRetry = require('p-retry')
const omit = require('lodash/omit')
const defaultProfile = require('./lib/default-profile')
const validateProfile = require('./lib/validate-profile')
const {INVALID_REQUEST} = require('./lib/errors')
const sliceLeg = require('./lib/slice-leg')
const isNonEmptyString = str => 'string' === typeof str && str.length > 0
const validateLocation = (loc, name = 'location') => {
if (!isObj(loc)) {
throw new TypeError(name + ' must be an object.')
} else if (loc.type !== 'location') {
throw new TypeError('invalid location object.')
} else if ('number' !== typeof loc.latitude) {
throw new TypeError(name + '.latitude must be a number.')
} else if ('number' !== typeof loc.longitude) {
throw new TypeError(name + '.longitude must be a number.')
}
}
const validateWhen = (when, name = 'when') => {
if (Number.isNaN(+when)) {
throw new TypeError(name + ' is invalid')
}
}
const createClient = (profile, userAgent, opt = {}) => {
profile = Object.assign({}, defaultProfile, profile)
validateProfile(profile)
if ('string' !== typeof userAgent) {
throw new TypeError('userAgent must be a string');
}
const _stationBoard = (station, type, parse, opt = {}) => {
if (isObj(station)) station = profile.formatStation(station.id)
else if ('string' === typeof station) station = profile.formatStation(station)
else throw new TypeError('station must be an object or a string.')
if ('string' !== typeof type || !type) {
throw new TypeError('type must be a non-empty string.')
}
if (!profile.departuresGetPasslist && ('stopovers' in opt)) {
throw new Error('opt.stopovers is not supported by this endpoint')
}
if (!profile.departuresStbFltrEquiv && ('includeRelatedStations' in opt)) {
throw new Error('opt.includeRelatedStations is not supported by this endpoint')
}
opt = Object.assign({
// todo: for arrivals(), this is actually a station it *has already* stopped by
direction: null, // only show departures stopping by this station
line: null, // filter by line ID
duration: 10, // show departures for the next n minutes
results: null, // max. number of results; `null` means "whatever HAFAS wants"
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
linesOfStops: false, // parse & expose lines at the stop/station?
remarks: true, // parse & expose hints & warnings?
stopovers: false, // fetch & parse previous/next stopovers?
// departures at related stations
// e.g. those that belong together on the metro map.
includeRelatedStations: true
}, opt)
opt.when = new Date(opt.when || Date.now())
if (Number.isNaN(+opt.when)) throw new Error('opt.when is invalid')
const req = profile.formatStationBoardReq({profile, opt}, station, type)
// todo [breaking]: return object with realtimeDataUpdatedAt
return profile.request({profile, opt}, userAgent, req)
.then(({res, common}) => {
if (!Array.isArray(res.jnyL)) return []
const ctx = {profile, opt, common, res}
return res.jnyL.map(res => parse(ctx, res))
.sort((a, b) => new Date(a.when) - new Date(b.when)) // todo
})
}
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, 'from')
to = profile.formatLocation(profile, to, 'to')
if (('earlierThan' in opt) && ('laterThan' in opt)) {
throw new TypeError('opt.earlierThan and opt.laterThan are mutually exclusive.')
}
if (('departure' in opt) && ('arrival' in opt)) {
throw new TypeError('opt.departure and opt.arrival are mutually exclusive.')
}
let journeysRef = null
if ('earlierThan' in opt) {
if (!isNonEmptyString(opt.earlierThan)) {
throw new TypeError('opt.earlierThan must be a non-empty string.')
}
if (('departure' in opt) || ('arrival' in opt)) {
throw new TypeError('opt.earlierThan and opt.departure/opt.arrival are mutually exclusive.')
}
journeysRef = opt.earlierThan
}
if ('laterThan' in opt) {
if (!isNonEmptyString(opt.laterThan)) {
throw new TypeError('opt.laterThan must be a non-empty string.')
}
if (('departure' in opt) || ('arrival' in opt)) {
throw new TypeError('opt.laterThan and opt.departure/opt.arrival are mutually exclusive.')
}
journeysRef = opt.laterThan
}
opt = Object.assign({
results: null, // number of journeys – `null` means "whatever HAFAS returns"
via: null, // let journeys pass this station?
stopovers: false, // return stations on the way?
transfers: -1, // maximum nr of transfers
transferTime: 0, // minimum time for a single transfer in minutes
// todo: does this work with every endpoint?
accessibility: 'none', // 'none', 'partial' or 'complete'
bike: false, // only bike-friendly journeys
walkingSpeed: 'normal', // 'slow', 'normal', 'fast'
// Consider walking to nearby stations at the beginning of a journey?
startWithWalking: true,
tickets: false, // return tickets?
polylines: false, // return leg shapes?
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
remarks: true, // parse & expose hints & warnings?
scheduledDays: false
}, opt)
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 TypeError('opt.departure is invalid')
} else if (opt.arrival !== undefined && opt.arrival !== null) {
if (!profile.journeysOutFrwd) {
throw new Error('opt.arrival is unsupported')
}
when = new Date(opt.arrival)
if (Number.isNaN(+when)) throw new TypeError('opt.arrival is invalid')
outFrwd = false
}
const filters = [
profile.formatProductsFilter({profile}, opt.products || {})
]
if (
opt.accessibility &&
profile.filters &&
profile.filters.accessibility &&
profile.filters.accessibility[opt.accessibility]
) {
filters.push(profile.filters.accessibility[opt.accessibility])
}
if (!['slow','normal','fast'].includes(opt.walkingSpeed)) {
throw new Error('opt.walkingSpeed must be one of these values: "slow", "normal", "fast".')
}
const gisFltrL = []
if (profile.journeysWalkingSpeed) {
gisFltrL.push({
meta: 'foot_speed_' + opt.walkingSpeed,
mode: 'FB',
type: 'M'
})
}
const query = {
getPasslist: !!opt.stopovers,
maxChg: opt.transfers,
minChgTime: opt.transferTime,
depLocL: [from],
viaLocL: opt.via ? [{loc: opt.via}] : [],
arrLocL: [to],
jnyFltrL: filters,
gisFltrL,
getTariff: !!opt.tickets,
// todo: this is actually "take additional stations nearby the given start and destination station into account"
// see rest.exe docs
ushrp: !!opt.startWithWalking,
getPT: true, // todo: what is this?
getIV: false, // todo: walk & bike as alternatives?
getPolyline: !!opt.polylines
// todo: `getConGroups: false` what is this?
// todo: what is getEco, fwrd?
}
if (journeysRef) query.ctxScr = journeysRef
else {
query.outDate = profile.formatDate(profile, when)
query.outTime = profile.formatTime(profile, when)
}
if (opt.results !== null) query.numF = opt.results
if (profile.journeysOutFrwd) query.outFrwd = outFrwd
return profile.request({profile, opt}, userAgent, {
cfg: {polyEnc: 'GPA'},
meth: 'TripSearch',
req: profile.transformJourneysQuery({profile, opt}, query)
})
.then(({res, common}) => {
if (!Array.isArray(res.outConL)) return []
// todo: outConGrpL
const ctx = {profile, opt, common, res}
const journeys = res.outConL
.map(j => profile.parseJourney(ctx, j))
return {
earlierRef: res.outCtxScrB,
laterRef: res.outCtxScrF,
journeys,
// todo [breaking]: rename to realtimeDataUpdatedAt
realtimeDataFrom: res.planrtTS ? parseInt(res.planrtTS) : null,
}
})
}
const refreshJourney = (refreshToken, opt = {}) => {
if ('string' !== typeof refreshToken || !refreshToken) {
throw new TypeError('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? (not supported by all endpoints)
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
remarks: true // parse & expose hints & warnings?
}, opt)
const req = profile.formatRefreshJourneyReq({profile, opt}, refreshToken)
return profile.request({profile, opt}, userAgent, req)
.then(({res, common}) => {
if (!Array.isArray(res.outConL) || !res.outConL[0]) {
const err = new Error('invalid response')
// technically this is not a HAFAS error
// todo: find a different flag with decent DX
err.isHafasError = true
throw err
}
const ctx = {profile, opt, common, res}
return {
// todo [breaking]: rename to realtimeDataUpdatedAt
realtimeDataFrom: res.planrtTS ? parseInt(res.planrtTS) : null,
...profile.parseJourney(ctx, res.outConL[0])
}
})
}
// Although the DB Navigator app passes the *first* stopover of the trip
// (instead of the previous one), it seems to work with the previous one as well.
const journeysFromTrip = async (fromTripId, previousStopover, to, opt = {}) => {
if (!isNonEmptyString(fromTripId)) {
throw new Error('fromTripId must be a non-empty string.')
}
if ('string' === typeof to) {
to = profile.formatStation(to)
} else if (isObj(to) && (to.type === 'station' || to.type === 'stop')) {
to = profile.formatStation(to.id)
} else throw new Error('to must be a valid stop or station.')
if (!isObj(previousStopover)) throw new Error('previousStopover must be an object.')
let prevStop = previousStopover.stop
if (isObj(prevStop)) {
prevStop = profile.formatStation(prevStop.id)
} else if ('string' === typeof prevStop) {
prevStop = profile.formatStation(prevStop)
} else throw new Error('previousStopover.stop must be a valid stop or station.')
let depAtPrevStop = previousStopover.departure || previousStopover.plannedDeparture
if (!isNonEmptyString(depAtPrevStop)) {
throw new Error('previousStopover.(planned)departure must be a string')
}
depAtPrevStop = Date.parse(depAtPrevStop)
if (Number.isNaN(depAtPrevStop)) {
throw new Error('previousStopover.(planned)departure is invalid')
}
if (depAtPrevStop > Date.now()) {
throw new Error('previousStopover.(planned)departure must be in the past')
}
opt = Object.assign({
stopovers: false, // return stations on the way?
transferTime: 0, // minimum time for a single transfer in minutes
accessibility: 'none', // 'none', 'partial' or 'complete'
tickets: false, // return tickets?
polylines: false, // return leg shapes?
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
remarks: true, // parse & expose hints & warnings?
}, opt)
// make clear that `departure`/`arrival`/`when` are not supported
if (opt.departure) throw new Error('journeysFromTrip + opt.departure is not supported by HAFAS.')
if (opt.arrival) throw new Error('journeysFromTrip + opt.arrival is not supported by HAFAS.')
if (opt.when) throw new Error('journeysFromTrip + opt.when is not supported by HAFAS.')
const filters = [
profile.formatProductsFilter({profile}, opt.products || {})
]
if (
opt.accessibility &&
profile.filters &&
profile.filters.accessibility &&
profile.filters.accessibility[opt.accessibility]
) {
filters.push(profile.filters.accessibility[opt.accessibility])
}
// todo: support walking speed filter
// todo: are these supported?
// - getPT
// - getIV
// - trfReq
// features from `journeys()` not supported here:
// - `maxChg`: maximum nr of transfers
// - `bike`: only bike-friendly journeys
// - `numF`: how many journeys?
// - `via`: let journeys pass this station
// todo: find a way to support them
const query = {
// https://github.com/marudor/BahnhofsAbfahrten/blob/49ebf8b36576547112e61a6273bee770f0769660/packages/types/HAFAS/SearchOnTrip.ts#L16-L30
// todo: support search by `journey.refreshToken` (a.k.a. `ctxRecon`) via `sotMode: RC`?
sotMode: 'JI', // seach by trip ID (a.k.a. "JID")
jid: fromTripId,
locData: { // when & where the trip has been entered
loc: prevStop,
type: 'DEP', // todo: are there other values?
date: profile.formatDate(profile, depAtPrevStop),
time: profile.formatTime(profile, depAtPrevStop)
},
arrLocL: [to],
jnyFltrL: filters,
getPasslist: !!opt.stopovers,
getPolyline: !!opt.polylines,
minChgTime: opt.transferTime,
getTariff: !!opt.tickets,
}
const {res, common} = await profile.request({profile, opt}, userAgent, {
cfg: {polyEnc: 'GPA'},
meth: 'SearchOnTrip',
req: query,
})
if (!Array.isArray(res.outConL)) return []
const ctx = {profile, opt, common, res}
// todo [breaking]: return object with realtimeDataUpdatedAt
return res.outConL
.map(rawJourney => profile.parseJourney(ctx, rawJourney))
.map((journey) => {
// For the first (transit) leg, HAFAS sometimes returns *all* past
// stopovers of the trip, even though it should only return stopovers
// between the specified `depAtPrevStop` and the arrival at the
// interchange station. We slice the leg accordingly.
const fromLegI = journey.legs.findIndex(l => l.tripId === fromTripId)
if (fromLegI < 0) return journey
const fromLeg = journey.legs[fromLegI]
return {
...journey,
legs: [
...journey.legs.slice(0, fromLegI),
sliceLeg(fromLeg, previousStopover.stop, fromLeg.destination),
...journey.legs.slice(fromLegI + 2),
],
}
})
}
const locations = (query, opt = {}) => {
if (!isNonEmptyString(query)) {
throw new TypeError('query must be a non-empty string.')
}
opt = Object.assign({
fuzzy: true, // find only exact matches?
results: 5, // how many search results?
stops: true, // return stops/stations?
addresses: true,
poi: true, // points of interest
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
linesOfStops: false // parse & expose lines at each stop/station?
}, opt)
const req = profile.formatLocationsReq({profile, opt}, query)
return profile.request({profile, opt}, userAgent, req)
.then(({res, common}) => {
if (!res.match || !Array.isArray(res.match.locL)) return []
const ctx = {profile, opt, common, res}
return res.match.locL.map(loc => profile.parseLocation(ctx, loc))
})
}
const stop = (stop, opt = {}) => {
if ('object' === typeof stop) stop = profile.formatStation(stop.id)
else if ('string' === typeof stop) stop = profile.formatStation(stop)
else throw new TypeError('stop must be an object or a string.')
opt = Object.assign({
linesOfStops: false, // parse & expose lines at the stop/station?
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
remarks: true, // parse & expose hints & warnings?
}, opt)
const req = profile.formatStopReq({profile, opt}, stop)
return profile.request({profile, opt}, userAgent, req)
.then(({res, common}) => {
if (!res || !Array.isArray(res.locL) || !res.locL[0]) {
// todo: proper stack trace?
// todo: DRY with lib/request.js
const err = new Error('response has no stop')
// technically this is not a HAFAS error
// todo: find a different flag with decent DX
err.isHafasError = true
err.code = INVALID_REQUEST
throw err
}
const ctx = {profile, opt, res, common}
return profile.parseLocation(ctx, res.locL[0])
})
}
const nearby = (location, opt = {}) => {
validateLocation(location, 'location')
opt = Object.assign({
results: 8, // maximum number of results
distance: null, // maximum walking distance in meters
poi: false, // return points of interest?
stops: true, // return stops/stations?
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
linesOfStops: false // parse & expose lines at each stop/station?
}, opt)
const req = profile.formatNearbyReq({profile, opt}, location)
return profile.request({profile, opt}, userAgent, req)
.then(({common, res}) => {
if (!Array.isArray(res.locL)) return []
// todo: parse `.dur` – walking duration?
const ctx = {profile, opt, common, res}
const results = res.locL.map(loc => profile.parseNearby(ctx, loc))
return Number.isInteger(opt.results) ? results.slice(0, opt.results) : results
})
}
const trip = (id, lineName, opt = {}) => {
if (!isNonEmptyString(id)) {
throw new TypeError('id must be a non-empty string.')
}
if (!isNonEmptyString(lineName)) {
throw new TypeError('lineName must be a non-empty string.')
}
opt = Object.assign({
stopovers: true, // return stations on the way?
polyline: false, // return a track shape?
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
remarks: true // parse & expose hints & warnings?
}, opt)
const req = profile.formatTripReq({profile, opt}, id, lineName)
// todo [breaking]: return object with realtimeDataUpdatedAt
return profile.request({profile, opt}, userAgent, req)
.then(({common, res}) => {
const ctx = {profile, opt, common, res}
return profile.parseTrip(ctx, res.journey)
})
}
// todo [breaking]: rename to trips()?
const tripsByName = (lineNameOrFahrtNr = '*', opt = {}) => {
if (!isNonEmptyString(lineNameOrFahrtNr)) {
throw new TypeError('lineNameOrFahrtNr must be a non-empty string.')
}
opt = Object.assign({
when: null,
fromWhen: null, untilWhen: null,
onlyCurrentlyRunning: true,
products: {},
currentlyStoppingAt: null,
lineName: null,
operatorNames: null,
additionalFilters: [], // undocumented
}, opt)
const req = {
// fields: https://github.com/marudor/BahnhofsAbfahrten/blob/f619e754f212980261eb7e2b151cd73ba0213da8/packages/types/HAFAS/JourneyMatch.ts#L4-L23
input: lineNameOrFahrtNr,
onlyCR: opt.onlyCurrentlyRunning,
jnyFltrL: [
profile.formatProductsFilter({profile}, opt.products),
],
// todo: passing `tripId` yields a `CGI_READ_FAILED` error
// todo: passing a stop ID as `extId` yields a `PARAMETER` error
// todo: `onlyRT: true` reduces the number of results, but filters recent trips 🤔
// todo: `onlyTN: true` yields a `NO_MATCH` error
// todo: useAeqi
}
if (opt.when !== null) {
req.date = profile.formatDate(profile, new Date(opt.when))
req.time = profile.formatTime(profile, new Date(opt.when))
}
// todo: fromWhen doesn't work yet, but untilWhen does
if (opt.fromWhen !== null) {
req.dateB = profile.formatDate(profile, new Date(opt.fromWhen))
req.timeB = profile.formatTime(profile, new Date(opt.fromWhen))
}
if (opt.untilWhen !== null) {
req.dateE = profile.formatDate(profile, new Date(opt.untilWhen))
req.timeE = profile.formatTime(profile, new Date(opt.untilWhen))
}
const filter = (mode, type, value) => ({mode, type, value})
if (opt.currentlyStoppingAt !== null) {
if (!isNonEmptyString(opt.currentlyStoppingAt)) {
throw new TypeError('opt.currentlyStoppingAt must be a non-empty string.')
}
req.jnyFltrL.push(filter('INC', 'STATIONS', opt.currentlyStoppingAt))
}
if (opt.lineName !== null) {
if (!isNonEmptyString(opt.lineName)) {
throw new TypeError('opt.lineName must be a non-empty string.')
}
// todo: does this target `line` or `lineId`?
req.jnyFltrL.push(filter('INC', 'LINE', opt.lineName))
}
if (opt.operatorNames !== null) {
if (
!Array.isArray(opt.operatorNames)
|| opt.operatorNames.length === 0
|| !opt.operatorNames.every(isNonEmptyString)
) {
throw new TypeError('opt.operatorNames must be an array of non-empty strings.')
}
// todo: is the an escaping mechanism for ","
req.jnyFltrL.push(filter('INC', 'OP', opt.operatorNames.join(',')))
}
req.jnyFltrL = [...req.jnyFltrL, ...opt.additionalFilters]
// todo [breaking]: return object with realtimeDataUpdatedAt
return profile.request({profile, opt}, userAgent, {
cfg: {polyEnc: 'GPA'},
meth: 'JourneyMatch',
req,
})
// todo [breaking]: catch `NO_MATCH` errors, return []
.then(({res, common}) => {
const ctx = {profile, opt, common, res}
return res.jnyL.map(t => profile.parseTrip(ctx, t))
})
}
const radar = ({north, west, south, east}, opt) => {
if ('number' !== typeof north) throw new TypeError('north must be a number.')
if ('number' !== typeof west) throw new TypeError('west must be a number.')
if ('number' !== typeof south) throw new TypeError('south must be a number.')
if ('number' !== typeof east) throw new TypeError('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: true, // return a track shape for each vehicle?
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
}, opt || {})
opt.when = new Date(opt.when || Date.now())
if (Number.isNaN(+opt.when)) throw new TypeError('opt.when is invalid')
const req = profile.formatRadarReq({profile, opt}, north, west, south, east)
// todo [breaking]: return object with realtimeDataUpdatedAt
return profile.request({profile, opt}, userAgent, req)
.then(({res, common}) => {
if (!Array.isArray(res.jnyL)) return []
const ctx = {profile, opt, common, res}
return res.jnyL.map(m => profile.parseMovement(ctx, m))
})
}
const reachableFrom = (address, opt = {}) => {
validateLocation(address, 'address')
opt = Object.assign({
when: Date.now(),
maxTransfers: 5, // maximum of 5 transfers
maxDuration: 20, // maximum travel duration in minutes, pass `null` for infinite
products: {},
subStops: true, // parse & expose sub-stops of stations?
entrances: true, // parse & expose entrances of stops/stations?
polylines: false, // return leg shapes?
}, opt)
if (Number.isNaN(+opt.when)) throw new TypeError('opt.when is invalid')
const req = profile.formatReachableFromReq({profile, opt}, address)
const refetch = () => {
return profile.request({profile, opt}, userAgent, req)
.then(({res, common}) => {
if (!Array.isArray(res.posL)) {
const err = new Error('invalid response')
err.shouldRetry = true
throw err
}
const byDuration = []
let i = 0, lastDuration = NaN
for (const pos of sortBy(res.posL, 'dur')) {
const loc = common.locations[pos.locX]
if (!loc) continue
if (pos.dur !== lastDuration) {
lastDuration = pos.dur
i = byDuration.length
byDuration.push({
duration: pos.dur,
stations: [loc]
})
} else {
byDuration[i].stations.push(loc)
}
}
// todo [breaking]: return object with realtimeDataUpdatedAt
return byDuration
})
}
return pRetry(refetch, {
retries: 3,
factor: 2,
minTimeout: 2 * 1000
})
}
const remarks = async (opt = {}) => {
opt = {
results: 100, // maximum number of remarks
// filter by time
from: Date.now(),
to: null,
products: null, // filter by affected products
polylines: false, // return leg shapes? (not supported by all endpoints)
...opt
}
if (opt.from !== null) {
opt.from = new Date(opt.from)
validateWhen(opt.from, 'opt.from')
}
if (opt.to !== null) {
opt.to = new Date(opt.to)
validateWhen(opt.to, 'opt.to')
}
const req = profile.formatRemarksReq({profile, opt})
const {
res, common,
} = await profile.request({profile, opt}, userAgent, req)
// todo [breaking]: return object with realtimeDataUpdatedAt
const ctx = {profile, opt, common, res}
return (res.msgL || [])
.map(w => profile.parseWarning(ctx, w))
}
const lines = async (query, opt = {}) => {
if (!isNonEmptyString(query)) {
throw new TypeError('query must be a non-empty string.')
}
const req = profile.formatLinesReq({profile, opt}, query)
const {
res, common,
} = await profile.request({profile, opt}, userAgent, req)
// todo [breaking]: return object with realtimeDataUpdatedAt
if (!Array.isArray(res.lineL)) return []
const ctx = {profile, opt, common, res}
return res.lineL.map(l => {
const parseDirRef = i => (res.common.dirL[i] || {}).txt || null
return {
...omit(l.line, ['id', 'fahrtNr']),
id: l.lineId,
// todo: what is locX?
directions: Array.isArray(l.dirRefL)
? l.dirRefL.map(parseDirRef)
: null,
trips: Array.isArray(l.jnyL)
? l.jnyL.map(t => profile.parseTrip(ctx, t))
: null,
}
})
}
const serverInfo = async (opt = {}) => {
const {res, common} = await profile.request({profile, opt}, userAgent, {
meth: 'ServerInfo',
req: {}
})
const ctx = {profile, opt, common, res}
return {
timetableStart: res.fpB || null,
timetableEnd: res.fpE || null,
serverTime: res.sD && res.sT
? profile.parseDateTime(ctx, res.sD, res.sT)
: null,
realtimeDataUpdatedAt: res.planrtTS
? parseInt(res.planrtTS)
: null,
}
}
const client = {
departures,
arrivals,
journeys,
locations,
stop,
nearby,
serverInfo,
}
if (profile.trip) client.trip = trip
if (profile.radar) client.radar = radar
if (profile.refreshJourney) client.refreshJourney = refreshJourney
if (profile.journeysFromTrip) client.journeysFromTrip = journeysFromTrip
if (profile.reachableFrom) client.reachableFrom = reachableFrom
if (profile.tripsByName) client.tripsByName = tripsByName
if (profile.remarks !== false) client.remarks = remarks
if (profile.lines !== false) client.lines = lines
Object.defineProperty(client, 'profile', {value: profile})
return client
}
module.exports = createClient
@@ -0,0 +1,121 @@
'use strict'
const request = require('../lib/request')
const formatStationBoardReq = require('../format/station-board-req')
const formatLocationsReq = require('../format/locations-req')
const formatStopReq = require('../format/stop-req')
const formatNearbyReq = require('../format/nearby-req')
const formatTripReq = require('../format/trip-req')
const formatRadarReq = require('../format/radar-req')
const formatReachableFromReq = require('../format/reachable-from-req')
const formatRefreshJourneyReq = require('../format/refresh-journey-req')
const formatRemarksReq = require('../format/remarks-req')
const formatLinesReq = require('../format/lines-req')
const parseDateTime = require('../parse/date-time')
const parsePlatform = require('../parse/platform')
const parseProductsBitmask = require('../parse/products-bitmask')
const parseIcon = require('../parse/icon')
const parseWhen = require('../parse/when')
const parseDeparture = require('../parse/departure')
const parseArrival = require('../parse/arrival')
const parseTrip = require('../parse/trip')
const parseJourneyLeg = require('../parse/journey-leg')
const parseJourney = require('../parse/journey')
const parseLine = require('../parse/line')
const parseLocation = require('../parse/location')
const parseCommon = require('../parse/common')
const parsePolyline = require('../parse/polyline')
const parseMovement = require('../parse/movement')
const parseNearby = require('../parse/nearby')
const parseOperator = require('../parse/operator')
const parseHint = require('../parse/hint')
const parseWarning = require('../parse/warning')
const parseStopover = require('../parse/stopover')
const formatAddress = require('../format/address')
const formatCoord = require('../format/coord')
const formatDate = require('../format/date')
const formatLocationFilter = require('../format/location-filter')
const formatProductsFilter = require('../format/products-filter')
const formatPoi = require('../format/poi')
const formatStation = require('../format/station')
const formatTime = require('../format/time')
const formatLocation = require('../format/location')
const formatRectangle = require('../format/rectangle')
const filters = require('../format/filters')
const id = (ctx, x) => x
const defaultProfile = {
request,
transformReqBody: id,
transformReq: id,
salt: null,
addChecksum: false,
addMicMac: false,
formatStationBoardReq,
formatLocationsReq,
formatStopReq,
formatNearbyReq,
formatTripReq,
formatRadarReq,
formatReachableFromReq,
formatRefreshJourneyReq,
formatRemarksReq,
formatLinesReq,
transformJourneysQuery: id,
parseDateTime,
parsePlatform,
parseProductsBitmask,
parseIcon,
parseWhen,
parseDeparture,
parseArrival,
parseTrip,
parseJourneyLeg,
parseJourney,
parseLine,
parseStationName: (_, name) => name,
parseLocation,
parseCommon,
parsePolyline,
parseMovement,
parseNearby,
parseOperator,
parseHint,
parseWarning,
parseStopover,
formatAddress,
formatCoord,
formatDate,
formatLocationFilter,
formatProductsFilter,
formatPoi,
formatStation,
formatTime,
formatLocation,
formatRectangle,
filters,
journeysOutFrwd: true, // `journeys()` method: support for `outFrwd` field?
// todo: https://github.com/KDE/kpublictransport/commit/c7c54304160d8f22eab0c91812a107aca82304b7
departuresGetPasslist: true, // `departures()` method: support for `getPasslist` field?
departuresStbFltrEquiv: true, // `departures()` method: support for `stbFltrEquiv` field?
trip: false,
radar: false,
refreshJourney: true,
// refreshJourney(): use `outReconL[]` instead of `ctxRecon`?
refreshJourneyUseOutReconL: false,
tripsByName: true,
remarks: true,
// `remarks()` method: support for `getPolyline` field?
remarksGetPolyline: true, // `remarks()` method: support for `getPolyline` field?
lines: true,
}
module.exports = defaultProfile
@@ -0,0 +1,229 @@
'use strict'
const ACCESS_DENIED = 'ACCESS_DENIED'
const INVALID_REQUEST = 'INVALID_REQUEST'
const NOT_FOUND = 'NOT_FOUND'
const SERVER_ERROR = 'SERVER_ERROR'
// https://gist.github.com/derhuerst/79d49c0f04c1c192a5d15756e5af575f/edit
// todo:
// `code: 'METHOD_NA', message: 'HCI Service: service method disabled'`
// "err": "PARAMETER", "errTxt": "HCI Service: parameter invalid"
const byErrorCode = Object.assign(Object.create(null), {
H_UNKNOWN: {
isServer: false,
code: SERVER_ERROR,
message: 'unknown internal error',
statusCode: 500,
},
AUTH: {
isClient: true,
code: ACCESS_DENIED,
message: 'invalid or missing authentication data',
statusCode: 401
},
R0001: {
isClient: true,
code: INVALID_REQUEST,
message: 'unknown method',
statusCode: 400
},
R0002: {
isClient: true,
code: INVALID_REQUEST,
message: 'invalid or missing request parameters',
statusCode: 400
},
R0007: {
isServer: true,
code: SERVER_ERROR,
message: 'internal communication error',
statusCode: 500
},
R5000: {
isClient: true,
code: ACCESS_DENIED,
message: 'access denied',
statusCode: 401
},
S1: {
isServer: true,
code: SERVER_ERROR,
message: 'journeys search: a connection to the backend server couldn\'t be established',
statusCode: 503
},
LOCATION: {
isClient: true,
code: INVALID_REQUEST,
message: 'location/stop not found',
statusCode: 400
},
H390: {
isClient: true,
code: INVALID_REQUEST,
message: 'journeys search: departure/arrival station replaced',
statusCode: 400
},
H410: {
// todo: or is it a client error?
// todo: statusCode?
isServer: true,
code: SERVER_ERROR,
message: 'journeys search: incomplete response due to timetable change'
},
H455: {
isClient: true,
code: INVALID_REQUEST,
message: 'journeys search: prolonged stop',
statusCode: 400
},
H460: {
isClient: true,
code: INVALID_REQUEST,
message: 'journeys search: stop(s) passed multiple times',
statusCode: 400
},
H500: {
isClient: true,
code: INVALID_REQUEST,
message: 'journeys search: too many trains, connection is not complete',
statusCode: 400
},
H890: {
isClient: true,
code: NOT_FOUND,
message: 'journeys search unsuccessful',
statusCode: 404
},
H891: {
isClient: true,
code: NOT_FOUND,
message: 'journeys search: no route found, try with an intermediate stations',
statusCode: 404
},
H892: {
isClient: true,
code: INVALID_REQUEST,
message: 'journeys search: query too complex, try less intermediate stations',
statusCode: 400
},
H895: {
isClient: true,
code: INVALID_REQUEST,
message: 'journeys search: departure & arrival are too near',
statusCode: 400
},
H899: {
// todo: or is it a client error?
// todo: statusCode?
isServer: true,
code: SERVER_ERROR,
message: 'journeys search unsuccessful or incomplete due to timetable change'
},
H900: {
// todo: or is it a client error?
// todo: statusCode?
isServer: true,
code: SERVER_ERROR,
message: 'journeys search unsuccessful or incomplete due to timetable change'
},
H9220: {
isClient: true,
code: NOT_FOUND,
message: 'journeys search: no stations found close to the address',
statusCode: 400
},
H9230: {
isServer: true,
code: SERVER_ERROR,
message: 'journeys search: an internal error occured',
statusCode: 500
},
H9240: {
isClient: true,
code: NOT_FOUND,
message: 'journeys search unsuccessful',
statusCode: 404
},
H9250: {
isServer: true,
code: SERVER_ERROR,
message: 'journeys search: leg query interrupted',
statusCode: 500
},
H9260: {
isClient: true,
code: INVALID_REQUEST,
message: 'journeys search: unknown departure station',
statusCode: 400
},
H9280: {
isClient: true,
code: INVALID_REQUEST,
message: 'journeys search: unknown intermediate station',
statusCode: 400
},
H9300: {
isClient: true,
code: INVALID_REQUEST,
message: 'journeys search: unknown arrival station',
statusCode: 400
},
H9320: {
isClient: true,
code: INVALID_REQUEST,
message: 'journeys search: the input is incorrect or incomplete',
statusCode: 400
},
H9360: {
isClient: true,
code: INVALID_REQUEST,
message: 'journeys search: invalid date/time',
statusCode: 400
},
H9380: {
isClient: true,
code: INVALID_REQUEST,
message: 'journeys search: departure/arrival/intermediate station defined more than once',
statusCode: 400
},
SQ001: {
isServer: true,
code: SERVER_ERROR,
message: 'no departures/arrivals data available',
statusCode: 503
},
SQ005: {
isClient: true,
code: NOT_FOUND,
message: 'no trips found',
statusCode: 404
},
TI001: {
isServer: true,
code: SERVER_ERROR,
message: 'no trip info available',
statusCode: 503
}
})
const addErrorInfo = (err, errorCode, errorText, responseId) => {
if (byErrorCode[errorCode]) {
Object.assign(err, byErrorCode[errorCode])
if (errorCode) err.hafasErrorCode = errorCode
if (errorText) err.hafasErrorMessage = errorText
} else {
err.code = errorCode || null
err.message = errorText || errorCode || null
err.responseId = responseId || null
}
}
module.exports = {
ACCESS_DENIED,
INVALID_REQUEST,
NOT_FOUND,
SERVER_ERROR,
byErrorCode,
addErrorInfo,
}
@@ -0,0 +1,23 @@
'use strict'
const objectScan = require('object-scan')
const createFindInTree = (needles) => {
const scanner = objectScan(needles, {
filterFn: ({ value, parents, matchedBy, context }) => {
matchedBy.forEach((needle) => {
context[needle].push([value, parents])
})
}
})
return (haystack) => {
const context = Object.create(null)
needles.forEach((needle) => {
context[needle] = []
})
return scanner(haystack, context)
}
}
module.exports = createFindInTree
@@ -0,0 +1,25 @@
'use strict'
// For any type of "thing to parse", there's >=1 parse functions.
// By composing custom parse function(s) with the default ones, one
// can customize the behaviour of hafas-client. Profiles extensively
// use this mechanism.
// Each parse function has the following signature:
// ({opt, profile, common, res}, ...raw) => newParsed
// Compose a new/custom parse function with the old/existing parse
// function, so that the new fn will be called with the output of the
// old fn.
const parseHook = (oldParse, newParse) => {
return (ctx, ...args) => {
return newParse({
...ctx,
parsed: oldParse({...ctx, parsed: {}}, ...args)
}, ...args)
}
}
module.exports = {
parseHook
}
@@ -0,0 +1,158 @@
'use strict'
const DEV = process.env.NODE_ENV === 'dev'
const DEBUG = /(^|,)hafas-client(,|$)/.test(process.env.DEBUG || '')
const ProxyAgent = require('https-proxy-agent')
const {isIP} = require('net')
const {Agent: HttpsAgent} = require('https')
const roundRobin = require('@derhuerst/round-robin-scheduler')
const {randomBytes} = require('crypto')
const createHash = require('create-hash')
const pick = require('lodash/pick')
const captureStackTrace = DEV ? require('capture-stack-trace') : () => {}
const {stringify} = require('qs')
const Promise = require('pinkie-promise')
const {fetch} = require('fetch-ponyfill')({Promise})
const {parse: parseContentType} = require('content-type')
const {addErrorInfo} = require('./errors')
const proxyAddress = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || null
const localAddresses = process.env.LOCAL_ADDRESS || null
if (proxyAddress && localAddresses) {
console.error('Both env vars HTTPS_PROXY/HTTP_PROXY and LOCAL_ADDRESS are not supported.')
process.exit(1)
}
let getAgent = () => null
if (proxyAddress) {
const agent = new ProxyAgent(proxyAddress)
getAgent = () => agent
} else if (localAddresses) {
const agents = process.env.LOCAL_ADDRESS.split(',')
.map((addr) => {
const family = isIP(addr)
if (family === 0) throw new Error('invalid local address:' + addr)
return new HttpsAgent({localAddress: addr, family})
})
const pool = roundRobin(agents)
getAgent = () => pool.get()
}
const id = randomBytes(6).toString('hex')
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 request = (ctx, userAgent, reqData) => {
const {profile, opt} = ctx
const body = profile.transformReqBody(ctx, {
// todo: is it `eng` actually?
// RSAG has `deu` instead of `de`
lang: opt.language || profile.defaultLanguage || 'en',
svcReqL: [reqData]
})
Object.assign(body, pick(profile, [
'client', // client identification
'ext', // ?
'ver', // HAFAS protocol version
'auth', // static authentication
]))
if (DEBUG) console.error(JSON.stringify(body))
const req = profile.transformReq(ctx, {
agent: getAgent(),
method: 'post',
// todo: CORS? referrer policy?
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
'Accept-Encoding': 'gzip, br, deflate',
'Accept': 'application/json',
'user-agent': randomizeUserAgent(userAgent)
},
redirect: 'follow',
query: {}
})
if (profile.addChecksum || profile.addMicMac) {
if (!Buffer.isBuffer(profile.salt) && 'string' !== typeof profile.salt) {
throw new TypeError('profile.salt must be a Buffer or a string.')
}
// Buffer.from(buf, 'hex') just returns buf
const salt = Buffer.from(profile.salt, 'hex')
if (profile.addChecksum) {
const checksum = md5(Buffer.concat([
Buffer.from(req.body, 'utf8'),
salt,
]))
req.query.checksum = checksum.toString('hex')
}
if (profile.addMicMac) {
const mic = md5(Buffer.from(req.body, 'utf8'))
req.query.mic = mic.toString('hex')
const micAsHex = Buffer.from(mic.toString('hex'), 'utf8')
const mac = md5(Buffer.concat([micAsHex, salt]))
req.query.mac = mac.toString('hex')
}
}
const url = profile.endpoint + '?' + stringify(req.query)
// Async stack traces are not supported everywhere yet, so we create our own.
const err = new Error()
err.isHafasError = true // todo: rename to `isHafasClientError`
err.request = req.body // todo: commit as bugfix
err.url = url
captureStackTrace(err)
return fetch(url, req)
.then((res) => {
err.statusCode = res.status
if (!res.ok) {
err.message = res.statusText
throw err
}
let cType = res.headers.get('content-type')
if (cType) {
const {type} = parseContentType(cType)
if (type !== 'application/json') {
const err = new Error('invalid response content-type: ' + cType)
err.response = res
throw err
}
}
return res.json()
})
.then((b) => {
if (DEBUG) console.error(JSON.stringify(b))
if (b.err && b.err !== 'OK') {
addErrorInfo(err, b.err, b.errTxt, b.id)
throw err
}
if (!b.svcResL || !b.svcResL[0]) {
err.message = 'invalid response'
throw err
}
if (b.svcResL[0].err !== 'OK') {
addErrorInfo(err, b.svcResL[0].err, b.svcResL[0].errTxt, b.id)
throw err
}
const res = b.svcResL[0].res
return {
res,
common: profile.parseCommon({...ctx, res})
}
})
}
module.exports = request
@@ -0,0 +1,47 @@
'use strict'
const findById = (needle) => {
const needleStopId = needle.id
const needleStationId = needle.station ? needle.station.id : null
return (stop) => {
if (needleStopId === stop.id) return true
const stationId = stop.station ? stop.station.id : null
if (needleStationId && stationId && needleStationId === stationId) return true
// todo: `needleStationId === stop.id`? `needleStopId === stationId`?
return false
}
}
const sliceLeg = (leg, from, to) => {
if (!Array.isArray(leg.stopovers)) throw new Error('leg.stopovers must be an array.')
const stops = leg.stopovers.map(st => st.stop)
const fromI = stops.findIndex(findById(from))
if (fromI === -1) throw new Error('from not found in stopovers')
const fromStopover = leg.stopovers[fromI]
const toI = stops.findIndex(findById(to))
if (toI === -1) throw new Error('to not found in stopovers')
const toStopover = leg.stopovers[toI]
if (fromI === 0 && toI === leg.stopovers.length - 1) return leg
const newLeg = Object.assign({}, leg)
newLeg.stopovers = leg.stopovers.slice(fromI, toI + 1)
newLeg.origin = fromStopover.stop
newLeg.departure = fromStopover.departure
newLeg.departureDelay = fromStopover.departureDelay
newLeg.scheduledDeparture = fromStopover.scheduledDeparture
newLeg.departurePlatform = fromStopover.departurePlatform
newLeg.destination = toStopover.stop
newLeg.arrival = toStopover.arrival
newLeg.arrivalDelay = toStopover.arrivalDelay
newLeg.scheduledArrival = toStopover.scheduledArrival
newLeg.arrivalPlatform = toStopover.arrivalPlatform
return newLeg
}
module.exports = sliceLeg
@@ -0,0 +1,95 @@
'use strict'
const types = {
locale: 'string',
timezone: 'string',
request: 'function',
transformReq: 'function',
transformReqBody: 'function',
formatStationBoardReq: 'function',
formatLocationsReq: 'function',
formatStopReq: 'function',
formatNearbyReq: 'function',
formatTripReq: 'function',
formatRadarReq: 'function',
formatReachableFromReq: 'function',
formatRefreshJourneyReq: 'function',
transformJourneysQuery: 'function',
products: 'array',
parseDateTime: 'function',
parseDeparture: 'function',
parseArrival: 'function',
parseJourneyLeg: 'function',
parseJourney: 'function',
parseLine: 'function',
parseStationName: 'function',
parseLocation: 'function',
parsePolyline: 'function',
parseMovement: 'function',
parseNearby: 'function',
parseOperator: 'function',
parseHint: 'function',
parseWarning: 'function',
parseStopover: 'function',
formatAddress: 'function',
formatCoord: 'function',
formatDate: 'function',
formatLocationFilter: 'function',
formatProductsFilter: 'function',
formatPoi: 'function',
formatStation: 'function',
formatTime: 'function',
formatLocation: 'function',
formatRectangle: 'function'
}
const validateProfile = (profile) => {
for (let key of Object.keys(types)) {
const type = types[key]
if (type === 'array') {
if (!Array.isArray(profile[key])) {
throw new TypeError(`profile.${key} must be an array.`)
}
} else if (type !== typeof profile[key]) {
throw new TypeError(`profile.${key} must be a ${type}.`)
}
if (type === 'object' && profile[key] === null) {
throw new TypeError(`profile.${key} must not be null.`)
}
}
if (!Array.isArray(profile.products)) {
throw new TypeError('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 TypeError('profile.products[].id must be a string.')
}
if ('boolean' !== typeof product.default) {
throw new TypeError('profile.products[].default must be a boolean.')
}
if (!Array.isArray(product.bitmasks)) {
throw new TypeError(product.id + '.bitmasks must be an array.')
}
for (let bitmask of product.bitmasks) {
if ('number' !== typeof bitmask) {
throw new TypeError(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
@@ -0,0 +1,5 @@
Copyright (c) 2020, Jannis R
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
@@ -0,0 +1,13 @@
{
"auth": {
"type": "AID",
"aid": "4vV1AcH3N511icH"
},
"client": {
"type": "WEB",
"id": "AVV_AACHEN",
"name": "webapp"
},
"endpoint": "https://auskunft.avv.de/bin/mgate.exe",
"defaultLanguage": "de"
}
@@ -0,0 +1,53 @@
'use strict'
const createClient = require('../..')
const vosProfile = require('.')
const client = createClient(vosProfile, 'hafas-client-example')
const rwth = '1057'
const kronenberg = '1397'
// client.journeys(rwth, kronenberg, {results: 1, stopovers: true})
// .then(({journeys}) => {
// const [journey] = journeys
// return client.refreshJourney(journey.refreshToken, {stopovers: true, remarks: true})
// })
// .then(({journeys}) => {
// const [journey] = journeys
// const leg = journey.legs[0]
// return client.trip(leg.tripId, leg.line.name, {polyline: true})
// })
// client.departures(rwth, {duration: 1})
// client.arrivals(rwth, {duration: 10, linesOfStops: true})
// client.radar({
// north: 50.78141,
// west: 6.06031,
// south: 50.75022,
// east: 6.10316,
// }, {results: 10})
client.locations('kronenberg', {results: 3})
// client.stop(rwth, {linesOfStops: true})
// client.nearby({
// type: 'location',
// latitude: 50.770607,
// longitude: 6.104637,
// }, {distance: 500})
// client.reachableFrom({
// type: 'location',
// id: '990000745',
// address: 'Aachen, Charlottenstraße 11',
// latitude: 50.770607,
// longitude: 6.104637,
// }, {
// maxDuration: 8,
// })
.then((data) => {
console.log(require('util').inspect(data, {depth: null, colors: true}))
})
.catch(console.error)
@@ -0,0 +1,102 @@
'use strict'
const baseProfile = require('./base.json')
const products = [{
id: 'regional-train',
mode: 'train',
bitmasks: [1],
name: 'Regionalzug',
short: 'Regionalzug',
default: true,
}, {
id: 'long-distance-train',
mode: 'train',
bitmasks: [2],
name: 'Fernzug',
short: 'Fernzug',
default: true,
}, {
id: 'express-train',
mode: 'train',
bitmasks: [4],
name: 'ICE/Thalys',
short: 'ICE/Thalys',
default: true,
}, {
id: 'fernbus',
mode: 'bus',
bitmasks: [8],
name: 'Fernbus',
short: 'Fernbus',
default: true,
}, {
id: 'suburban-train',
mode: 'train',
bitmasks: [16],
name: 'S-Bahn',
short: 'S',
default: true,
}, {
id: 'subway',
mode: 'train',
bitmasks: [32],
name: 'U-Bahn',
short: 'U',
default: true,
}, {
id: 'tram',
mode: 'train',
bitmasks: [64],
name: 'Straßenbahn',
short: 'Straßenbahn',
default: true,
}, {
id: 'bus',
mode: 'bus',
bitmasks: [128],
name: 'Bus',
short: 'Bus',
default: true,
}, {
id: 'added-bus',
mode: 'bus',
bitmasks: [256],
name: 'Bus, Verstärkerfahrt',
short: 'Bus V',
default: true,
}, {
id: 'on-call',
mode: 'taxi',
bitmasks: [512],
name: 'Bedarfsverkehr',
short: 'Bedarfsverkehr',
default: true,
}, {
id: 'ferry',
mode: 'watercraft',
bitmasks: [1024],
name: 'Fähre',
short: 'Fähre',
default: true,
}]
const avvProfile = {
...baseProfile,
locale: 'de-DE',
timezone: 'Europe/Berlin',
ver: '1.26',
products,
departuresGetPasslist: false,
departuresStbFltrEquiv: false,
refreshJourneyUseOutReconL: true,
trip: true,
radar: true,
reachableFrom: true,
remarks: true,
remarksGetPolyline: false,
}
module.exports = avvProfile
@@ -0,0 +1,13 @@
# AVV profile for `hafas-client`
[*Aachener Verkehrsverbund (AVV)*](https://de.wikipedia.org/wiki/Aachener_Verkehrsverbund) is the local transport provider of [Aachen](https://en.wikipedia.org/wiki/Aachen). This profile adds *AVV* support to `hafas-client`.
## Usage
```js
const createClient = require('hafas-client')
const avvProfile = require('hafas-client/p/AVV')
// create a client with AVV profile
const client = createClient(avvProfile, 'my-awesome-program')
```
@@ -0,0 +1,13 @@
{
"auth": {
"type": "AID",
"aid": "kEwHkFUCIL500dym"
},
"client": {
"type": "WEB",
"id": "BART",
"name": "webapp"
},
"endpoint": "https://planner.bart.gov/bin/mgate.exe",
"defaultLanguage": "en"
}
@@ -0,0 +1,53 @@
'use strict'
const createClient = require('../..')
const vosProfile = require('.')
const client = createClient(vosProfile, 'hafas-client-example')
const fremont = '100013296'
const embarcadero = '100013295'
// client.journeys(fremont, embarcadero, {results: 1, stopovers: true})
// .then(({journeys}) => {
// const [journey] = journeys
// return client.refreshJourney(journey.refreshToken, {stopovers: true, remarks: true})
// })
// .then(({journeys}) => {
// const [journey] = journeys
// const leg = journey.legs[0]
// return client.trip(leg.tripId, leg.line.name, {polyline: true})
// })
// client.departures(fremont, {duration: 1})
// client.arrivals(fremont, {duration: 10, linesOfStops: true})
// client.radar({
// north: 37.8735,
// west: -122.5250,
// south: 37.6884,
// east: -122.2105,
// }, {results: 10})
client.locations('embarcadero', {results: 3})
// client.stop(fremont, {linesOfStops: true})
// client.nearby({
// type: 'location',
// latitude: 38.554779,
// longitude: -121.738798,
// }, {distance: 500})
// client.reachableFrom({
// type: 'location',
// id: '980557173',
// address: '1000 Alice St, Davis, 95616',
// latitude: 38.554779,
// longitude: -121.738798,
// }, {
// maxDuration: 8,
// })
.then((data) => {
console.log(require('util').inspect(data, {depth: null, colors: true}))
})
.catch(console.error)
@@ -0,0 +1,66 @@
'use strict'
const baseProfile = require('./base.json')
const products = [{
id: 'bart',
mode: 'train',
bitmasks: [128],
name: 'BART',
short: 'BART',
default: true,
}, {
id: 'regional-train',
mode: 'train',
bitmasks: [8],
name: 'regional trains (Caltrain, Capitol Corridor, ACE)',
short: 'regional trains',
default: true,
}, {
id: 'bus',
mode: 'bus',
bitmasks: [32],
name: 'Bus',
short: 'Bus',
default: true,
}, {
id: 'ferry',
mode: 'watercraft',
bitmasks: [64],
name: 'Ferry',
short: 'Ferry',
default: true,
}, {
id: 'tram',
mode: 'train',
bitmasks: [256],
name: 'Tram',
short: 'Tram',
default: true,
}, {
id: 'cable-car',
mode: 'train',
bitmasks: [4],
name: 'cable car',
short: 'cable car',
default: true,
}]
const bartProfile = {
...baseProfile,
locale: 'en-US',
timezone: 'America/Los_Angeles',
ver: '1.40',
products,
trip: true,
radar: true,
reachableFrom: true,
departuresGetPasslist: false, // `departures()` method: support for `getPasslist` field?
departuresStbFltrEquiv: false, // `departures()` method: support for `stbFltrEquiv` field?
refreshJourneyUseOutReconL: true,
}
module.exports = bartProfile
@@ -0,0 +1,13 @@
# BART profile for `hafas-client`
[*Bay Area Rapid Transit (BART)*](https://en.wikipedia.org/wiki/Bay_Area_Rapid_Transit) is the rapid transit public transportation system serving the [San Francisco Bay Area](https://en.wikipedia.org/wiki/San_Francisco_Bay_Area). This profile adds *BART* support to `hafas-client`.
## Usage
```js
const createClient = require('hafas-client')
const bartProfile = require('hafas-client/p/BART')
// create a client with BART profile
const client = createClient(bartProfile, 'my-awesome-program')
```
@@ -0,0 +1,13 @@
{
"auth": {
"type": "AID",
"aid": "3jkAncud78HSoqclmN54812A"
},
"client": {
"type": "WEB",
"id": "HAFAS",
"name": "webapp"
},
"endpoint": "https://bls.hafas.de/bin/mgate.exe",
"defaultLanguage": "de"
}
@@ -0,0 +1,53 @@
'use strict'
const createClient = require('../..')
const blsProfile = require('.')
const client = createClient(blsProfile, 'hafas-client-example')
const bernDennigkofengässli = '8590093'
const münsingenSpital = '8578932'
client.journeys(bernDennigkofengässli, münsingenSpital, {results: 1, stopovers: true})
// .then(({journeys}) => {
// const [journey] = journeys
// return client.refreshJourney(journey.refreshToken, {stopovers: true, remarks: true})
// })
// .then(({journeys}) => {
// const [journey] = journeys
// const leg = journey.legs[0]
// return client.trip(leg.tripId, leg.line.name, {polyline: true})
// })
// client.departures(bernDennigkofengässli, {duration: 1})
// client.arrivals(bernDennigkofengässli, {duration: 10, linesOfStops: true})
// client.radar({
// north: 46.969,
// west: 7.3941,
// south: 46.921,
// east: 7.5141,
// }, {results: 10})
// client.locations('münsingen spital', {results: 3})
// client.stop(bernDennigkofengässli, {linesOfStops: true})
// client.nearby({
// type: 'location',
// latitude: 53.554422,
// longitude: 9.977934
// }, {distance: 500})
// client.reachableFrom({
// type: 'location',
// id: '990017698',
// address: 'Bern, Schänzlihalde 17',
// latitude: 46.952835,
// longitude: 7.447527,
// }, {
// maxDuration: 10,
// })
.then((data) => {
console.log(require('util').inspect(data, {depth: null, colors: true}))
})
.catch(console.error)
@@ -0,0 +1,94 @@
'use strict'
const baseProfile = require('./base.json')
const products = [{
id: 'ice',
mode: 'train',
bitmasks: [1],
name: 'ICE',
short: 'ICE',
default: true,
}, {
id: 'ic-ec',
mode: 'train',
bitmasks: [2],
name: 'IC/EC',
short: 'IC/EC',
default: true,
}, {
id: 'ir',
mode: 'train',
bitmasks: [4],
name: 'IR',
short: 'IR',
default: true,
}, {
id: 'local-train',
mode: 'train',
bitmasks: [8],
name: 'Nahverkehr',
short: 'Nahverkehr',
default: true,
}, {
id: 'watercraft',
mode: 'watercraft',
bitmasks: [16],
name: 'Schiff',
short: 'Schiff',
default: true,
}, {
id: 's-bahn',
mode: 'train',
bitmasks: [32],
name: 'S-Bahn',
short: 'S',
default: true,
}, {
id: 'bus',
mode: 'bus',
bitmasks: [64],
name: 'Bus',
short: 'Bus',
default: true,
}, {
id: 'funicular',
mode: 'gondola',
bitmasks: [128],
name: 'Seilbahn',
short: 'Seilbahn',
default: true,
}, {
id: 'tram',
mode: 'train',
bitmasks: [512],
name: 'Tram',
short: 'Tram',
default: true,
}, {
id: 'car-shuttle-train',
mode: 'train',
bitmasks: [4096],
name: 'Autoverlad',
short: 'Autoverlad',
default: true,
}]
const blsProfile = {
...baseProfile,
locale: 'de-DE',
timezone: 'Europe/Berlin',
ver: '1.46',
products,
trip: true,
radar: true,
refreshJourneyUseOutReconL: true,
reachableFrom: true,
departuresGetPasslist: false,
departuresStbFltrEquiv: false,
}
module.exports = blsProfile
@@ -0,0 +1,15 @@
# BLS profile for `hafas-client`
[*BLS AG*](https://en.wikipedia.org/wiki/BLS_AG) is the local transport provider of the [Canton of Bern](https://en.wikipedia.org/wiki/Canton_of_Bern). This profile adds *BLS* support to `hafas-client`.
## Usage
```js
const createClient = require('hafas-client')
const blsProfile = require('hafas-client/p/BLS')
// create a client with BLS profile
const client = createClient(blsProfile, 'my-awesome-program')
```
Check out the [code examples](example.js).
@@ -0,0 +1,16 @@
{
"auth": {
"type": "AID",
"aid": "Mz0YdF9Fgx0Mb9"
},
"client": {
"type": "IPA",
"id": "BVG",
"v": "6020000",
"name": "FahrInfo"
},
"endpoint": "https://bvg.hafas.cloud/bin/mgate.exe",
"ext": "BVG.1",
"ver": "1.44",
"defaultLanguage": "de"
}
@@ -0,0 +1,48 @@
'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, linesOfStops: true})
// client.locations('Alexanderplatz', {results: 2})
// client.stop('900000042101', {linesOfStops: 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})
// client.reachableFrom({
// type: 'location',
// address: '13353 Berlin-Wedding, Torfstr. 17',
// latitude: 52.541797,
// longitude: 13.350042
// }, {
// when: new Date('2018-08-27T10:00:00+0200'),
// maxDuration: 10
// })
// .then(({journeys}) => {
// const [journey] = journeys
// const leg = journey.legs[0]
// return client.trip(leg.tripId, leg.line.name, {polyline: true})
// })
// .then(({journeys}) => {
// const [journey] = journeys
// return client.refreshJourney(journey.refreshToken, {stopovers: true, remarks: true})
// })
.then((data) => {
console.log(require('util').inspect(data, {depth: null, colors: true}))
})
.catch(console.error)
@@ -0,0 +1,207 @@
'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 {parseHook} = require('../../lib/profile-hooks')
const {
parseLine: _parseLine,
parseLocation: _parseLocation,
parseArrival: _parseArrival,
parseDeparture: _parseDeparture,
parseStopover: _parseStopover,
parseJourneyLeg: _parseJourneyLeg,
formatStation: _formatStation,
} = require('../../lib/default-profile')
const baseProfile = require('./base.json')
const products = require('./products')
// todo: there's also a referenced icon `{"res":"occup_fig_{low,mid}"}`
const addOccupancy = (item, occupancyCodes) => {
const remIdx = (item.remarks || [])
.findIndex(r => r.code && occupancyCodes.has(r.code))
if (remIdx < 0) return;
const rem = item.remarks[remIdx]
item.occupancy = occupancyCodes.get(rem.code)
item.remarks = [
...item.remarks.slice(0, remIdx),
...item.remarks.slice(remIdx + 1),
]
}
const stopoverOccupancyCodes = new Map([
['text.occup.loc.max.11', 'low'],
['text.occup.loc.max.12', 'medium'],
['text.occup.loc.max.13', 'high'],
])
const journeyLegOccupancyCodes = new Map([
['text.occup.jny.max.11', 'low'],
['text.occup.jny.max.12', 'medium'],
['text.occup.jny.max.13', 'high'],
])
// todo: https://m.tagesspiegel.de/berlin/fahrerlebnis-wie-im-regionalexpress-so-faehrt-es-sich-in-der-neuen-express-s-bahn/25338674.html
const parseLineWithMoreDetails = ({parsed}, p) => {
parsed.name = p.name.replace(/^(bus|tram)\s+/i, '')
const details = parseLineName(parsed.name)
parsed.symbol = details.symbol
parsed.nr = details.nr
parsed.metro = details.metro
parsed.express = details.express
parsed.night = details.night
return parsed
}
const parseLocation = ({parsed}, l) => {
if (parsed.type === 'stop' || parsed.type === 'station') {
parsed.name = shorten(parsed.name)
parsed.id = to12Digit(parsed.id)
if (!parsed.location.latitude || !parsed.location.longitude) {
const [s] = getStations(parsed.id)
if (s) Object.assign(parsed.location, s.location)
}
}
return parsed
}
// todo: S45, S46?
const ringbahnClockwise = /^ringbahn s\s?41$/i
const ringbahnAnticlockwise = /^ringbahn s\s?42$/i
const parseDepartureRenameRingbahn = ({parsed}, dep) => {
if (parsed.line && parsed.line.product === 'suburban') {
const d = parsed.direction && parsed.direction.trim()
if (ringbahnClockwise.test(d)) {
parsed.direction = 'Ringbahn S41 ⟳'
} else if (ringbahnAnticlockwise.test(d)) {
parsed.direction = 'Ringbahn S42 ⟲'
}
}
return parsed
}
const parseArrivalRenameRingbahn = ({parsed}, arr) => {
if (parsed.line && parsed.line.product === 'suburban') {
const p = parsed.provenance && parsed.provenance.trim()
if (ringbahnClockwise.test(p)) {
parsed.provenance = 'Ringbahn S41 ⟳'
} else if (ringbahnAnticlockwise.test(p)) {
parsed.provenance = 'Ringbahn S42 ⟲'
}
}
return parsed
}
const parseArrDepWithOccupancy = ({parsed}, d) => {
addOccupancy(parsed, stopoverOccupancyCodes)
return parsed
}
const parseStopoverWithOccupancy = ({parsed}, st, date) => {
addOccupancy(parsed, stopoverOccupancyCodes)
return parsed
}
const parseJourneyLegWithBerlkönig = (ctx, leg, date) => {
if (leg.type === 'KISS') {
const icon = ctx.common.icons[leg.icoX]
if (icon && icon.type === 'prod_berl') {
const res = _parseJourneyLeg(ctx, {
...leg, type: 'WALK'
}, date)
delete res.walking
const mcp = leg.dep.mcp || {}
const mcpData = mcp.mcpData || {}
// todo: mcp.lid
// todo: mcpData.occupancy, mcpData.type
// todo: journey.trfRes.bkgData
res.line = {
type: 'line',
id: null, // todo
// todo: fahrtNr?
name: mcpData.providerName,
public: true,
mode: 'taxi',
product: 'berlkoenig'
// todo: operator
}
return res
}
}
return _parseJourneyLeg(ctx, leg, date)
}
const parseJourneyLegWithOccupancy = ({parsed}, leg, date) => {
if (leg.type === 'JNY') {
addOccupancy(parsed, journeyLegOccupancyCodes)
}
return parsed
}
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)
}
// use the Berlkönig ride sharing service?
const requestJourneysWithBerlkoenig = ({opt}, query) => {
if (('numF' in query) && opt.berlkoenig) {
// todo: check if this is still true
throw new Error('The `berlkoenig` and `results` options are mutually exclusive.')
}
query.jnyFltrL.push({type: 'GROUP', mode: 'INC', value: 'OEV'})
if (opt.berlkoenig) query.jnyFltrL.push({type: 'GROUP', mode: 'INC', value: 'BERLKOENIG'})
query.gisFltrL = [{meta: 'foot_speed_normal', type: 'M', mode: 'FB'}]
return query
}
// todo: adapt/extend `vbb-parse-ticket` to support the BVG markup
const bvgProfile = {
...baseProfile,
locale: 'de-DE',
timezone: 'Europe/Berlin',
transformJourneysQuery: requestJourneysWithBerlkoenig,
products,
parseLine: parseHook(_parseLine, parseLineWithMoreDetails),
parseLocation: parseHook(_parseLocation, parseLocation),
parseStationName: (ctx, name) => shorten(name),
parseArrival: parseHook(
parseHook(_parseArrival, parseArrivalRenameRingbahn),
parseArrDepWithOccupancy,
),
parseDeparture: parseHook(
parseHook(_parseDeparture, parseDepartureRenameRingbahn),
parseArrDepWithOccupancy,
),
parseStopover: parseHook(_parseStopover, parseStopoverWithOccupancy),
parseJourneyLeg: parseHook(
parseJourneyLegWithBerlkönig,
parseJourneyLegWithOccupancy,
),
formatStation,
departuresGetPasslist: false, // `departures()` method: support for `getPasslist` field?
departuresStbFltrEquiv: false, // `departures()` method: support for `stbFltrEquiv` field?
refreshJourneyUseOutReconL: true,
trip: true,
radar: true,
refreshJourney: true,
reachableFrom: true
}
module.exports = bvgProfile
@@ -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
}
]
@@ -0,0 +1,43 @@
# 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*)
- supports [BerlKönig `journey` legs](#berlkoenig)
- strips parts from station names that are unnecessary in the Berlin context
- parses line names to give more information (e.g. "Is it an express bus?")
- renames *Ringbahn* line names to contain `⟳` and `⟲`
### BerlKönig
BVG has recently announced [a ride-sharing service called *BerlKönig*](https://www.berlkoenig.de). Pass `berlkoenig: true` into `journeys()` to get special legs:
```js
{
mode: 'walking',
departure: // …
arrival: // …
origin: // …
destination: // …
line: {
type: 'line',
name: 'BerlKönig',
public: true,
mode: 'taxi',
product: 'berlkoenig'
}
}
```
@@ -0,0 +1,15 @@
{
"auth": {
"type": "AID",
"aid": "ALT2vl7LAFDFu2dz"
},
"client": {
"type": "IPH",
"id": "HAFAS",
"v": "4000000",
"name": "cflPROD-STORE"
},
"endpoint": "https://horaires.cfl.lu/bin/mgate.exe",
"ver": "1.43",
"defaultLanguage": "fr"
}
@@ -0,0 +1,52 @@
'use strict'
const createClient = require('../..')
const cflProfile = require('.')
const client = createClient(cflProfile, 'hafas-client-example')
const mersch = '9864348'
const bruxellesCentral = '8800003'
// from Mersch to Bruxelles Central
client.journeys(mersch, bruxellesCentral, {results: 1})
// .then(({journeys}) => {
// const leg = journeys[0].legs[0]
// return client.trip(leg.tripId, leg.line.name, {polyline: true})
// })
// .then(({journeys}) => {
// const [journey] = journeys
// return client.refreshJourney(journey.refreshToken, {stopovers: true, remarks: true})
// })
// client.locations('mersch', {results: 3})
// client.stop(mersch)
// client.nearby({
// type: 'location',
// latitude: 49.7523,
// longitude: 6.1103
// }, {distance: 500})
// client.departures(mersch, {duration: 5})
// client.arrivals(mersch, {duration: 10, linesOfStops: true})
// client.radar({
// north: 49.9,
// west: 6.11,
// south: 49.7,
// east: 6.13
// }, {results: 10})
// client.reachableFrom({
// type: 'location',
// id: '980005067',
// address: '7557 Mersch, Rue Mies 1',
// latitude: 49.746044,
// longitude: 6.102228,
// }, {
// maxDuration: 30,
// })
.then(data => {
console.log(require('util').inspect(data, {depth: null, colors: true}))
})
.catch(console.error)
@@ -0,0 +1,28 @@
'use strict'
const baseProfile = require('./base.json')
const products = require('./products')
const transformReqBody = (ctx, body) => {
body.lang = 'de'
return body
}
const cflProfile = {
...baseProfile,
locale: 'de-LU',
timezone: 'Europe/Luxembourg',
transformReqBody,
products: products,
departuresGetPasslist: false,
departuresStbFltrEquiv: false,
refreshJourneyUseOutReconL: true,
trip: true,
radar: true,
reachableFrom: true,
remarksGetPolyline: false,
}
module.exports = cflProfile;
@@ -0,0 +1,45 @@
'use strict'
module.exports = [
// todo: other bits
{
id: 'express-train',
mode: 'train',
bitmasks: [1, 2],
name: 'TGV, ICE, EuroCity',
short: 'TGV/ICE/EC',
default: true
},
{
id: 'local-train',
mode: 'train',
bitmasks: [8, 16],
name: 'local trains',
short: 'local',
default: true
},
{
id: 'tram',
mode: 'train',
bitmasks: [256],
name: 'tram',
short: 'tram',
default: true
},
{
id: 'bus',
mode: 'bus',
bitmasks: [32],
name: 'bus',
short: 'bus',
default: true
},
{
id: 'gondola',
mode: 'gondola',
bitmasks: [512],
name: 'Fun', // taken from the horaires.cfl.lu website
short: 'Fun', // abbreviation for funicular?
default: true
}
]
@@ -0,0 +1,18 @@
# CFL profile for `hafas-client`
The [*Société Nationale des Chemins de Fer Luxembourgeois (CFL)*](https://en.wikipedia.org/wiki/Société_Nationale_des_Chemins_de_Fer_Luxembourgeois) is the national railway company of [Luxembourg](https://en.wikipedia.org/wiki/Luxembourg). This profile adds *CFL*-specific customisations to `hafas-client`.
## Usage
```js
const createClient = require('hafas-client')
const cflProfile = require('hafas-client/p/cfl')
// create a client with CFL profile
const client = createClient(cflProfile)
```
## Customisations
- *CFL*-specific products (such as [*Standseilbahn_Pfaffenthal-Kirchberg*](https://de.wikipedia.org/wiki/Standseilbahn_Pfaffenthal-Kirchberg))
@@ -0,0 +1,15 @@
{
"auth": {
"type": "AID",
"aid": "ioslaskdcndrjcmlsd"
},
"client": {
"type": "IPH",
"id": "CMTA",
"v": "2",
"name": "CapMetro"
},
"endpoint": "https://capmetro.hafas.cloud/bin/mgate.exe",
"ver": "1.40",
"defaultLanguage": "en"
}
@@ -0,0 +1,48 @@
'use strict'
const createClient = require('../..')
const cmtaProfile = require('.')
const client = createClient(cmtaProfile, 'hafas-client-example')
// Broadie Oaks to Domain
client.journeys('000002370', '000005919', {results: 1, polylines: true})
// client.departures('000002370', {duration: 1})
// client.arrivals('000002370', {duration: 10, linesOfStops: true})
// client.locations('Westgate', {results: 2})
// client.stop('000005534') // Downtown light rail station
// client.nearby({
// type: 'location',
// latitude: 30.266222,
// longitude: -97.746058
// }, {distance: 60})
// client.radar({
// north: 30.240877,
// west: -97.804588,
// south: 30.225378,
// east: -97.786692
// }, {results: 10})
// client.reachableFrom({
// type: 'location',
// address: '604 W 9TH ST, Austin, TX 78701',
// latitude: 30.272910,
// longitude: -97.747883
// }, {
// when: new Date('2018-08-27T10:00:00+0200'),
// maxDuration: 15
// })
// .then(({journeys}) => {
// const [journey] = journeys
// const leg = journey.legs[0]
// return client.trip(leg.tripId, leg.line.name, {polyline: true})
// })
// .then(({journeys}) => {
// const [journey] = journeys
// return client.refreshJourney(journey.refreshToken, {stopovers: true, remarks: true})
// })
.then((data) => {
console.log(require('util').inspect(data, {depth: null, colors: true}))
})
.catch(console.error)
@@ -0,0 +1,23 @@
'use strict'
const baseProfile = require('./base.json')
const products = require('./products')
const cmtaProfile = {
...baseProfile,
locale: 'en-US',
timezone: 'America/Chicago',
products,
departuresGetPasslist: false,
departuresStbFltrEquiv: false,
refreshJourneyUseOutReconL: true,
trip: true,
radar: true,
refreshJourney: true,
reachableFrom: true,
remarks: true, // `.svcResL[0].res.msgL[]` is missing though 🤔
}
module.exports = cmtaProfile
@@ -0,0 +1,28 @@
'use strict'
module.exports = [
{
id: 'bus',
mode: 'bus',
bitmasks: [32],
name: 'MetroBus',
short: 'B',
default: true
},
{
id: 'rapid',
mode: 'bus',
bitmasks: [4096],
name: 'MetroRapid',
short: 'R',
default: true
},
{
id: 'rail',
mode: 'train',
bitmasks: [8],
name: 'MetroRail',
short: 'M',
default: true
}
]
@@ -0,0 +1,18 @@
# CMTA profile for `hafas-client`
[*Capital Metropolitan Transportation Authority (CMTA)* or *CapMetro*](https://en.wikipedia.org/wiki/Capital_Metropolitan_Transportation_Authority) is a public transportation provider serving [Austin, Texas](https://en.wikipedia.org/wiki/Austin,_Texas) metropolitan area. This profile adds *CMTA*-specific customizations to `hafas-client`.
## Usage
```js
const createClient = require('hafas-client')
const cmtaProfile = require('hafas-client/p/cmta')
// create a client with CMTA profile
const client = createClient(cmtaProfile, 'my-awesome-program')
```
## Customisations
- parses *CMTA*-specific products (such as *MetroRapid* and *MetroRail*)
@@ -0,0 +1,14 @@
{
"auth": {
"type": "AID",
"aid": "XNFGL2aSkxfDeK8N4waOZnsdJ"
},
"client": {
"type": "WEB",
"id": "DART",
"name": "webapp"
},
"endpoint": "https://dart.hafas.de/bin/mgate.exe",
"ver": "1.35",
"defaultLanguage": "en"
}
@@ -0,0 +1,54 @@
'use strict'
const createClient = require('../..')
const dartProfile = require('.')
const client = createClient(dartProfile, 'hafas-client example')
const mlkJrPkwyAdamsAveDsm2055 = '100002702'
const se5thStEHackleyAveDsm2294 = '100004972'
// client.journeys(mlkJrPkwyAdamsAveDsm2055, se5thStEHackleyAveDsm2294, {
// results: 1, stopovers: true,
// })
// .then(({journeys}) => {
// const [journey] = journeys
// return client.refreshJourney(journey.refreshToken, {stopovers: true, remarks: true})
// })
// .then(({journeys}) => {
// const [journey] = journeys
// const leg = journey.legs.find(l => !!l.line)
// return client.trip(leg.tripId, leg.line.name, {polyline: true})
// })
// client.departures(mlkJrPkwyAdamsAveDsm2055, {duration: 10})
// client.arrivals(mlkJrPkwyAdamsAveDsm2055, {linesOfStops: true})
client.locations('adams ave', {results: 3})
// client.stop(mlkJrPkwyAdamsAveDsm2055, {linesOfStops: true})
// client.nearby({
// type: 'location',
// id: '980010311',
// address: 'Austraße 37, 6700 Bludenz',
// latitude: 41.6056,
// longitude: -93.5916,
// }, {distance: 1000})
// client.reachableFrom({
// type: 'location',
// latitude: 41.613584,
// longitude: -93.881803,
// address: 'Laurel St, Waukee, 50263',
// }, {
// maxDuration: 20,
// })
// client.radar({
// north: 41.6266,
// west: -93.7299,
// south: 41.5503,
// east: -93.5699,
// })
.then((data) => {
console.log(require('util').inspect(data, {depth: null, colors: true}))
})
.catch(console.error)
@@ -0,0 +1,29 @@
'use strict'
const baseProfile = require('./base.json')
const products = [{
id: 'bus',
mode: 'bus',
bitmasks: [32],
name: 'Bus',
short: 'Bus',
default: true,
}]
const dartProfile = {
...baseProfile,
locale: 'en-US',
timezone: 'America/Chicago',
products,
departuresGetPasslist: false,
departuresStbFltrEquiv: false,
refreshJourneyUseOutReconL: true,
trip: true,
reachableFrom: true,
radar: true,
}
module.exports = dartProfile
@@ -0,0 +1,17 @@
# DART profile for `hafas-client`
[*Des Moines Area Rapid Transit (DART)*](https://en.wikipedia.org/wiki/Des_Moines_Area_Regional_Transit) is the local transport provider of [Des Moines](https://en.wikipedia.org/wiki/Des_Moines_metropolitan_area), Iowa, USA. This profile adds *DART* support to `hafas-client`.
*Note:* This profile *does not* support [*Dallas Area Rapid Transit (DART)*](https://de.wikipedia.org/wiki/Verkehrsverbund_Vorarlberg) in [Dallas–Fort Worth](https://en.wikipedia.org/wiki/Dallas–Fort_Worth_metroplex), Texas, USA.
## Usage
```js
const createClient = require('hafas-client')
const dartProfile = require('hafas-client/p/dart')
// create a client with DART profile
const client = createClient(dartProfile, 'my-awesome-program')
```
Check out the [code examples](example.js).
@@ -0,0 +1,16 @@
{
"auth": {
"type": "AID",
"aid": "OGBAqytjHhCvr0J4"
},
"client": {
"type": "AND",
"id": "DB-REGIO",
"v": 100021,
"name": "DB Busradar NRW"
},
"endpoint": "https://db-regio.hafas.de/bin/hci/mgate.exe",
"ext": "DB.REGIO.1",
"ver": "1.24",
"defaultLanguage": "de"
}
@@ -0,0 +1,41 @@
'use strict'
const createClient = require('../../')
const dbbusradarnrwProfile = require('.')
const client = createClient(dbbusradarnrwProfile, 'hafas-client-example')
// Hagen Bauhaus to Schwerte Bahnhof
// returns hafas error PARSE
client.journeys('3307002', '3357026', {results: 1})
// .then(({journeys}) => {
// const leg = journeys[0].legs[0]
// return client.trip(leg.tripId, leg.line.name, {polyline: true})
// })
// .then(({journeys}) => {
// const [journey] = journeys
// return client.refreshJourney(journey.refreshToken, {stopovers: true, remarks: true})
// })
// client.departures('3307002', {duration: 60})
// client.arrivals('3307002', {duration: 30, linesOfStops: true})
// client.locations('Hagen Vorhalle')
// client.stop('3307002') // Hagen Bauhaus
// client.nearby({
// type: 'location',
// latitude: 51.38,
// longitude: 7.45
// }, {results: 1})
// client.radar({
// north: 51.5,
// west: 7.2,
// south: 51.2,
// east: 7.8
// }, {results: 10})
.then((data) => {
console.log(require('util').inspect(data, {depth: null, colors: true}))
}, console.error)
@@ -0,0 +1,96 @@
'use strict'
const baseProfile = require('./base.json')
// DB Busradar NRW app does not allow selecting specific modes of transport to filter results,
// so the bitmasks had to be determined by querying some stations and looking at the results..
const products = [
{
id: 'national-express',
mode: 'train',
bitmasks: [1],
name: 'InterCityExpress',
short: 'ICE',
default: true
},
{
id: 'national',
mode: 'train',
bitmasks: [2],
name: 'InterCity & EuroCity',
short: 'IC/EC',
default: true
},
// todo: not always true when a station has RE stopping at it
// maybe something else?
{
id: 'regional-express',
mode: 'train',
bitmasks: [4],
name: 'Regionalexpress',
short: 'RE',
default: true
},
// todo: also used for replacement service incl. S-Bahn replacement
{
id: 'regional',
mode: 'train',
bitmasks: [8],
name: 'Regionalzug',
short: 'RB/RE',
default: true
},
{
id: 'suburban',
mode: 'train',
bitmasks: [16],
name: 'S-Bahn',
short: 'S',
default: true
},
{
id: 'bus',
mode: 'bus',
bitmasks: [32],
name: 'Bus',
short: 'Bus',
default: true
},
{
id: 'ferry',
mode: 'watercraft',
bitmasks: [64],
name: 'Ferry',
short: 'F',
default: true
},
// todo: are `128` & `256` unused?
{
id: 'taxi',
mode: 'taxi',
bitmasks: [512],
name: 'AnrufSammelTaxi',
short: 'AST',
default: true
}
]
const dbBusradarNrwProfile = {
...baseProfile,
locale: 'de-DE',
timezone: 'Europe/Berlin',
products: products,
departuresGetPasslist: false,
departuresStbFltrEquiv: false,
refreshJourneyUseOutReconL: true,
journeysOutFrwd: false,
trip: true,
radar: true,
remarks: true, // `.svcResL[0].res.msgL[]` is missing though 🤔
lines: false, // `.svcResL[0].res.lineL[]` is missing 🤔
}
module.exports = dbBusradarNrwProfile
@@ -0,0 +1,19 @@
# DB Busradar NRW profile for `hafas-client`
[*DB Busradar NRW*](https://www.bahn.de/westfalenbus/view/fahrplan/busradar.shtml) is a mobile application used in [North Rhine-Westphalia](https://en.wikipedia.org/wiki/North_Rhine-Westphalia).
It shows realtime locations and arrival/departure information for vehicles operated by bus companies which are part of [DB Regio Bus](https://www.dbregio.de/db_regio/view/wir/bus.shtml) in NRW, namely:
- [BVR Busverkehr Rheinland GmbH](https://www.rheinlandbus.de/) (DB Rheinlandbus)
- [WB Westfalen Bus GmbH](https://www.westfalenbus.de/) (DB Westfalenbus)
- [BVO Busverkehr Ostwestfalen GmbH](https://www.ostwestfalen-lippe-bus.de) (DB Ostwestfalen-Lippe-Bus)
This profile adapts `hafas-client` to the HAFAS endpoint used by the application.
## Usage
```js
const createClient = require('hafas-client')
const dbbusradarnrwProfile = require('hafas-client/p/db-busradar-nrw')
// create a client with DB Busradar NRW profile
const client = createClient(dbbusradarnrwProfile, 'my-awesome-program')
```
@@ -0,0 +1,17 @@
{
"auth": {
"type": "AID",
"aid": "n91dB8Z77MLdoR0K"
},
"salt": "6264493855566A34304B356676787766",
"client": {
"type": "AND",
"id": "DB",
"v": 19040000,
"name": "DB Navigator"
},
"endpoint": "https://reiseauskunft.bahn.de/bin/mgate.exe",
"ext": "DB.R20.12.b",
"ver": "1.34",
"defaultLanguage": "en"
}
@@ -0,0 +1,59 @@
'use strict'
const createClient = require('../../')
const dbProfile = require('.')
const client = createClient(dbProfile, 'hafas-client-example')
// Berlin Jungfernheide to München Hbf
client.journeys('8011167', '8000261', {results: 1, tickets: true})
// .then(({journeys}) => {
// const leg = journeys[0].legs[0]
// return client.trip(leg.tripId, leg.line.name, {polyline: true})
// })
// .then(({journeys}) => {
// const [journey] = journeys
// return client.refreshJourney(journey.refreshToken, {stopovers: true, remarks: true})
// })
// client.departures('8011167', {duration: 1})
// client.arrivals('8011167', {duration: 10, linesOfStops: true})
// client.locations('Berlin Jungfernheide')
// client.locations('Atze Musiktheater', {poi: true, addressses: false, fuzzy: false})
// client.stop('8000309') // Regensburg Hbf
// client.nearby({
// type: 'location',
// latitude: 52.4751309,
// longitude: 13.3656537
// }, {results: 1})
// client.reachableFrom({
// type: 'location',
// address: '13353 Berlin-Wedding, Torfstr. 17',
// latitude: 52.541797,
// longitude: 13.350042
// }, {
// when: new Date('2018-08-27T10:00:00+0200'),
// maxDuration: 50
// })
// client.radar({
// north: 52.52411,
// west: 13.41002,
// south: 52.51942,
// east: 13.41709
// }, {results: 10})
// client.journeys('8011113', '8000261', {
// departure: Date.now() - 2 * 60 * 60 * 1000,
// results: 1, stopovers: true, transfers: 1
// })
// .then(({journeys}) => {
// const leg = journeys[0].legs.find(l => l.line && l.line.product === 'nationalExpress')
// const prevStopover = leg.stopovers.find((st) => {
// return st.departure && Date.parse(st.departure) < Date.now()
// })
// return client.journeysFromTrip(leg.tripId, prevStopover, '8000207')
// })
.then((data) => {
console.log(require('util').inspect(data, {depth: null, colors: true}))
}, console.error)
@@ -0,0 +1,510 @@
'use strict'
const trim = require('lodash/trim')
const uniqBy = require('lodash/uniqBy')
const slugg = require('slugg')
const without = require('lodash/without')
const {parseHook} = require('../../lib/profile-hooks')
const _parseJourney = require('../../parse/journey')
const _parseJourneyLeg = require('../../parse/journey-leg')
const _parseLine = require('../../parse/line')
const _parseArrival = require('../../parse/arrival')
const _parseDeparture = require('../../parse/departure')
const _parseHint = require('../../parse/hint')
const _parseLocation = require('../../parse/location')
const _formatStation = require('../../format/station')
const {bike} = require('../../format/filters')
const products = require('./products')
const baseProfile = require('./base.json')
const formatLoyaltyCard = require('./loyalty-cards').format
const transformReqBody = (ctx, body) => {
const req = body.svcReqL[0] || {}
// see https://pastebin.com/qZ9WS3Cx
req.cfg = {...req.cfg, rtMode: 'HYBRID'} // todo: use `REALTIME`?
return body
}
const slices = (n, arr) => {
const initialState = {slices: [], count: Infinity}
return arr.reduce(({slices, count}, item) => {
if (count >= n) {
slices.push([item])
count = 1
} else {
slices[slices.length - 1].push(item)
count++
}
return {slices, count}
}, initialState).slices
}
const parseGrid = (g) => {
// todo: g.type, e.g. `S`
// todo: respect `g.itemL[].(col|row)`?
// todo
// parseGrid is being called by parseLocWithDetails, which is being called as
// profile.parseLocation by profile.parseCommon, parseCommon hasn't finished
// resolving all references yet, so we have to resolve them manually here.
// This would be fixed if we resolve references on-the-fly or in a recursive/
// iterative process.
return {
title: g.title,
rows: slices(g.nCols, g.itemL.map(item => (
Array.isArray(item.hints) && item.hints[0] ||
Array.isArray(item.remarkRefs) && item.remarkRefs[0] && item.remarkRefs[0].hint ||
{}
))),
}
}
const ausstattungKeys = Object.assign(Object.create(null), {
'3-s-zentrale': '3SZentrale',
'parkplatze': 'parkingLots',
'fahrrad-stellplatze': 'bicycleParkingRacks',
'opnv-anbindung': 'localPublicTransport',
'wc': 'toilets',
'schliessfacher': 'lockers',
'reisebedarf': 'travelShop',
'stufenfreier-zugang': 'stepFreeAccess',
'ein-umsteigehilfe': 'boardingAid',
'taxi-am-bahnhof': 'taxis'
})
const parseAusstattungVal = (val) => {
val = val.toLowerCase()
return val === 'ja' ? true : (val === 'nein' ? false : val)
}
const parseAusstattungGrid = (g) => {
// filter duplicate hint rows
const rows = uniqBy(g.rows, ([key, val]) => key + ':' + val)
const res = {}
Object.defineProperty(res, 'raw', {value: rows})
for (let [key, val] of rows) {
key = ausstattungKeys[slugg(key)]
if (key) res[key] = parseAusstattungVal(val)
}
return res
}
const parseReisezentrumÖffnungszeiten = (g) => {
const res = {}
for (const [dayOfWeek, val] of g.rows) res[dayOfWeek] = val
res.raw = g.rows
return res
}
const parseLocWithDetails = ({parsed, common}, l) => {
if (!parsed) return parsed
if (parsed.type !== 'stop' && parsed.type !== 'station') return parsed
if (Array.isArray(l.gridL)) {
const resolveCells = grid => ({
...grid,
rows: grid.rows.map(row => row.map(cell => cell && cell.text)),
})
let grids = l.gridL
.map(grid => parseGrid(grid, common))
.map(resolveCells)
const ausstattung = grids.find(g => slugg(g.title) === 'ausstattung')
if (ausstattung) {
parsed.facilities = parseAusstattungGrid(ausstattung)
}
const öffnungszeiten = grids.find(g => slugg(g.title) === 'offnungszeiten-reisezentrum')
if (öffnungszeiten) {
parsed.reisezentrumOpeningHours = parseReisezentrumÖffnungszeiten(öffnungszeiten)
}
grids = without(grids, ausstattung, öffnungszeiten)
if (grids.length > 0) parsed.grids = grids
}
return parsed
}
// https://www.bahn.de/p/view/service/buchung/auslastungsinformation.shtml
const loadFactors = []
loadFactors[1] = 'low-to-medium'
loadFactors[2] = 'high'
loadFactors[3] = 'very-high'
loadFactors[4] = 'exceptionally-high'
const parseLoadFactor = (opt, tcocL, tcocX) => {
const cls = opt.firstClass ? 'FIRST' : 'SECOND'
const load = tcocX.map(i => tcocL[i]).find(lf => lf.c === cls)
return load && loadFactors[load.r] || null
}
const parseArrOrDepWithLoadFactor = ({parsed, res, opt}, d) => {
if (d.stbStop.dTrnCmpSX && Array.isArray(d.stbStop.dTrnCmpSX.tcocX)) {
const load = parseLoadFactor(opt, res.common.tcocL || [], d.stbStop.dTrnCmpSX.tcocX)
if (load) parsed.loadFactor = load
}
return parsed
}
const transformJourneysQuery = ({opt}, query) => {
const filters = query.jnyFltrL
if (opt.bike) filters.push(bike)
query.trfReq = {
jnyCl: opt.firstClass === true ? 1 : 2,
tvlrProf: [{
type: 'E',
redtnCard: opt.loyaltyCard
? formatLoyaltyCard(opt.loyaltyCard)
: null
}],
cType: 'PK'
}
return query
}
// todo: fix this
// line: {
// type: 'line',
// id: '5-vbbbvb-x9',
// fahrtNr: '52496',
// name: 'X9',
// public: true,
// mode: 'bus',
// product: 'bus',
// operator: {type: 'operator', id: 'nahreisezug', name: 'Nahreisezug'}
// }
const parseLineWithAdditionalName = ({parsed}, l) => {
if (l.nameS && ['bus', 'tram', 'ferry'].includes(l.product)) {
parsed.name = l.nameS
}
if (l.addName) {
parsed.additionalName = parsed.name
parsed.name = l.addName
}
return parsed
}
// todo: sotRating, conSubscr, isSotCon, showARSLink, sotCtxt
// todo: conSubscr, showARSLink, useableTime
const parseJourneyWithPrice = ({parsed}, raw) => {
parsed.price = null
// todo: find cheapest, find discounts
// todo: write a parser like vbb-parse-ticket
// {
// "statusCode": "OK",
// "fareSetL": [
// {
// "fareL": [
// {
// "isFromPrice": true,
// "isPartPrice": false,
// "isBookable": true,
// "isUpsell": false,
// "targetCtx": "D",
// "buttonText": "To offer selection",
// "price": {
// "amount": 11400
// }
// }
// ]
// }
// ]
// }
if (
raw.trfRes &&
Array.isArray(raw.trfRes.fareSetL) &&
raw.trfRes.fareSetL[0] &&
Array.isArray(raw.trfRes.fareSetL[0].fareL) &&
raw.trfRes.fareSetL[0].fareL[0]
) {
const tariff = raw.trfRes.fareSetL[0].fareL[0]
if (tariff.price && tariff.price.amount >= 0) { // wat
parsed.price = {
amount: tariff.price.amount / 100,
currency: 'EUR',
hint: null
}
}
}
return parsed
}
const parseJourneyLegWithLoadFactor = ({parsed, res, opt}, raw) => {
const tcocX = raw.jny && raw.jny.dTrnCmpSX && raw.jny.dTrnCmpSX.tcocX
if (Array.isArray(tcocX) && Array.isArray(res.common.tcocL)) {
const load = parseLoadFactor(opt, res.common.tcocL, tcocX)
if (load) parsed.loadFactor = load
}
return parsed
}
// todo:
// [ { type: 'hint',
// code: 'P5',
// text: 'Es gilt ein besonderer Fahrpreis' }
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 parseHintByCode = ({parsed}, raw) => {
// plain-text hints used e.g. for stop metadata
if (raw.type === 'K') {
return {type: 'hint', text: raw.txtN}
}
if (raw.type === 'A') {
const hint = hintsByCode[raw.code && raw.code.trim().toLowerCase()]
if (hint) {
return Object.assign({text: raw.txtN}, hint)
}
}
if (parsed && raw.txtN) {
const text = trim(raw.txtN.toLowerCase(), ' ()')
if (codesByText[text]) parsed.code = codesByText[text]
}
return parsed
}
const isIBNR = /^\d{6,}$/
const formatStation = (id) => {
if (!isIBNR.test(id)) throw new Error('station ID must be an IBNR.')
return _formatStation(id)
}
// todo: find option for absolute number of results
const dbProfile = {
...baseProfile,
locale: 'de-DE',
timezone: 'Europe/Berlin',
addChecksum: true,
transformReqBody,
transformJourneysQuery,
products: products,
parseLocation: parseHook(_parseLocation, parseLocWithDetails),
parseJourney: parseHook(_parseJourney, parseJourneyWithPrice),
parseJourneyLeg: parseHook(_parseJourneyLeg, parseJourneyLegWithLoadFactor),
parseLine: parseHook(_parseLine, parseLineWithAdditionalName),
parseArrival: parseHook(_parseArrival, parseArrOrDepWithLoadFactor),
parseDeparture: parseHook(_parseDeparture, parseArrOrDepWithLoadFactor),
parseHint: parseHook(_parseHint, parseHintByCode),
formatStation,
departuresGetPasslist: false,
departuresStbFltrEquiv: false,
refreshJourneyUseOutReconL: true,
trip: true,
journeysFromTrip: true,
radar: true,
reachableFrom: true,
lines: false, // `.svcResL[0].res.lineL[]` is missing 🤔
}
module.exports = dbProfile
@@ -0,0 +1,31 @@
'use strict'
// todo: generate from https://reiseauskunft.bahn.de/addons/fachkonfig-utf8.cfg ?
const c = {
NONE: Symbol('no loyalty card'),
BAHNCARD: Symbol('Bahncard'),
VORTEILSCARD: Symbol('VorteilsCard'),
HALBTAXABO: Symbol('HalbtaxAbo'),
VOORDEELURENABO: Symbol('Voordeelurenabo'),
SHCARD: Symbol('SH-Card'),
GENERALABONNEMENT: Symbol('General-Abonnement')
}
// see https://gist.github.com/juliuste/202bb04f450a79f8fa12a2ec3abcd72d
const formatLoyaltyCard = (data) => {
if (data.type === c.BAHNCARD) {
if (data.discount === 25) return data.class === 1 ? 1 : 2
if (data.discount === 50) return data.class === 1 ? 3 : 4
}
if (data.type === c.VORTEILSCARD) return 9
if (data.type === c.HALBTAXABO) return data.railplus ? 10 : 11
if (data.type === c.VOORDEELURENABO) return data.railplus ? 12 : 13
if (data.type === c.SHCARD) return 14
if (data.type === c.GENERALABONNEMENT) return 15
return 0
}
module.exports = {
data: c,
format: formatLoyaltyCard
}
@@ -0,0 +1,85 @@
'use strict'
// todo: https://gist.github.com/anonymous/d3323a5d2d6e159ed42b12afd0380434#file-haf_products-properties-L1-L95
module.exports = [
{
id: 'nationalExpress',
mode: 'train',
bitmasks: [1],
name: 'InterCityExpress',
short: 'ICE',
default: true
},
{
id: 'national',
mode: 'train',
bitmasks: [2],
name: 'InterCity & EuroCity',
short: 'IC/EC',
default: true
},
{
id: 'regionalExp', // todo: rename
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
}
]
@@ -0,0 +1,82 @@
# DB profile for `hafas-client`
[*Deutsche Bahn (DB)*](https://en.wikipedia.org/wiki/Deutsche_Bahn) is the largest German long-distance public transport company. This profile adds *DB*-specific customizations to `hafas-client`. Consider using [`db-hafas`](https://github.com/derhuerst/db-hafas#db-hafas), to always get the customized client right away.
## Usage
```js
const createClient = require('hafas-client')
const dbProfile = require('hafas-client/p/db')
// create a client with DB profile
const client = createClient(dbProfile, 'my-awesome-program')
```
## Customisations
- supports 1st and 2nd class with `journey()`
- supports [their loyalty cards](https://en.wikipedia.org/wiki/Deutsche_Bahn#Tickets) with `journey()`
- parses *DB*-specific products (such as *InterCity-Express*)
- exposes the cheapest ticket price for a `journey`
- parses [*DB*-specific station info](#additional-station-info)
### additional station info
With the `db` profile, `hafas-client` will return more station information whenever the endpoint provides it:
```js
{
type: 'station',
id: '8004585',
name: 'Oberstdorf',
// …
facilities: {
'3SZentrale': '089/13081055',
parkingLots: true,
bicycleParkingRacks: true,
localPublicTransport: true,
toilets: true,
lockers: true,
travelShop: true,
stepFreeAccess: true,
boardingAid: 'ja, um voranmeldung unter 01806 512 512* wird gebeten',
taxis: true
},
reisezentrumOpeningHours: {
Mo: '08:00-18:00',
Di: '08:00-18:00',
Mi: '08:00-18:00',
Do: '08:00-18:00',
Fr: '08:00-18:00',
Sa: '09:00-14:00',
So: '09:00-14:00'
},
// …
stops: [{
type: 'stop',
id: '965503',
name: 'Busbahnhof, Oberstdorf',
// …
reisezentrumOpeningHours: {
Mo: '08:00-18:00',
Di: '08:00-18:00',
Mi: '08:00-18:00',
Do: '08:00-18:00',
Fr: '08:00-18:00',
Sa: '09:00-14:00',
So: '09:00-14:00'
}
}]
}
```
## Using the `loyaltyCard` option
```js
const {data: loyaltyCards} = require('hafas-client/p/db/loyalty-cards')
hafas.journeys(from, to, {
loyaltyCard: {type: data.BAHNCARD, discount: 25}
})
```
@@ -0,0 +1,17 @@
{
"auth": {
"type": "AID",
"aid": "andcXUmC9Mq6hjrwDIGd2l3oiaMrTUzyH"
},
"salt": "pqjM3iKEGOAhYbX76k9R5zutv",
"client": {
"type": "AND",
"id": "HVV",
"v": "4020100",
"name": "HVVPROD_ADHOC"
},
"endpoint": "https://hvv-app.hafas.de/bin/mgate.exe",
"ext": "HVV.1",
"ver": "1.18",
"defaultLanguage": "de"
}
@@ -0,0 +1,47 @@
'use strict'
const createClient = require('../..')
const hvvProfile = require('.')
const client = createClient(hvvProfile, 'hafas-client-example')
// client.journeys('116', '5900', {results: 1, polylines: true})
// client.departures('116', {duration: 1})
// client.arrivals('116', {duration: 10, linesOfStops: true})
client.locations('dammtor', {results: 2})
// client.stop('116', {linesOfStops: true}) // Dammtor
// client.nearby({
// type: 'location',
// latitude: 53.554422,
// longitude: 9.977934
// }, {distance: 60})
// client.radar({
// north: 53.55,
// west: 9.95,
// south: 52.51,
// east: 10
// }, {results: 10})
// client.reachableFrom({
// type: 'location',
// address: 'Hamburg, Holstenwall 9',
// latitude: 53.553766,
// longitude: 9.977514
// }, {
// when: new Date('2019-05-16T10:00:00+0200'),
// maxDuration: 8
// })
// .then(({journeys}) => {
// const [journey] = journeys
// const leg = journey.legs[0]
// return client.trip(leg.tripId, leg.line.name, {polyline: true})
// })
// .then(({journeys}) => {
// const [journey] = journeys
// return client.refreshJourney(journey.refreshToken, {stopovers: true, remarks: true})
// })
.then((data) => {
console.log(require('util').inspect(data, {depth: null, colors: true}))
})
.catch(console.error)
@@ -0,0 +1,24 @@
'use strict'
const baseProfile = require('./base.json')
const products = require('./products')
const hvvProfile = {
...baseProfile,
locale: 'de-DE',
timezone: 'Europe/Berlin',
// baseProfile.salt is interpreted as hex by hafas-client
salt: Buffer.from('pqjM3iKEGOAhYbX76k9R5zutv', 'utf8'),
addMicMac: true,
products,
trip: true,
radar: true,
refreshJourney: true,
reachableFrom: true,
remarksGetPolyline: false,
lines: false, // fails with `FAIL` "HCI Service: request failed"
}
module.exports = hvvProfile
@@ -0,0 +1,92 @@
'use strict'
module.exports = [ // todo: what is `512`?
{
id: 'subway',
mode: 'train',
bitmasks: [1],
name: 'U-Bahn',
short: 'U',
default: true
},
{
id: 'suburban',
mode: 'train',
bitmasks: [2],
name: 'S-Bahn',
short: 'S',
default: true
},
{
id: 'akn',
mode: 'train',
bitmasks: [4],
name: 'AKN',
short: 'A',
default: true
},
{
id: 'regional-express-train',
mode: 'train',
bitmasks: [8],
name: 'RegionalExpress',
short: 'RE',
default: true
},
{
id: 'regional-train',
mode: 'train',
bitmasks: [16],
name: 'Regionalbahn',
short: 'RB',
default: true
},
{
id: 'ferry',
mode: 'watercraft',
bitmasks: [32],
name: 'Fähre',
short: 'F',
default: true
},
{
id: 'bus',
mode: 'bus',
bitmasks: [128],
name: 'Bus',
short: 'Bus',
default: true
},
{
id: 'express-bus',
mode: 'bus',
bitmasks: [256],
name: 'Schnellbus',
short: 'Schnellbus',
default: true
},
{
id: 'anruf-sammel-taxi',
mode: null, // todo
bitmasks: [1024],
name: 'Anruf-Sammel-Taxi',
short: 'AST',
default: true
},
{
id: 'long-distance-train',
mode: 'train',
bitmasks: [4096, 64],
name: 'Fernzug',
short: 'ICE/IC/EC/EN',
default: false
},
{
id: 'long-distance-bus',
mode: 'bus',
bitmasks: [2048],
name: 'Fernbus',
short: 'Fernbus',
default: false
}
]
@@ -0,0 +1,18 @@
# HVV profile for `hafas-client`
[*Hamburger Verkehrsverbund (HVV)*](https://en.wikipedia.org/wiki/Hamburger_Verkehrsverbund) is the major local transport provider in [Hamburg](https://en.wikipedia.org/wiki/Hamburg). This profile adds *HVV*-specific customizations to `hafas-client`.
## Usage
```js
const createClient = require('hafas-client')
const hvvProfile = require('hafas-client/p/hvv')
// create a client with HVV profile
const client = createClient(hvvProfile, 'my-awesome-program')
```
## Customisations
- parses *HVV*-specific products (such as *AKN*)
@@ -0,0 +1,15 @@
{
"auth": {
"type": "AID",
"aid": "nasa-apps"
},
"client": {
"type": "IPH",
"id": "NASA",
"v": "4000200",
"name": "nasaPROD"
},
"endpoint": "https://reiseauskunft.insa.de/bin/mgate.exe",
"ver": "1.44",
"defaultLanguage": "de"
}

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