From 904f890442173c1099a8d4e8f8bbc68e0cad7f2c Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 15 Jan 2018 00:45:05 +0100 Subject: [PATCH 01/25] move checksum calculation to lib/request --- lib/default-profile.js | 3 +++ lib/request.js | 18 ++++++++++++++++-- p/db/index.js | 18 ++++-------------- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/lib/default-profile.js b/lib/default-profile.js index ec2a09fe..1e767cbc 100644 --- a/lib/default-profile.js +++ b/lib/default-profile.js @@ -26,6 +26,9 @@ const filters = require('../format/filters') const id = x => x const defaultProfile = { + salt: null, + addChecksum: false, + transformReqBody: id, transformReq: id, diff --git a/lib/request.js b/lib/request.js index 0eccbb52..d4494f91 100644 --- a/lib/request.js +++ b/lib/request.js @@ -1,5 +1,6 @@ 'use strict' +const crypto = require('crypto') const Promise = require('pinkie-promise') const {fetch} = require('fetch-ponyfill')({Promise}) const {stringify} = require('query-string') @@ -9,6 +10,8 @@ const hafasError = (err) => { return err } +const md5 = input => crypto.createHash('md5').update(input).digest() + const request = (profile, data) => { const body = profile.transformReqBody({lang: 'en', svcReqL: [data]}) const req = profile.transformReq({ @@ -20,10 +23,21 @@ const request = (profile, data) => { 'Accept-Encoding': 'gzip, deflate', 'user-agent': 'https://github.com/derhuerst/hafas-client' }, - query: null + query: {} }) - const url = profile.endpoint + (req.query ? '?' + stringify(req.query) : '') + if (profile.addChecksum) { + if (!Buffer.isBuffer(profile.salt)) { + throw new Error('profile.salt must be a Buffer.') + } + const checksum = md5(Buffer.concat([ + Buffer.from(req.body, 'utf8'), + profile.salt + ])) + req.query.checksum = checksum.toString('hex') + } + + const url = profile.endpoint +'?' + stringify(req.query) return fetch(url, req) .then((res) => { if (!res.ok) { diff --git a/p/db/index.js b/p/db/index.js index 4b6dd41c..a5973581 100644 --- a/p/db/index.js +++ b/p/db/index.js @@ -1,7 +1,5 @@ 'use strict' -const crypto = require('crypto') - const _createParseLine = require('../../parse/line') const _createParseJourney = require('../../parse/journey') const _formatStation = require('../../format/station') @@ -23,17 +21,6 @@ const transformReqBody = (body) => { return body } -const salt = 'bdI8UVj40K5fvxwf' -const transformReq = (req) => { - const hash = crypto.createHash('md5') - hash.update(req.body + salt) - - if (!req.query) req.query = {} - req.query.checksum = hash.digest('hex') - - return req -} - const transformJourneysQuery = (query, opt) => { const filters = query.jnyFltrL if (opt.bike) filters.push(bike) @@ -142,8 +129,11 @@ const dbProfile = { locale: 'de-DE', timezone: 'Europe/Berlin', endpoint: 'https://reiseauskunft.bahn.de/bin/mgate.exe', + + salt: Buffer.from('bdI8UVj40K5fvxwf', 'utf8'), + addChecksum: true, + transformReqBody, - transformReq, transformJourneysQuery, products: modes.allProducts, From 6f958e14be6fc2683684aea2a11744a96f67f2ea Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 15 Jan 2018 00:45:40 +0100 Subject: [PATCH 02/25] mic & mac calculation thanks to @alexander-albers! see https://github.com/schildbach/public-transport-enabler/issues/187#issuecomment-357436079 --- lib/default-profile.js | 1 + lib/request.js | 21 +++++++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/default-profile.js b/lib/default-profile.js index 1e767cbc..7abcf60d 100644 --- a/lib/default-profile.js +++ b/lib/default-profile.js @@ -28,6 +28,7 @@ const id = x => x const defaultProfile = { salt: null, addChecksum: false, + addMicMac: false, transformReqBody: id, transformReq: id, diff --git a/lib/request.js b/lib/request.js index d4494f91..cc5192a9 100644 --- a/lib/request.js +++ b/lib/request.js @@ -26,15 +26,24 @@ const request = (profile, data) => { query: {} }) - if (profile.addChecksum) { + if (profile.addChecksum || profile.addMicMac) { if (!Buffer.isBuffer(profile.salt)) { throw new Error('profile.salt must be a Buffer.') } - const checksum = md5(Buffer.concat([ - Buffer.from(req.body, 'utf8'), - profile.salt - ])) - req.query.checksum = checksum.toString('hex') + if (profile.addChecksum) { + const checksum = md5(Buffer.concat([ + Buffer.from(req.body, 'utf8'), + profile.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 mac = md5(Buffer.concat([mic, profile.salt])) + req.query.mac = mac.toString('hex') + } } const url = profile.endpoint +'?' + stringify(req.query) From ab80e81ccce2f678d051229fd66b6c1be75199e7 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Wed, 28 Feb 2018 16:09:23 +0100 Subject: [PATCH 03/25] validate more input --- format/products-bitmask.js | 5 +++-- index.js | 16 +++++++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/format/products-bitmask.js b/format/products-bitmask.js index f28e5a9a..b3baab87 100644 --- a/format/products-bitmask.js +++ b/format/products-bitmask.js @@ -1,11 +1,12 @@ 'use strict' -const createFormatBitmask = (modes) => { +const createFormatBitmask = (allProducts) => { const formatBitmask = (products) => { if(Object.keys(products).length === 0) throw new Error('products filter must not be empty') let bitmask = 0 for (let product in products) { - if (products[product] === true) bitmask += modes[product].bitmask + if (!allProducts[product]) throw new Error('unknown product ' + product) + if (products[product] === true) bitmask += allProducts[product].bitmask } return bitmask } diff --git a/index.js b/index.js index 91c9cf50..1a7ce2a5 100644 --- a/index.js +++ b/index.js @@ -7,12 +7,14 @@ const validateProfile = require('./lib/validate-profile') const defaultProfile = require('./lib/default-profile') const _request = require('./lib/request') +const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o) + const createClient = (profile, request = _request) => { profile = Object.assign({}, defaultProfile, profile) validateProfile(profile) const departures = (station, opt = {}) => { - if ('object' === typeof station) station = profile.formatStation(station.id) + if (isObj(station)) station = profile.formatStation(station.id) else if ('string' === typeof station) station = profile.formatStation(station) else throw new Error('station must be an object or a string.') @@ -108,7 +110,9 @@ const createClient = (profile, request = _request) => { } const locations = (query, opt = {}) => { - if ('string' !== typeof query) throw new Error('query must be a string.') + if ('string' !== typeof query || !query) { + throw new Error('query must be a non-empty string.') + } opt = Object.assign({ fuzzy: true, // find only exact matches? results: 10, // how many search results? @@ -158,7 +162,7 @@ const createClient = (profile, request = _request) => { } const nearby = (location, opt = {}) => { - if ('object' !== typeof location || Array.isArray(location)) { + if (!isObj(location)) { throw new Error('location must be an object.') } else if (location.type !== 'location') { throw new Error('invalid location object.') @@ -200,6 +204,12 @@ const createClient = (profile, request = _request) => { } const journeyLeg = (ref, lineName, opt = {}) => { + if ('string' !== typeof ref || !ref) { + throw new Error('ref must be a non-empty string.') + } + if ('string' !== typeof lineName || !lineName) { + throw new Error('lineName must be a non-empty string.') + } opt = Object.assign({ passedStations: true // return stations on the way? }, opt) From fd8eb92806cd2fcb58bc61364f30d3cfc0af082d Mon Sep 17 00:00:00 2001 From: Jannis R Date: Fri, 2 Mar 2018 00:35:54 +0100 Subject: [PATCH 04/25] journeys: add journey.type :bug: To make it FPTF-compliant. --- parse/journey.js | 1 + test/db.js | 2 ++ test/oebb.js | 2 ++ test/vbb.js | 2 ++ 4 files changed, 7 insertions(+) diff --git a/parse/journey.js b/parse/journey.js index 2463c114..340991f4 100644 --- a/parse/journey.js +++ b/parse/journey.js @@ -14,6 +14,7 @@ const createParseJourney = (profile, stations, lines, remarks) => { const parseJourney = (j) => { const legs = j.secL.map(leg => parseLeg(j, leg)) const res = { + type: 'journey', legs, origin: legs[0].origin, destination: legs[legs.length - 1].destination, diff --git a/test/db.js b/test/db.js index d8a7d640..17b1a140 100644 --- a/test/db.js +++ b/test/db.js @@ -100,6 +100,8 @@ test('Berlin Jungfernheide to München Hbf', co(function* (t) { t.ok(Array.isArray(journeys)) t.ok(journeys.length > 0, 'no journeys') for (let journey of journeys) { + t.equal(journey.type, 'journey') + assertValidStation(t, journey.origin) assertValidStationProducts(t, journey.origin.products) if (!(yield findStation(journey.origin.id))) { diff --git a/test/oebb.js b/test/oebb.js index 90a7b350..31ba75d4 100644 --- a/test/oebb.js +++ b/test/oebb.js @@ -120,6 +120,8 @@ test('Salzburg Hbf to Wien Westbahnhof', co(function* (t) { t.ok(Array.isArray(journeys)) t.ok(journeys.length > 0, 'no journeys') for (let journey of journeys) { + t.equal(journey.type, 'journey') + assertValidStation(t, journey.origin) assertValidStationProducts(t, journey.origin.products) // todo diff --git a/test/vbb.js b/test/vbb.js index 8d562c81..318cb9ad 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -67,6 +67,8 @@ test('journeys – station to station', co(function* (t) { t.strictEqual(journeys.length, 3) for (let journey of journeys) { + t.equal(journey.type, 'journey') + assertValidStation(t, journey.origin) assertValidStationProducts(t, journey.origin.products) t.ok(journey.origin.name.indexOf('(Berlin)') === -1) From d56be3eceb7c78b744859ef32a064335d762c582 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Fri, 2 Mar 2018 00:39:56 +0100 Subject: [PATCH 05/25] 2.3.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 367ab6d5..1fff615a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hafas-client", "description": "JavaScript client for HAFAS public transport APIs.", - "version": "2.3.2", + "version": "2.3.3", "main": "index.js", "files": [ "index.js", From 0a9042aa837118128ced573547502bbe4c0d7d2d Mon Sep 17 00:00:00 2001 From: Jannis R Date: Fri, 2 Mar 2018 16:34:22 +0100 Subject: [PATCH 06/25] wip --- test/vbb.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/vbb.js b/test/vbb.js index 318cb9ad..4a3ce7fd 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -148,7 +148,7 @@ test('journeys – only subway', co(function* (t) { test('journeys – fails with no product', co(function* (t) { try { - yield client.journeys(spichernstr, bismarckstr, { + client.journeys(spichernstr, bismarckstr, { when, products: { suburban: false, @@ -160,6 +160,8 @@ test('journeys – fails with no product', co(function* (t) { regional: false } }) + // silence rejections, we're only interested in exceptions + .catch(() => {}) } catch (err) { t.ok(err, 'error thrown') t.end() From 5ac8d5f89e2dc64c50cacf5511124a267e6483ef Mon Sep 17 00:00:00 2001 From: Jannis R Date: Fri, 19 Jan 2018 15:47:41 +0100 Subject: [PATCH 07/25] mic & mac part 2, VBB salt Many thanks to @alexander-albers! --- lib/request.js | 3 ++- p/vbb/index.js | 8 +++++++- readme.md | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/request.js b/lib/request.js index cc5192a9..dcfe552d 100644 --- a/lib/request.js +++ b/lib/request.js @@ -41,7 +41,8 @@ const request = (profile, data) => { const mic = md5(Buffer.from(req.body, 'utf8')) req.query.mic = mic.toString('hex') - const mac = md5(Buffer.concat([mic, profile.salt])) + const micAsHex = Buffer.from(mic.toString('hex'), 'utf8') + const mac = md5(Buffer.concat([micAsHex, profile.salt])) req.query.mac = mac.toString('hex') } } diff --git a/p/vbb/index.js b/p/vbb/index.js index 2c9325c6..56519259 100644 --- a/p/vbb/index.js +++ b/p/vbb/index.js @@ -22,7 +22,7 @@ const formatBitmask = createFormatBitmask(modes) const transformReqBody = (body) => { body.client = {type: 'IPA', id: 'VBB', name: 'vbbPROD', v: '4010300'} body.ext = 'VBB.1' - body.ver = '1.11' // todo: 1.16 with `mic` and `mac` query params + body.ver = '1.16' body.auth = {type: 'AID', aid: 'hafas-vbb-apps'} return body @@ -168,6 +168,12 @@ const vbbProfile = { locale: 'de-DE', timezone: 'Europe/Berlin', endpoint: 'https://fahrinfo.vbb.de/bin/mgate.exe', + + // https://gist.github.com/derhuerst/a8d94a433358abc015ff77df4481070c#file-haf_config_base-properties-L39 + // https://runkit.com/derhuerst/hafas-decrypt-encrypted-mac-salt + salt: Buffer.from('5243544a4d3266467846667878516649', 'hex'), + addMicMac: true, + transformReqBody, products: modes.allProducts, diff --git a/readme.md b/readme.md index 8bcba40f..06081135 100644 --- a/readme.md +++ b/readme.md @@ -174,6 +174,7 @@ The returned [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript - [`vbb-hafas`](https://github.com/derhuerst/vbb-hafas#vbb-hafas) – JavaScript client for Berlin & Brandenburg public transport HAFAS API. - [`hafas-departures-in-direction`](https://github.com/derhuerst/hafas-departures-in-direction#hafas-departures-in-direction) – Pass in a HAFAS client, get departures in a certain direction. - [`hafas-collect-departures-at`](https://github.com/derhuerst/hafas-collect-departures-at#hafas-collect-departures-at) – Utility to collect departures, using any HAFAS client. +- [`hafas-discover-stations`](https://github.com/derhuerst/hafas-discover-stations#hafas-discover-stations) – Pass in a HAFAS client, discover stations by querying departures. - [`hafas-rest-api`](https://github.com/derhuerst/hafas-rest-api#hafas-rest-api) – Expose a HAFAS client via an HTTP REST API. - [List of european long-distance transport operators, available API endpoints, GTFS feeds and client modules.](https://github.com/public-transport/european-transport-operators) - [Collection of european transport JavaScript modules.](https://github.com/public-transport/european-transport-modules) From dfb894f70c3e2882e5b1fcfaafa6d403c27e8a21 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Thu, 15 Feb 2018 17:45:20 +0100 Subject: [PATCH 08/25] update deps --- package.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index f5d769c4..866e7431 100644 --- a/package.json +++ b/package.json @@ -33,28 +33,28 @@ }, "dependencies": { "capture-stack-trace": "^1.0.0", - "fetch-ponyfill": "^4.1.0", - "lodash": "^4.17.4", - "luxon": "^0.3.1", + "fetch-ponyfill": "^5.0.0", + "lodash": "^4.17.5", + "luxon": "^0.4.0", "p-throttle": "^1.1.0", "pinkie-promise": "^2.0.1", - "query-string": "^5.0.0", + "query-string": "^5.1.0", "slugg": "^1.2.0", - "vbb-parse-line": "^0.3.0", + "vbb-parse-line": "^0.3.1", "vbb-parse-ticket": "^0.2.1", "vbb-short-station-name": "^0.4.0", - "vbb-stations": "^6.1.0", + "vbb-stations": "^6.2.1", "vbb-translate-ids": "^3.1.0" }, "devDependencies": { - "db-stations": "^1.34.0", + "db-stations": "^2.3.0", "is-coordinates": "^2.0.2", "is-roughly-equal": "^0.1.0", "tap-spec": "^4.1.1", "tape": "^4.8.0", "tape-promise": "^2.0.1", - "validate-fptf": "^1.2.0", - "vbb-stations-autocomplete": "^3.0.0" + "validate-fptf": "^1.2.1", + "vbb-stations-autocomplete": "^3.1.0" }, "scripts": { "test": "env NODE_ENV=dev node test/index.js", From b1441d8be6ed313aa15d0b1d616db159f4673ac0 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Wed, 28 Feb 2018 16:09:23 +0100 Subject: [PATCH 09/25] validate more input --- format/products-bitmask.js | 5 +++-- index.js | 16 +++++++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/format/products-bitmask.js b/format/products-bitmask.js index f28e5a9a..b3baab87 100644 --- a/format/products-bitmask.js +++ b/format/products-bitmask.js @@ -1,11 +1,12 @@ 'use strict' -const createFormatBitmask = (modes) => { +const createFormatBitmask = (allProducts) => { const formatBitmask = (products) => { if(Object.keys(products).length === 0) throw new Error('products filter must not be empty') let bitmask = 0 for (let product in products) { - if (products[product] === true) bitmask += modes[product].bitmask + if (!allProducts[product]) throw new Error('unknown product ' + product) + if (products[product] === true) bitmask += allProducts[product].bitmask } return bitmask } diff --git a/index.js b/index.js index 91c9cf50..1a7ce2a5 100644 --- a/index.js +++ b/index.js @@ -7,12 +7,14 @@ const validateProfile = require('./lib/validate-profile') const defaultProfile = require('./lib/default-profile') const _request = require('./lib/request') +const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o) + const createClient = (profile, request = _request) => { profile = Object.assign({}, defaultProfile, profile) validateProfile(profile) const departures = (station, opt = {}) => { - if ('object' === typeof station) station = profile.formatStation(station.id) + if (isObj(station)) station = profile.formatStation(station.id) else if ('string' === typeof station) station = profile.formatStation(station) else throw new Error('station must be an object or a string.') @@ -108,7 +110,9 @@ const createClient = (profile, request = _request) => { } const locations = (query, opt = {}) => { - if ('string' !== typeof query) throw new Error('query must be a string.') + if ('string' !== typeof query || !query) { + throw new Error('query must be a non-empty string.') + } opt = Object.assign({ fuzzy: true, // find only exact matches? results: 10, // how many search results? @@ -158,7 +162,7 @@ const createClient = (profile, request = _request) => { } const nearby = (location, opt = {}) => { - if ('object' !== typeof location || Array.isArray(location)) { + if (!isObj(location)) { throw new Error('location must be an object.') } else if (location.type !== 'location') { throw new Error('invalid location object.') @@ -200,6 +204,12 @@ const createClient = (profile, request = _request) => { } const journeyLeg = (ref, lineName, opt = {}) => { + if ('string' !== typeof ref || !ref) { + throw new Error('ref must be a non-empty string.') + } + if ('string' !== typeof lineName || !lineName) { + throw new Error('lineName must be a non-empty string.') + } opt = Object.assign({ passedStations: true // return stations on the way? }, opt) From d3551d2e514de00ad6237b7d7a908f2c6e25b176 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Fri, 2 Mar 2018 00:35:54 +0100 Subject: [PATCH 10/25] journeys: add journey.type :bug: To make it FPTF-compliant. --- parse/journey.js | 1 + test/db.js | 2 ++ test/oebb.js | 2 ++ test/vbb.js | 2 ++ 4 files changed, 7 insertions(+) diff --git a/parse/journey.js b/parse/journey.js index 2463c114..340991f4 100644 --- a/parse/journey.js +++ b/parse/journey.js @@ -14,6 +14,7 @@ const createParseJourney = (profile, stations, lines, remarks) => { const parseJourney = (j) => { const legs = j.secL.map(leg => parseLeg(j, leg)) const res = { + type: 'journey', legs, origin: legs[0].origin, destination: legs[legs.length - 1].destination, diff --git a/test/db.js b/test/db.js index d8a7d640..17b1a140 100644 --- a/test/db.js +++ b/test/db.js @@ -100,6 +100,8 @@ test('Berlin Jungfernheide to München Hbf', co(function* (t) { t.ok(Array.isArray(journeys)) t.ok(journeys.length > 0, 'no journeys') for (let journey of journeys) { + t.equal(journey.type, 'journey') + assertValidStation(t, journey.origin) assertValidStationProducts(t, journey.origin.products) if (!(yield findStation(journey.origin.id))) { diff --git a/test/oebb.js b/test/oebb.js index 90a7b350..31ba75d4 100644 --- a/test/oebb.js +++ b/test/oebb.js @@ -120,6 +120,8 @@ test('Salzburg Hbf to Wien Westbahnhof', co(function* (t) { t.ok(Array.isArray(journeys)) t.ok(journeys.length > 0, 'no journeys') for (let journey of journeys) { + t.equal(journey.type, 'journey') + assertValidStation(t, journey.origin) assertValidStationProducts(t, journey.origin.products) // todo diff --git a/test/vbb.js b/test/vbb.js index e24a1ef8..1205b344 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -67,6 +67,8 @@ test('journeys – station to station', co(function* (t) { t.strictEqual(journeys.length, 3) for (let journey of journeys) { + t.equal(journey.type, 'journey') + assertValidStation(t, journey.origin) assertValidStationProducts(t, journey.origin.products) t.ok(journey.origin.name.indexOf('(Berlin)') === -1) From 07389ef418d3d0e2d469e38dc5351823c6eb7c03 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sat, 3 Mar 2018 00:18:52 +0100 Subject: [PATCH 11/25] use new location format see also cadd4105d11939ffe57031b4d7ed97d58d288900 --- format/address.js | 14 +++++++++----- format/location-identifier.js | 14 ++++++++++++++ format/poi.js | 14 ++++++++------ format/station.js | 12 +++++++++++- 4 files changed, 42 insertions(+), 12 deletions(-) create mode 100644 format/location-identifier.js diff --git a/format/address.js b/format/address.js index 2774c380..51e710dc 100644 --- a/format/address.js +++ b/format/address.js @@ -1,5 +1,6 @@ 'use strict' +const formatLocationIdentifier = require('./location-identifier') const formatCoord = require('./coord') const formatAddress = (a) => { @@ -7,13 +8,16 @@ const formatAddress = (a) => { throw new Error('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', name: a.address, - crd: { - x: formatCoord(a.longitude), - y: formatCoord(a.latitude) - } + lid: formatLocationIdentifier(data) } } diff --git a/format/location-identifier.js b/format/location-identifier.js new file mode 100644 index 00000000..83fedd3a --- /dev/null +++ b/format/location-identifier.js @@ -0,0 +1,14 @@ +'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 diff --git a/format/poi.js b/format/poi.js index 3991e4c6..22abe0f5 100644 --- a/format/poi.js +++ b/format/poi.js @@ -1,5 +1,6 @@ 'use strict' +const formatLocationIdentifier = require('./location-identifier') const formatCoord = require('./coord') const formatPoi = (p) => { @@ -8,13 +9,14 @@ const formatPoi = (p) => { } return { - type: 'P', name: p.name, - lid: 'L=' + p.id, - crd: { - x: formatCoord(p.longitude), - y: formatCoord(p.latitude) - } + lid: formatLocationIdentifier({ + A: '4', // POI + O: p.name, + L: p.id, + X: formatCoord(p.longitude), + Y: formatCoord(p.latitude) + }) } } diff --git a/format/station.js b/format/station.js index 418e5181..a2440c47 100644 --- a/format/station.js +++ b/format/station.js @@ -1,5 +1,15 @@ 'use strict' -const formatStation = id => ({type: 'S', lid: 'L=' + id}) +const formatLocationIdentifier = require('./location-identifier') + +const formatStation = (id) => { + return { + // todo: name necessary? + lid: formatLocationIdentifier({ + A: '1', // station + L: id + }) + } +} module.exports = formatStation From 869c9a8e9892d5034b4a7b06823a95dc9899c823 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 4 Mar 2018 19:53:53 +0100 Subject: [PATCH 12/25] earlierThan/laterThan options (cherry-picked from next-journeys-by-ref) --- docs/journeys.md | 242 +++++++++++++++++++++++++++-------------------- index.js | 37 +++++++- test/db.js | 74 ++++++++++++--- test/oebb.js | 68 ++++++++++--- test/vbb.js | 50 ++++++++++ 5 files changed, 341 insertions(+), 130 deletions(-) diff --git a/docs/journeys.md b/docs/journeys.md index 86446da8..0656934c 100644 --- a/docs/journeys.md +++ b/docs/journeys.md @@ -41,6 +41,8 @@ With `opt`, you can override the default options, which look like this: ```js { when: new Date(), + earlierThan: null, // ref to get journeys earlier than the last query + laterThan: null, // ref to get journeys later than the last query results: 5, // how many journeys? via: null, // let journeys pass this station passedStations: false, // return stations on the way? @@ -85,123 +87,127 @@ client.journeys('900000003201', '900000100008', { The response may look like this: ```js -[ { - legs: [ { - id: '1|31041|35|86|17122017', +[ + { + legs: [ { + id: '1|31041|35|86|17122017', + origin: { + type: 'station', + id: '900000003201', + name: 'S+U Berlin Hauptbahnhof', + location: { + type: 'location', + latitude: 52.52585, + longitude: 13.368928 + }, + products: { + suburban: true, + subway: true, + tram: true, + bus: true, + ferry: false, + express: true, + regional: true + } + }, + departure: '2017-12-17T19:07:00.000+01:00', + departurePlatform: '16', + destination: { + type: 'station', + id: '900000024101', + name: 'S Charlottenburg', + location: { + type: 'location', + latitude: 52.504806, + longitude: 13.303846 + }, + products: { + suburban: true, + subway: false, + tram: false, + bus: true, + ferry: false, + express: false, + regional: true + } + }, + arrival: '2017-12-17T19:47:00.000+01:00', + arrivalPlatform: '8', + arrivalDelay: 30, + line: { + type: 'line', + id: '16845', + name: 'S7', + public: true, + mode: 'train', + product: 'suburban', + symbol: 'S', + nr: 7, + metro: false, + express: false, + night: false, + productCode: 0, + operator: { + type: 'operator', + id: 's-bahn-berlin-gmbh', + name: 'S-Bahn Berlin GmbH' + } + }, + direction: 'S Potsdam Hauptbahnhof', + passed: [ { + station: { + type: 'station', + id: '900000003201', + name: 'S+U Berlin Hauptbahnhof', + location: { /* … */ }, + products: { /* … */ } + }, + arrival: null, + departure: null, + cancelled: true + }, { + station: { + type: 'station', + id: '900000003102', + name: 'S Bellevue', + location: { /* … */ }, + products: { /* … */ } + }, + arrival: '2017-12-17T19:09:00.000+01:00', + departure: '2017-12-17T19:09:00.000+01:00' + }, /* … */ { + station: { + type: 'station', + id: '900000024101', + name: 'S Charlottenburg', + location: { /* … */ }, + products: { /* … */ } + }, + arrival: '2017-12-17T19:17:00.000+01:00', + departure: '2017-12-17T19:17:00.000+01:00' + } ] + } ], origin: { type: 'station', id: '900000003201', name: 'S+U Berlin Hauptbahnhof', - location: { - type: 'location', - latitude: 52.52585, - longitude: 13.368928 - }, - products: { - suburban: true, - subway: true, - tram: true, - bus: true, - ferry: false, - express: true, - regional: true - } + location: { /* … */ }, + products: { /* … */ } }, departure: '2017-12-17T19:07:00.000+01:00', - departurePlatform: '16', destination: { type: 'station', id: '900000024101', name: 'S Charlottenburg', - location: { - type: 'location', - latitude: 52.504806, - longitude: 13.303846 - }, - products: { - suburban: true, - subway: false, - tram: false, - bus: true, - ferry: false, - express: false, - regional: true - } + location: { /* … */ }, + products: { /* … */ } }, arrival: '2017-12-17T19:47:00.000+01:00', - arrivalPlatform: '8', - arrivalDelay: 30, - line: { - type: 'line', - id: '16845', - name: 'S7', - public: true, - mode: 'train', - product: 'suburban', - symbol: 'S', - nr: 7, - metro: false, - express: false, - night: false, - productCode: 0, - operator: { - type: 'operator', - id: 's-bahn-berlin-gmbh', - name: 'S-Bahn Berlin GmbH' - } - }, - direction: 'S Potsdam Hauptbahnhof', - passed: [ { - station: { - type: 'station', - id: '900000003201', - name: 'S+U Berlin Hauptbahnhof', - location: { /* … */ }, - products: { /* … */ } - }, - arrival: null, - departure: null, - cancelled: true - }, { - station: { - type: 'station', - id: '900000003102', - name: 'S Bellevue', - location: { /* … */ }, - products: { /* … */ } - }, - arrival: '2017-12-17T19:09:00.000+01:00', - departure: '2017-12-17T19:09:00.000+01:00' - }, /* … */ { - station: { - type: 'station', - id: '900000024101', - name: 'S Charlottenburg', - location: { /* … */ }, - products: { /* … */ } - }, - arrival: '2017-12-17T19:17:00.000+01:00', - departure: '2017-12-17T19:17:00.000+01:00' - } ] - } ], - origin: { - type: 'station', - id: '900000003201', - name: 'S+U Berlin Hauptbahnhof', - location: { /* … */ }, - products: { /* … */ } + arrivalDelay: 30 }, - departure: '2017-12-17T19:07:00.000+01:00', - destination: { - type: 'station', - id: '900000024101', - name: 'S Charlottenburg', - location: { /* … */ }, - products: { /* … */ } - }, - arrival: '2017-12-17T19:47:00.000+01:00', - arrivalDelay: 30 -} ] + 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: @@ -242,3 +248,31 @@ Some [profiles](../p) are able to parse the ticket information, if returned by t ``` 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, use `journey.earlierRef`/`journey.laterRef` as follows: + +```js +const hbf = '900000003201' +const heinrichHeineStr = '900000100008' + +client.journeys(hbf, heinrichHeineStr) +.then((journeys) => { + const lastJourney = journeys[journeys.length - 1] + console.log('departure of last journey', lastJourney.departure) + + // get later journeys + return client.journeys(hbf, heinrichHeineStr, { + laterThan: journeys.laterRef + }) +}) +.then((laterourneys) => { + const firstJourney = laterourneys[laterourneys.length - 1] + console.log('departure of first (later) journey', firstJourney.departure) +}) +.catch(console.error) +``` + +``` +departure of last journey 2017-12-17T19:07:00.000+01:00 +departure of first (later) journey 2017-12-17T19:19:00.000+01:00 +``` diff --git a/index.js b/index.js index 1a7ce2a5..5e38e696 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,7 @@ const defaultProfile = require('./lib/default-profile') const _request = require('./lib/request') const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o) +const isNonEmptyString = str => 'string' === typeof str && str.length > 0 const createClient = (profile, request = _request) => { profile = Object.assign({}, defaultProfile, profile) @@ -51,6 +52,29 @@ const createClient = (profile, request = _request) => { from = profile.formatLocation(profile, from) to = profile.formatLocation(profile, to) + if (('earlierThan' in opt) && ('laterThan' in opt)) { + throw new Error('opt.laterThan and opt.laterThan are mutually exclusive.') + } + let journeysRef = null + if ('earlierThan' in opt) { + if (!isNonEmptyString(opt.earlierThan)) { + throw new Error('opt.earlierThan must be a non-empty string.') + } + if ('when' in opt) { + throw new Error('opt.earlierThan and opt.when are mutually exclusive.') + } + journeysRef = opt.earlierThan + } + if ('laterThan' in opt) { + if (!isNonEmptyString(opt.laterThan)) { + throw new Error('opt.laterThan must be a non-empty string.') + } + if ('when' in opt) { + throw new Error('opt.laterThan and opt.when are mutually exclusive.') + } + journeysRef = opt.laterThan + } + opt = Object.assign({ results: 5, // how many journeys? via: null, // let journeys pass this station? @@ -80,6 +104,7 @@ const createClient = (profile, request = _request) => { const query = profile.transformJourneysQuery({ outDate: profile.formatDate(profile, opt.when), outTime: profile.formatTime(profile, opt.when), + ctxScr: journeysRef, numF: opt.results, getPasslist: !!opt.passedStations, maxChg: opt.transfers, @@ -105,12 +130,16 @@ const createClient = (profile, request = _request) => { .then((d) => { if (!Array.isArray(d.outConL)) return [] const parse = profile.parseJourney(profile, d.locations, d.lines, d.remarks) - return d.outConL.map(parse) + const res = d.outConL.map(parse) + + if (d.outCtxScrB) res.earlierRef = d.outCtxScrB + if (d.outCtxScrF) res.laterRef = d.outCtxScrF + return res }) } const locations = (query, opt = {}) => { - if ('string' !== typeof query || !query) { + if (!isNonEmptyString(query)) { throw new Error('query must be a non-empty string.') } opt = Object.assign({ @@ -204,10 +233,10 @@ const createClient = (profile, request = _request) => { } const journeyLeg = (ref, lineName, opt = {}) => { - if ('string' !== typeof ref || !ref) { + if (!isNonEmptyString(ref)) { throw new Error('ref must be a non-empty string.') } - if ('string' !== typeof lineName || !lineName) { + if (!isNonEmptyString(lineName)) { throw new Error('lineName must be a non-empty string.') } opt = Object.assign({ diff --git a/test/db.js b/test/db.js index 17b1a140..f38d2ddd 100644 --- a/test/db.js +++ b/test/db.js @@ -53,7 +53,7 @@ const findStation = (id) => new Promise((yay, nay) => { const isJungfernheide = (s) => { return s.type === 'station' && - (s.id === '008011167' || s.id === '8011167') && + (s.id === '008011167' || s.id === jungfernh) && s.name === 'Berlin Jungfernheide' && s.location && isRoughlyEqual(s.location.latitude, 52.530408, .0005) && @@ -62,7 +62,7 @@ const isJungfernheide = (s) => { const assertIsJungfernheide = (t, s) => { t.equal(s.type, 'station') - t.ok(s.id === '008011167' || s.id === '8011167', 'id should be 8011167') + t.ok(s.id === '008011167' || s.id === jungfernh, 'id should be 8011167') t.equal(s.name, 'Berlin Jungfernheide') t.ok(s.location) t.ok(isRoughlyEqual(s.location.latitude, 52.530408, .0005)) @@ -92,8 +92,14 @@ const assertValidPrice = (t, p) => { const test = tapePromise(tape) const client = createClient(dbProfile) +const jungfernh = '8011167' +const berlinHbf = '8011160' +const münchenHbf = '8000261' +const hannoverHbf = '8000152' +const regensburgHbf = '8000309' + test('Berlin Jungfernheide to München Hbf', co(function* (t) { - const journeys = yield client.journeys('8011167', '8000261', { + const journeys = yield client.journeys(jungfernh, münchenHbf, { when, passedStations: true }) @@ -154,7 +160,7 @@ test('Berlin Jungfernheide to München Hbf', co(function* (t) { })) test('Berlin Jungfernheide to Torfstraße 17', co(function* (t) { - const journeys = yield client.journeys('8011167', { + const journeys = yield client.journeys(jungfernh, { type: 'location', address: 'Torfstraße 17', latitude: 52.5416823, longitude: 13.3491223 }, {when}) @@ -183,7 +189,7 @@ test('Berlin Jungfernheide to Torfstraße 17', co(function* (t) { })) test('Berlin Jungfernheide to ATZE Musiktheater', co(function* (t) { - const journeys = yield client.journeys('8011167', { + const journeys = yield client.journeys(jungfernh, { type: 'location', id: '991598902', name: 'ATZE Musiktheater', latitude: 52.542417, longitude: 13.350437 }, {when}) @@ -212,9 +218,6 @@ test('Berlin Jungfernheide to ATZE Musiktheater', co(function* (t) { })) test('Berlin Hbf to München Hbf with stopover at Hannover Hbf', co(function* (t) { - const berlinHbf = '8011160' - const münchenHbf = '8000261' - const hannoverHbf = '8000152' const [journey] = yield client.journeys(berlinHbf, münchenHbf, { via: hannoverHbf, results: 1 @@ -230,8 +233,58 @@ test('Berlin Hbf to München Hbf with stopover at Hannover Hbf', co(function* (t t.end() })) +test('earlier/later journeys, Jungfernheide -> München Hbf', co(function* (t) { + const model = yield client.journeys(jungfernh, münchenHbf, { + results: 3, when + }) + + t.equal(typeof model.earlierRef, 'string') + t.ok(model.earlierRef) + t.equal(typeof model.laterRef, 'string') + t.ok(model.laterRef) + + // when and earlierThan/laterThan should be mutually exclusive + t.throws(() => { + client.journeys(jungfernh, münchenHbf, { + when, earlierThan: model.earlierRef + }) + }) + t.throws(() => { + client.journeys(jungfernh, münchenHbf, { + when, laterThan: model.laterRef + }) + }) + + let earliestDep = Infinity, latestDep = -Infinity + for (let j of model) { + const dep = +new Date(j.departure) + if (dep < earliestDep) earliestDep = dep + else if (dep > latestDep) latestDep = dep + } + + const earlier = yield client.journeys(jungfernh, münchenHbf, { + results: 3, + // todo: single journey ref? + earlierThan: model.earlierRef + }) + for (let j of earlier) { + t.ok(new Date(j.departure) < earliestDep) + } + + const later = yield client.journeys(jungfernh, münchenHbf, { + results: 3, + // todo: single journey ref? + laterThan: model.laterRef + }) + for (let j of later) { + t.ok(new Date(j.departure) > latestDep) + } + + t.end() +})) + test('departures at Berlin Jungfernheide', co(function* (t) { - const deps = yield client.departures('8011167', { + const deps = yield client.departures(jungfernh, { duration: 5, when }) @@ -252,7 +305,7 @@ test('departures at Berlin Jungfernheide', co(function* (t) { test('departures with station object', co(function* (t) { yield client.departures({ type: 'station', - id: '8011167', + id: jungfernh, name: 'Berlin Jungfernheide', location: { type: 'location', @@ -308,7 +361,6 @@ test('locations named Jungfernheide', co(function* (t) { })) test('location', co(function* (t) { - const regensburgHbf = '8000309' const loc = yield client.location(regensburgHbf) assertValidStation(t, loc) diff --git a/test/oebb.js b/test/oebb.js index 31ba75d4..178c58db 100644 --- a/test/oebb.js +++ b/test/oebb.js @@ -110,9 +110,14 @@ const assertValidLine = (t, l) => { // with optional mode const test = tapePromise(tape) const client = createClient(oebbProfile) +const salzburgHbf = '8100002' +const wienWestbahnhof = '1291501' +const wien = '1190100' +const klagenfurtHbf = '8100085' +const muenchenHbf = '8000261' +const grazHbf = '8100173' + test('Salzburg Hbf to Wien Westbahnhof', co(function* (t) { - const salzburgHbf = '8100002' - const wienWestbahnhof = '1291501' const journeys = yield client.journeys(salzburgHbf, wienWestbahnhof, { when, passedStations: true }) @@ -178,7 +183,6 @@ test('Salzburg Hbf to Wien Westbahnhof', co(function* (t) { })) test('Salzburg Hbf to 1220 Wien, Wagramer Straße 5', co(function* (t) { - const salzburgHbf = '8100002' const wagramerStr = { type: 'location', latitude: 48.236216, @@ -223,7 +227,6 @@ test('Albertina to Salzburg Hbf', co(function* (t) { name: 'Albertina', id: '975900003' } - const salzburgHbf = '8100002' const journeys = yield client.journeys(albertina, salzburgHbf, {when}) t.ok(Array.isArray(journeys)) @@ -255,9 +258,6 @@ test('Albertina to Salzburg Hbf', co(function* (t) { })) test('Wien to Klagenfurt Hbf with stopover at Salzburg Hbf', co(function* (t) { - const wien = '1190100' - const klagenfurtHbf = '8100085' - const salzburgHbf = '8100002' const [journey] = yield client.journeys(wien, klagenfurtHbf, { via: salzburgHbf, results: 1, @@ -274,9 +274,57 @@ test('Wien to Klagenfurt Hbf with stopover at Salzburg Hbf', co(function* (t) { t.end() })) +test('earlier/later journeys, Salzburg Hbf -> Wien Westbahnhof', co(function* (t) { + const model = yield client.journeys(salzburgHbf, wienWestbahnhof, { + results: 3, when + }) + + t.equal(typeof model.earlierRef, 'string') + t.ok(model.earlierRef) + t.equal(typeof model.laterRef, 'string') + t.ok(model.laterRef) + + // when and earlierThan/laterThan should be mutually exclusive + t.throws(() => { + client.journeys(salzburgHbf, wienWestbahnhof, { + when, earlierThan: model.earlierRef + }) + }) + t.throws(() => { + client.journeys(salzburgHbf, wienWestbahnhof, { + when, laterThan: model.laterRef + }) + }) + + let earliestDep = Infinity, latestDep = -Infinity + for (let j of model) { + const dep = +new Date(j.departure) + if (dep < earliestDep) earliestDep = dep + else if (dep > latestDep) latestDep = dep + } + + const earlier = yield client.journeys(salzburgHbf, wienWestbahnhof, { + results: 3, + // todo: single journey ref? + earlierThan: model.earlierRef + }) + for (let j of earlier) { + t.ok(new Date(j.departure) < earliestDep) + } + + const later = yield client.journeys(salzburgHbf, wienWestbahnhof, { + results: 3, + // todo: single journey ref? + laterThan: model.laterRef + }) + for (let j of later) { + t.ok(new Date(j.departure) > latestDep) + } + + t.end() +})) + test('leg details for Wien Westbahnhof to München Hbf', co(function* (t) { - const wienWestbahnhof = '1291501' - const muenchenHbf = '8000261' const journeys = yield client.journeys(wienWestbahnhof, muenchenHbf, { results: 1, when }) @@ -301,7 +349,6 @@ test('leg details for Wien Westbahnhof to München Hbf', co(function* (t) { })) test('departures at Salzburg Hbf', co(function* (t) { - const salzburgHbf = '8100002' const deps = yield client.departures(salzburgHbf, { duration: 5, when }) @@ -365,7 +412,6 @@ test('locations named Salzburg', co(function* (t) { })) test('location', co(function* (t) { - const grazHbf = '8100173' const loc = yield client.location(grazHbf) assertValidStation(t, loc) diff --git a/test/vbb.js b/test/vbb.js index 1205b344..f2b87c22 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -166,6 +166,56 @@ test('journeys – fails with no product', co(function* (t) { } })) +test('earlier/later journeys', co(function* (t) { + const model = yield client.journeys(spichernstr, bismarckstr, { + results: 3, when + }) + + t.equal(typeof model.earlierRef, 'string') + t.ok(model.earlierRef) + t.equal(typeof model.laterRef, 'string') + t.ok(model.laterRef) + + // when and earlierThan/laterThan should be mutually exclusive + t.throws(() => { + client.journeys(spichernstr, bismarckstr, { + when, earlierThan: model.earlierRef + }) + }) + t.throws(() => { + client.journeys(spichernstr, bismarckstr, { + when, laterThan: model.laterRef + }) + }) + + let earliestDep = Infinity, latestDep = -Infinity + for (let j of model) { + const dep = +new Date(j.departure) + if (dep < earliestDep) earliestDep = dep + else if (dep > latestDep) latestDep = dep + } + + const earlier = yield client.journeys(spichernstr, bismarckstr, { + results: 3, + // todo: single journey ref? + earlierThan: model.earlierRef + }) + for (let j of earlier) { + t.ok(new Date(j.departure) < earliestDep) + } + + const later = yield client.journeys(spichernstr, bismarckstr, { + results: 3, + // todo: single journey ref? + laterThan: model.laterRef + }) + for (let j of later) { + t.ok(new Date(j.departure) > latestDep) + } + + t.end() +})) + test('journey leg details', co(function* (t) { const journeys = yield client.journeys(spichernstr, amrumerStr, { results: 1, when From 9f373353275456719a7cd2eb585d819e6fc6d896 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 5 Mar 2018 00:22:31 +0100 Subject: [PATCH 13/25] adapt VBB tests to latest data :green_build: --- test/vbb.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/test/vbb.js b/test/vbb.js index f2b87c22..5c886002 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -244,8 +244,9 @@ test('journey leg details', co(function* (t) { test('journeys – station to address', co(function* (t) { const journeys = yield client.journeys(spichernstr, { - type: 'location', address: 'Torfstraße 17', - latitude: 52.5416823, longitude: 13.3491223 + type: 'location', + address: 'Torfstr. 17, Berlin', + latitude: 52.541797, longitude: 13.350042 }, {results: 1, when}) t.ok(Array.isArray(journeys)) @@ -259,9 +260,9 @@ test('journeys – station to address', co(function* (t) { const dest = leg.destination assertValidAddress(t, dest) - t.strictEqual(dest.address, 'Torfstraße 17') - t.ok(isRoughlyEqual(.0001, dest.latitude, 52.5416823)) - t.ok(isRoughlyEqual(.0001, dest.longitude, 13.3491223)) + t.strictEqual(dest.address, '13353 Berlin-Wedding, Torfstr. 17') + t.ok(isRoughlyEqual(.0001, dest.latitude, 52.541797)) + t.ok(isRoughlyEqual(.0001, dest.longitude, 13.350042)) assertValidWhen(t, leg.arrival, when) t.end() @@ -271,7 +272,9 @@ test('journeys – station to address', co(function* (t) { test('journeys – station to POI', co(function* (t) { const journeys = yield client.journeys(spichernstr, { - type: 'location', id: '9980720', name: 'ATZE Musiktheater', + type: 'location', + id: '900980720', + name: 'Berlin, Atze Musiktheater für Kinder', latitude: 52.543333, longitude: 13.351686 }, {results: 1, when}) @@ -286,7 +289,8 @@ test('journeys – station to POI', co(function* (t) { const dest = leg.destination assertValidPoi(t, dest) - t.strictEqual(dest.name, 'ATZE Musiktheater') + t.strictEqual(dest.id, '900980720') + t.strictEqual(dest.name, 'Berlin, Atze Musiktheater für Kinder') t.ok(isRoughlyEqual(.0001, dest.latitude, 52.543333)) t.ok(isRoughlyEqual(.0001, dest.longitude, 13.351686)) assertValidWhen(t, leg.arrival, when) From 7fd574f0f8519ce843fd324fc444e472969086b9 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 5 Mar 2018 00:23:17 +0100 Subject: [PATCH 14/25] VBB: circumvent CGI_READ_FAILD if numF is used :bug: --- index.js | 119 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 87 insertions(+), 32 deletions(-) diff --git a/index.js b/index.js index 5e38e696..34c3308a 100644 --- a/index.js +++ b/index.js @@ -101,41 +101,96 @@ const createClient = (profile, request = _request) => { filters.push(profile.filters.accessibility[opt.accessibility]) } - const query = profile.transformJourneysQuery({ - outDate: profile.formatDate(profile, opt.when), - outTime: profile.formatTime(profile, opt.when), - ctxScr: journeysRef, - numF: opt.results, - getPasslist: !!opt.passedStations, - maxChg: opt.transfers, - minChgTime: opt.transferTime, - depLocL: [from], - viaLocL: opt.via ? [{loc: opt.via}] : null, - arrLocL: [to], - jnyFltrL: filters, - getTariff: !!opt.tickets, + // With protocol version `1.16`, the VBB endpoint fails with + // `CGI_READ_FAILED` if you pass `numF`, the parameter for the number + // of results. To circumvent this, we loop here, collecting journeys + // until we have enough. + // see https://github.com/derhuerst/hafas-client/pull/23#issuecomment-370246163 + // todo: check if `numF` is supported again, revert this change + const journeys = [] + const more = (when, journeysRef) => { + const query = { + // numF: opt.results, + getPasslist: !!opt.passedStations, + maxChg: opt.transfers, + minChgTime: opt.transferTime, + depLocL: [from], + viaLocL: opt.via ? [{loc: opt.via}] : null, + arrLocL: [to], + jnyFltrL: filters, + getTariff: !!opt.tickets, - // todo: what is req.gisFltrL? - getPT: true, // todo: what is this? - outFrwd: true, // todo: what is this? - getIV: false, // todo: walk & bike as alternatives? - getPolyline: false // todo: shape for displaying on a map? - }, opt) + // todo: what is req.gisFltrL? + getPT: true, // todo: what is this? + outFrwd: true, // todo: what is this? + getIV: false, // todo: walk & bike as alternatives? + getPolyline: false // todo: shape for displaying on a map? + } + if (when) { + query.outDate = profile.formatDate(profile, when) + query.outTime = profile.formatTime(profile, when) + } else if (journeysRef) { + query.ctxScr = journeysRef + } else throw new Error('when or ref required') - return request(profile, { - cfg: {polyEnc: 'GPA'}, - meth: 'TripSearch', - req: query - }) - .then((d) => { - if (!Array.isArray(d.outConL)) return [] - const parse = profile.parseJourney(profile, d.locations, d.lines, d.remarks) - const res = d.outConL.map(parse) + return request(profile, { + cfg: {polyEnc: 'GPA'}, + meth: 'TripSearch', + req: profile.transformJourneysQuery(query, opt) + }) + .then((d) => { + if (!Array.isArray(d.outConL)) return [] + const parse = profile.parseJourney(profile, d.locations, d.lines, d.remarks) + if (!journeys.earlierRef) journeys.earlierRef = d.outCtxScrB - if (d.outCtxScrB) res.earlierRef = d.outCtxScrB - if (d.outCtxScrF) res.laterRef = d.outCtxScrF - return res - }) + for (let j of d.outConL) { + journeys.push(parse(j)) + if (journeys.length === opt.results) { // collected enough + journeys.laterRef = d.outCtxScrF + return journeys + } + } + return more(null, d.outCtxScrF) // otherwise continue + }) + } + + return more(opt.when, journeysRef) + + // const query = profile.transformJourneysQuery({ + // outDate: profile.formatDate(profile, opt.when), + // outTime: profile.formatTime(profile, opt.when), + // ctxScr: journeysRef, + // numF: opt.results, + // getPasslist: !!opt.passedStations, + // maxChg: opt.transfers, + // minChgTime: opt.transferTime, + // depLocL: [from], + // viaLocL: opt.via ? [{loc: opt.via}] : null, + // arrLocL: [to], + // jnyFltrL: filters, + // getTariff: !!opt.tickets, + + // // todo: what is req.gisFltrL? + // getPT: true, // todo: what is this? + // outFrwd: true, // todo: what is this? + // getIV: false, // todo: walk & bike as alternatives? + // getPolyline: false // todo: shape for displaying on a map? + // }, opt) + + // return request(profile, { + // cfg: {polyEnc: 'GPA'}, + // meth: 'TripSearch', + // req: query + // }) + // .then((d) => { + // if (!Array.isArray(d.outConL)) return [] + // const parse = profile.parseJourney(profile, d.locations, d.lines, d.remarks) + // const res = d.outConL.map(parse) + + // if (d.outCtxScrB) res.earlierRef = d.outCtxScrB + // if (d.outCtxScrF) res.laterRef = d.outCtxScrF + // return res + // }) } const locations = (query, opt = {}) => { From 8ae3fe22369db8e8567980798360858cc352db91 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 5 Mar 2018 00:51:11 +0100 Subject: [PATCH 15/25] fix journeys collection :bug: --- index.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 34c3308a..07098f4e 100644 --- a/index.js +++ b/index.js @@ -110,6 +110,9 @@ const createClient = (profile, request = _request) => { const journeys = [] const more = (when, journeysRef) => { const query = { + outDate: profile.formatDate(profile, when), + outTime: profile.formatTime(profile, when), + ctxScr: journeysRef, // numF: opt.results, getPasslist: !!opt.passedStations, maxChg: opt.transfers, @@ -126,12 +129,6 @@ const createClient = (profile, request = _request) => { getIV: false, // todo: walk & bike as alternatives? getPolyline: false // todo: shape for displaying on a map? } - if (when) { - query.outDate = profile.formatDate(profile, when) - query.outTime = profile.formatTime(profile, when) - } else if (journeysRef) { - query.ctxScr = journeysRef - } else throw new Error('when or ref required') return request(profile, { cfg: {polyEnc: 'GPA'}, @@ -143,14 +140,21 @@ const createClient = (profile, request = _request) => { const parse = profile.parseJourney(profile, d.locations, d.lines, d.remarks) if (!journeys.earlierRef) journeys.earlierRef = d.outCtxScrB + let latestDep = -Infinity for (let j of d.outConL) { - journeys.push(parse(j)) + j = parse(j) + journeys.push(j) + if (journeys.length === opt.results) { // collected enough journeys.laterRef = d.outCtxScrF return journeys } + const dep = +new Date(j.departure) + if (dep > latestDep) latestDep = dep } - return more(null, d.outCtxScrF) // otherwise continue + + const when = new Date(latestDep) + return more(when, d.outCtxScrF) // otherwise continue }) } From 084fed729a7ab81ff909335651a83528c5be2c0f Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 5 Mar 2018 00:51:55 +0100 Subject: [PATCH 16/25] remove leftover code :shirt: --- index.js | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/index.js b/index.js index 07098f4e..4861c6b4 100644 --- a/index.js +++ b/index.js @@ -159,42 +159,6 @@ const createClient = (profile, request = _request) => { } return more(opt.when, journeysRef) - - // const query = profile.transformJourneysQuery({ - // outDate: profile.formatDate(profile, opt.when), - // outTime: profile.formatTime(profile, opt.when), - // ctxScr: journeysRef, - // numF: opt.results, - // getPasslist: !!opt.passedStations, - // maxChg: opt.transfers, - // minChgTime: opt.transferTime, - // depLocL: [from], - // viaLocL: opt.via ? [{loc: opt.via}] : null, - // arrLocL: [to], - // jnyFltrL: filters, - // getTariff: !!opt.tickets, - - // // todo: what is req.gisFltrL? - // getPT: true, // todo: what is this? - // outFrwd: true, // todo: what is this? - // getIV: false, // todo: walk & bike as alternatives? - // getPolyline: false // todo: shape for displaying on a map? - // }, opt) - - // return request(profile, { - // cfg: {polyEnc: 'GPA'}, - // meth: 'TripSearch', - // req: query - // }) - // .then((d) => { - // if (!Array.isArray(d.outConL)) return [] - // const parse = profile.parseJourney(profile, d.locations, d.lines, d.remarks) - // const res = d.outConL.map(parse) - - // if (d.outCtxScrB) res.earlierRef = d.outCtxScrB - // if (d.outCtxScrF) res.laterRef = d.outCtxScrF - // return res - // }) } const locations = (query, opt = {}) => { From d8c9fd7a33f018903a34eef2a971b06db587c0a6 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 4 Mar 2018 19:53:53 +0100 Subject: [PATCH 17/25] minor refactoring --- index.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 1a7ce2a5..57bfaaaf 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,7 @@ const defaultProfile = require('./lib/default-profile') const _request = require('./lib/request') const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o) +const isNonEmptyString = str => 'string' === typeof str && str.length > 0 const createClient = (profile, request = _request) => { profile = Object.assign({}, defaultProfile, profile) @@ -110,7 +111,7 @@ const createClient = (profile, request = _request) => { } const locations = (query, opt = {}) => { - if ('string' !== typeof query || !query) { + if (!isNonEmptyString(query)) { throw new Error('query must be a non-empty string.') } opt = Object.assign({ @@ -204,10 +205,10 @@ const createClient = (profile, request = _request) => { } const journeyLeg = (ref, lineName, opt = {}) => { - if ('string' !== typeof ref || !ref) { + if (!isNonEmptyString(ref)) { throw new Error('ref must be a non-empty string.') } - if ('string' !== typeof lineName || !lineName) { + if (!isNonEmptyString(lineName)) { throw new Error('lineName must be a non-empty string.') } opt = Object.assign({ From 56e660d08ed385599013677b6904573468495700 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 4 Mar 2018 19:54:21 +0100 Subject: [PATCH 18/25] beforeJourneys/afterJourneys options --- index.js | 30 +++++++++++++++++++++++++++++- test/vbb.js | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 57bfaaaf..a5d2560e 100644 --- a/index.js +++ b/index.js @@ -52,6 +52,29 @@ const createClient = (profile, request = _request) => { from = profile.formatLocation(profile, from) to = profile.formatLocation(profile, to) + if (('beforeJourneys' in opt) && ('afterJourneys' in opt)) { + throw new Error('opt.afterJourneys and opt.afterJourneys are mutually exclusive.') + } + let journeysRef = null + if ('beforeJourneys' in opt) { + if (!isNonEmptyString(opt.beforeJourneys)) { + throw new Error('opt.beforeJourneys must be a non-empty string.') + } + if ('when' in opt) { + throw new Error('opt.beforeJourneys and opt.when are mutually exclusive.') + } + journeysRef = opt.beforeJourneys + } + if ('afterJourneys' in opt) { + if (!isNonEmptyString(opt.afterJourneys)) { + throw new Error('opt.afterJourneys must be a non-empty string.') + } + if ('when' in opt) { + throw new Error('opt.afterJourneys and opt.when are mutually exclusive.') + } + journeysRef = opt.afterJourneys + } + opt = Object.assign({ results: 5, // how many journeys? via: null, // let journeys pass this station? @@ -81,6 +104,7 @@ const createClient = (profile, request = _request) => { const query = profile.transformJourneysQuery({ outDate: profile.formatDate(profile, opt.when), outTime: profile.formatTime(profile, opt.when), + ctxScr: journeysRef, numF: opt.results, getPasslist: !!opt.passedStations, maxChg: opt.transfers, @@ -106,7 +130,11 @@ const createClient = (profile, request = _request) => { .then((d) => { if (!Array.isArray(d.outConL)) return [] const parse = profile.parseJourney(profile, d.locations, d.lines, d.remarks) - return d.outConL.map(parse) + const res = d.outConL.map(parse) + + if (d.outCtxScrB) res.earlierJourneysRef = d.outCtxScrB + if (d.outCtxScrF) res.laterJourneysRef = d.outCtxScrF + return res }) } diff --git a/test/vbb.js b/test/vbb.js index 4a3ce7fd..3c8dd45f 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -168,6 +168,44 @@ test('journeys – fails with no product', co(function* (t) { } })) +test('earlier/later journeys', co(function* (t) { + const model = yield client.journeys(spichernstr, bismarckstr, { + results: 3, when + }) + + t.equal(typeof model.earlierJourneysRef, 'string') + t.ok(model.earlierJourneysRef) + t.equal(typeof model.laterJourneysRef, 'string') + t.ok(model.laterJourneysRef) + + let earliestDep = Infinity, latestDep = -Infinity + for (let j of model) { + const dep = +new Date(j.departure) + if (dep < earliestDep) earliestDep = dep + else if (dep > latestDep) latestDep = dep + } + + const earlier = yield client.journeys(spichernstr, bismarckstr, { + results: 3, + // todo: single journey ref? + beforeJourneys: model.earlierJourneysRef + }) + for (let j of earlier) { + t.ok(new Date(j.departure) < earliestDep) + } + + const later = yield client.journeys(spichernstr, bismarckstr, { + results: 3, + // todo: single journey ref? + afterJourneys: model.laterJourneysRef + }) + for (let j of later) { + t.ok(new Date(j.departure) > latestDep) + } + + t.end() +})) + test('journey leg details', co(function* (t) { const journeys = yield client.journeys(spichernstr, amrumerStr, { results: 1, when From fe7822883eb4f04ec2161d707245eb44f8658d15 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 4 Mar 2018 20:12:38 +0100 Subject: [PATCH 19/25] beforeJourneys/afterJourneys: more tests :white_check_mark: --- test/db.js | 62 ++++++++++++++++++++++++++++++++++++++++++---------- test/oebb.js | 56 +++++++++++++++++++++++++++++++++++++---------- 2 files changed, 96 insertions(+), 22 deletions(-) diff --git a/test/db.js b/test/db.js index 17b1a140..afb5d797 100644 --- a/test/db.js +++ b/test/db.js @@ -53,7 +53,7 @@ const findStation = (id) => new Promise((yay, nay) => { const isJungfernheide = (s) => { return s.type === 'station' && - (s.id === '008011167' || s.id === '8011167') && + (s.id === '008011167' || s.id === jungfernh) && s.name === 'Berlin Jungfernheide' && s.location && isRoughlyEqual(s.location.latitude, 52.530408, .0005) && @@ -62,7 +62,7 @@ const isJungfernheide = (s) => { const assertIsJungfernheide = (t, s) => { t.equal(s.type, 'station') - t.ok(s.id === '008011167' || s.id === '8011167', 'id should be 8011167') + t.ok(s.id === '008011167' || s.id === jungfernh, 'id should be 8011167') t.equal(s.name, 'Berlin Jungfernheide') t.ok(s.location) t.ok(isRoughlyEqual(s.location.latitude, 52.530408, .0005)) @@ -92,8 +92,14 @@ const assertValidPrice = (t, p) => { const test = tapePromise(tape) const client = createClient(dbProfile) +const jungfernh = '8011167' +const berlinHbf = '8011160' +const münchenHbf = '8000261' +const hannoverHbf = '8000152' +const regensburgHbf = '8000309' + test('Berlin Jungfernheide to München Hbf', co(function* (t) { - const journeys = yield client.journeys('8011167', '8000261', { + const journeys = yield client.journeys(jungfernh, münchenHbf, { when, passedStations: true }) @@ -154,7 +160,7 @@ test('Berlin Jungfernheide to München Hbf', co(function* (t) { })) test('Berlin Jungfernheide to Torfstraße 17', co(function* (t) { - const journeys = yield client.journeys('8011167', { + const journeys = yield client.journeys(jungfernh, { type: 'location', address: 'Torfstraße 17', latitude: 52.5416823, longitude: 13.3491223 }, {when}) @@ -183,7 +189,7 @@ test('Berlin Jungfernheide to Torfstraße 17', co(function* (t) { })) test('Berlin Jungfernheide to ATZE Musiktheater', co(function* (t) { - const journeys = yield client.journeys('8011167', { + const journeys = yield client.journeys(jungfernh, { type: 'location', id: '991598902', name: 'ATZE Musiktheater', latitude: 52.542417, longitude: 13.350437 }, {when}) @@ -212,9 +218,6 @@ test('Berlin Jungfernheide to ATZE Musiktheater', co(function* (t) { })) test('Berlin Hbf to München Hbf with stopover at Hannover Hbf', co(function* (t) { - const berlinHbf = '8011160' - const münchenHbf = '8000261' - const hannoverHbf = '8000152' const [journey] = yield client.journeys(berlinHbf, münchenHbf, { via: hannoverHbf, results: 1 @@ -230,8 +233,46 @@ test('Berlin Hbf to München Hbf with stopover at Hannover Hbf', co(function* (t t.end() })) +test('earlier/later journeys, Jungfernheide -> München Hbf', co(function* (t) { + const model = yield client.journeys(jungfernh, münchenHbf, { + results: 3, when + }) + + t.equal(typeof model.earlierJourneysRef, 'string') + t.ok(model.earlierJourneysRef) + t.equal(typeof model.laterJourneysRef, 'string') + t.ok(model.laterJourneysRef) + + let earliestDep = Infinity, latestDep = -Infinity + for (let j of model) { + const dep = +new Date(j.departure) + if (dep < earliestDep) earliestDep = dep + else if (dep > latestDep) latestDep = dep + } + + const earlier = yield client.journeys(jungfernh, münchenHbf, { + results: 3, + // todo: single journey ref? + beforeJourneys: model.earlierJourneysRef + }) + for (let j of earlier) { + t.ok(new Date(j.departure) < earliestDep) + } + + const later = yield client.journeys(jungfernh, münchenHbf, { + results: 3, + // todo: single journey ref? + afterJourneys: model.laterJourneysRef + }) + for (let j of later) { + t.ok(new Date(j.departure) > latestDep) + } + + t.end() +})) + test('departures at Berlin Jungfernheide', co(function* (t) { - const deps = yield client.departures('8011167', { + const deps = yield client.departures(jungfernh, { duration: 5, when }) @@ -252,7 +293,7 @@ test('departures at Berlin Jungfernheide', co(function* (t) { test('departures with station object', co(function* (t) { yield client.departures({ type: 'station', - id: '8011167', + id: jungfernh, name: 'Berlin Jungfernheide', location: { type: 'location', @@ -308,7 +349,6 @@ test('locations named Jungfernheide', co(function* (t) { })) test('location', co(function* (t) { - const regensburgHbf = '8000309' const loc = yield client.location(regensburgHbf) assertValidStation(t, loc) diff --git a/test/oebb.js b/test/oebb.js index 31ba75d4..2feaf8eb 100644 --- a/test/oebb.js +++ b/test/oebb.js @@ -110,9 +110,14 @@ const assertValidLine = (t, l) => { // with optional mode const test = tapePromise(tape) const client = createClient(oebbProfile) +const salzburgHbf = '8100002' +const wienWestbahnhof = '1291501' +const wien = '1190100' +const klagenfurtHbf = '8100085' +const muenchenHbf = '8000261' +const grazHbf = '8100173' + test('Salzburg Hbf to Wien Westbahnhof', co(function* (t) { - const salzburgHbf = '8100002' - const wienWestbahnhof = '1291501' const journeys = yield client.journeys(salzburgHbf, wienWestbahnhof, { when, passedStations: true }) @@ -178,7 +183,6 @@ test('Salzburg Hbf to Wien Westbahnhof', co(function* (t) { })) test('Salzburg Hbf to 1220 Wien, Wagramer Straße 5', co(function* (t) { - const salzburgHbf = '8100002' const wagramerStr = { type: 'location', latitude: 48.236216, @@ -223,7 +227,6 @@ test('Albertina to Salzburg Hbf', co(function* (t) { name: 'Albertina', id: '975900003' } - const salzburgHbf = '8100002' const journeys = yield client.journeys(albertina, salzburgHbf, {when}) t.ok(Array.isArray(journeys)) @@ -255,9 +258,6 @@ test('Albertina to Salzburg Hbf', co(function* (t) { })) test('Wien to Klagenfurt Hbf with stopover at Salzburg Hbf', co(function* (t) { - const wien = '1190100' - const klagenfurtHbf = '8100085' - const salzburgHbf = '8100002' const [journey] = yield client.journeys(wien, klagenfurtHbf, { via: salzburgHbf, results: 1, @@ -274,9 +274,45 @@ test('Wien to Klagenfurt Hbf with stopover at Salzburg Hbf', co(function* (t) { t.end() })) +test('earlier/later journeys, Salzburg Hbf -> Wien Westbahnhof', co(function* (t) { + const model = yield client.journeys(salzburgHbf, wienWestbahnhof, { + results: 3, when + }) + + t.equal(typeof model.earlierJourneysRef, 'string') + t.ok(model.earlierJourneysRef) + t.equal(typeof model.laterJourneysRef, 'string') + t.ok(model.laterJourneysRef) + + let earliestDep = Infinity, latestDep = -Infinity + for (let j of model) { + const dep = +new Date(j.departure) + if (dep < earliestDep) earliestDep = dep + else if (dep > latestDep) latestDep = dep + } + + const earlier = yield client.journeys(salzburgHbf, wienWestbahnhof, { + results: 3, + // todo: single journey ref? + beforeJourneys: model.earlierJourneysRef + }) + for (let j of earlier) { + t.ok(new Date(j.departure) < earliestDep) + } + + const later = yield client.journeys(salzburgHbf, wienWestbahnhof, { + results: 3, + // todo: single journey ref? + afterJourneys: model.laterJourneysRef + }) + for (let j of later) { + t.ok(new Date(j.departure) > latestDep) + } + + t.end() +})) + test('leg details for Wien Westbahnhof to München Hbf', co(function* (t) { - const wienWestbahnhof = '1291501' - const muenchenHbf = '8000261' const journeys = yield client.journeys(wienWestbahnhof, muenchenHbf, { results: 1, when }) @@ -301,7 +337,6 @@ test('leg details for Wien Westbahnhof to München Hbf', co(function* (t) { })) test('departures at Salzburg Hbf', co(function* (t) { - const salzburgHbf = '8100002' const deps = yield client.departures(salzburgHbf, { duration: 5, when }) @@ -365,7 +400,6 @@ test('locations named Salzburg', co(function* (t) { })) test('location', co(function* (t) { - const grazHbf = '8100173' const loc = yield client.location(grazHbf) assertValidStation(t, loc) From 88c511da27e208739fcc3908c460234bf6531be9 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 4 Mar 2018 20:38:50 +0100 Subject: [PATCH 20/25] earlierJourneysRef -> earlierRef, laterJourneysRef -> laterRef --- index.js | 4 ++-- test/db.js | 12 ++++++------ test/oebb.js | 12 ++++++------ test/vbb.js | 12 ++++++------ 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/index.js b/index.js index a5d2560e..21826517 100644 --- a/index.js +++ b/index.js @@ -132,8 +132,8 @@ const createClient = (profile, request = _request) => { const parse = profile.parseJourney(profile, d.locations, d.lines, d.remarks) const res = d.outConL.map(parse) - if (d.outCtxScrB) res.earlierJourneysRef = d.outCtxScrB - if (d.outCtxScrF) res.laterJourneysRef = d.outCtxScrF + if (d.outCtxScrB) res.earlierRef = d.outCtxScrB + if (d.outCtxScrF) res.laterRef = d.outCtxScrF return res }) } diff --git a/test/db.js b/test/db.js index afb5d797..56ff0ce2 100644 --- a/test/db.js +++ b/test/db.js @@ -238,10 +238,10 @@ test('earlier/later journeys, Jungfernheide -> München Hbf', co(function* (t) { results: 3, when }) - t.equal(typeof model.earlierJourneysRef, 'string') - t.ok(model.earlierJourneysRef) - t.equal(typeof model.laterJourneysRef, 'string') - t.ok(model.laterJourneysRef) + t.equal(typeof model.earlierRef, 'string') + t.ok(model.earlierRef) + t.equal(typeof model.laterRef, 'string') + t.ok(model.laterRef) let earliestDep = Infinity, latestDep = -Infinity for (let j of model) { @@ -253,7 +253,7 @@ test('earlier/later journeys, Jungfernheide -> München Hbf', co(function* (t) { const earlier = yield client.journeys(jungfernh, münchenHbf, { results: 3, // todo: single journey ref? - beforeJourneys: model.earlierJourneysRef + beforeJourneys: model.earlierRef }) for (let j of earlier) { t.ok(new Date(j.departure) < earliestDep) @@ -262,7 +262,7 @@ test('earlier/later journeys, Jungfernheide -> München Hbf', co(function* (t) { const later = yield client.journeys(jungfernh, münchenHbf, { results: 3, // todo: single journey ref? - afterJourneys: model.laterJourneysRef + afterJourneys: model.laterRef }) for (let j of later) { t.ok(new Date(j.departure) > latestDep) diff --git a/test/oebb.js b/test/oebb.js index 2feaf8eb..eb7bd7ec 100644 --- a/test/oebb.js +++ b/test/oebb.js @@ -279,10 +279,10 @@ test('earlier/later journeys, Salzburg Hbf -> Wien Westbahnhof', co(function* (t results: 3, when }) - t.equal(typeof model.earlierJourneysRef, 'string') - t.ok(model.earlierJourneysRef) - t.equal(typeof model.laterJourneysRef, 'string') - t.ok(model.laterJourneysRef) + t.equal(typeof model.earlierRef, 'string') + t.ok(model.earlierRef) + t.equal(typeof model.laterRef, 'string') + t.ok(model.laterRef) let earliestDep = Infinity, latestDep = -Infinity for (let j of model) { @@ -294,7 +294,7 @@ test('earlier/later journeys, Salzburg Hbf -> Wien Westbahnhof', co(function* (t const earlier = yield client.journeys(salzburgHbf, wienWestbahnhof, { results: 3, // todo: single journey ref? - beforeJourneys: model.earlierJourneysRef + beforeJourneys: model.earlierRef }) for (let j of earlier) { t.ok(new Date(j.departure) < earliestDep) @@ -303,7 +303,7 @@ test('earlier/later journeys, Salzburg Hbf -> Wien Westbahnhof', co(function* (t const later = yield client.journeys(salzburgHbf, wienWestbahnhof, { results: 3, // todo: single journey ref? - afterJourneys: model.laterJourneysRef + afterJourneys: model.laterRef }) for (let j of later) { t.ok(new Date(j.departure) > latestDep) diff --git a/test/vbb.js b/test/vbb.js index 3c8dd45f..908ae980 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -173,10 +173,10 @@ test('earlier/later journeys', co(function* (t) { results: 3, when }) - t.equal(typeof model.earlierJourneysRef, 'string') - t.ok(model.earlierJourneysRef) - t.equal(typeof model.laterJourneysRef, 'string') - t.ok(model.laterJourneysRef) + t.equal(typeof model.earlierRef, 'string') + t.ok(model.earlierRef) + t.equal(typeof model.laterRef, 'string') + t.ok(model.laterRef) let earliestDep = Infinity, latestDep = -Infinity for (let j of model) { @@ -188,7 +188,7 @@ test('earlier/later journeys', co(function* (t) { const earlier = yield client.journeys(spichernstr, bismarckstr, { results: 3, // todo: single journey ref? - beforeJourneys: model.earlierJourneysRef + beforeJourneys: model.earlierRef }) for (let j of earlier) { t.ok(new Date(j.departure) < earliestDep) @@ -197,7 +197,7 @@ test('earlier/later journeys', co(function* (t) { const later = yield client.journeys(spichernstr, bismarckstr, { results: 3, // todo: single journey ref? - afterJourneys: model.laterJourneysRef + afterJourneys: model.laterRef }) for (let j of later) { t.ok(new Date(j.departure) > latestDep) From c835467d855fa7fa049b5c54d4fb9ce88eaae832 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 4 Mar 2018 20:41:26 +0100 Subject: [PATCH 21/25] beforeJourneys -> earlierThan, afterJourneys -> laterThan --- index.js | 24 ++++++++++++------------ test/db.js | 16 ++++++++++++++-- test/oebb.js | 16 ++++++++++++++-- test/vbb.js | 16 ++++++++++++++-- 4 files changed, 54 insertions(+), 18 deletions(-) diff --git a/index.js b/index.js index 21826517..5e38e696 100644 --- a/index.js +++ b/index.js @@ -52,27 +52,27 @@ const createClient = (profile, request = _request) => { from = profile.formatLocation(profile, from) to = profile.formatLocation(profile, to) - if (('beforeJourneys' in opt) && ('afterJourneys' in opt)) { - throw new Error('opt.afterJourneys and opt.afterJourneys are mutually exclusive.') + if (('earlierThan' in opt) && ('laterThan' in opt)) { + throw new Error('opt.laterThan and opt.laterThan are mutually exclusive.') } let journeysRef = null - if ('beforeJourneys' in opt) { - if (!isNonEmptyString(opt.beforeJourneys)) { - throw new Error('opt.beforeJourneys must be a non-empty string.') + if ('earlierThan' in opt) { + if (!isNonEmptyString(opt.earlierThan)) { + throw new Error('opt.earlierThan must be a non-empty string.') } if ('when' in opt) { - throw new Error('opt.beforeJourneys and opt.when are mutually exclusive.') + throw new Error('opt.earlierThan and opt.when are mutually exclusive.') } - journeysRef = opt.beforeJourneys + journeysRef = opt.earlierThan } - if ('afterJourneys' in opt) { - if (!isNonEmptyString(opt.afterJourneys)) { - throw new Error('opt.afterJourneys must be a non-empty string.') + if ('laterThan' in opt) { + if (!isNonEmptyString(opt.laterThan)) { + throw new Error('opt.laterThan must be a non-empty string.') } if ('when' in opt) { - throw new Error('opt.afterJourneys and opt.when are mutually exclusive.') + throw new Error('opt.laterThan and opt.when are mutually exclusive.') } - journeysRef = opt.afterJourneys + journeysRef = opt.laterThan } opt = Object.assign({ diff --git a/test/db.js b/test/db.js index 56ff0ce2..f38d2ddd 100644 --- a/test/db.js +++ b/test/db.js @@ -243,6 +243,18 @@ test('earlier/later journeys, Jungfernheide -> München Hbf', co(function* (t) { t.equal(typeof model.laterRef, 'string') t.ok(model.laterRef) + // when and earlierThan/laterThan should be mutually exclusive + t.throws(() => { + client.journeys(jungfernh, münchenHbf, { + when, earlierThan: model.earlierRef + }) + }) + t.throws(() => { + client.journeys(jungfernh, münchenHbf, { + when, laterThan: model.laterRef + }) + }) + let earliestDep = Infinity, latestDep = -Infinity for (let j of model) { const dep = +new Date(j.departure) @@ -253,7 +265,7 @@ test('earlier/later journeys, Jungfernheide -> München Hbf', co(function* (t) { const earlier = yield client.journeys(jungfernh, münchenHbf, { results: 3, // todo: single journey ref? - beforeJourneys: model.earlierRef + earlierThan: model.earlierRef }) for (let j of earlier) { t.ok(new Date(j.departure) < earliestDep) @@ -262,7 +274,7 @@ test('earlier/later journeys, Jungfernheide -> München Hbf', co(function* (t) { const later = yield client.journeys(jungfernh, münchenHbf, { results: 3, // todo: single journey ref? - afterJourneys: model.laterRef + laterThan: model.laterRef }) for (let j of later) { t.ok(new Date(j.departure) > latestDep) diff --git a/test/oebb.js b/test/oebb.js index eb7bd7ec..178c58db 100644 --- a/test/oebb.js +++ b/test/oebb.js @@ -284,6 +284,18 @@ test('earlier/later journeys, Salzburg Hbf -> Wien Westbahnhof', co(function* (t t.equal(typeof model.laterRef, 'string') t.ok(model.laterRef) + // when and earlierThan/laterThan should be mutually exclusive + t.throws(() => { + client.journeys(salzburgHbf, wienWestbahnhof, { + when, earlierThan: model.earlierRef + }) + }) + t.throws(() => { + client.journeys(salzburgHbf, wienWestbahnhof, { + when, laterThan: model.laterRef + }) + }) + let earliestDep = Infinity, latestDep = -Infinity for (let j of model) { const dep = +new Date(j.departure) @@ -294,7 +306,7 @@ test('earlier/later journeys, Salzburg Hbf -> Wien Westbahnhof', co(function* (t const earlier = yield client.journeys(salzburgHbf, wienWestbahnhof, { results: 3, // todo: single journey ref? - beforeJourneys: model.earlierRef + earlierThan: model.earlierRef }) for (let j of earlier) { t.ok(new Date(j.departure) < earliestDep) @@ -303,7 +315,7 @@ test('earlier/later journeys, Salzburg Hbf -> Wien Westbahnhof', co(function* (t const later = yield client.journeys(salzburgHbf, wienWestbahnhof, { results: 3, // todo: single journey ref? - afterJourneys: model.laterRef + laterThan: model.laterRef }) for (let j of later) { t.ok(new Date(j.departure) > latestDep) diff --git a/test/vbb.js b/test/vbb.js index 908ae980..50a86e5f 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -178,6 +178,18 @@ test('earlier/later journeys', co(function* (t) { t.equal(typeof model.laterRef, 'string') t.ok(model.laterRef) + // when and earlierThan/laterThan should be mutually exclusive + t.throws(() => { + client.journeys(spichernstr, bismarckstr, { + when, earlierThan: model.earlierRef + }) + }) + t.throws(() => { + client.journeys(spichernstr, bismarckstr, { + when, laterThan: model.laterRef + }) + }) + let earliestDep = Infinity, latestDep = -Infinity for (let j of model) { const dep = +new Date(j.departure) @@ -188,7 +200,7 @@ test('earlier/later journeys', co(function* (t) { const earlier = yield client.journeys(spichernstr, bismarckstr, { results: 3, // todo: single journey ref? - beforeJourneys: model.earlierRef + earlierThan: model.earlierRef }) for (let j of earlier) { t.ok(new Date(j.departure) < earliestDep) @@ -197,7 +209,7 @@ test('earlier/later journeys', co(function* (t) { const later = yield client.journeys(spichernstr, bismarckstr, { results: 3, // todo: single journey ref? - afterJourneys: model.laterRef + laterThan: model.laterRef }) for (let j of later) { t.ok(new Date(j.departure) > latestDep) From 1aac40079abb015797dfadb9ee790692a39e9ef2 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 4 Mar 2018 23:25:08 +0100 Subject: [PATCH 22/25] earlierThan/laterThan: docs :memo: --- docs/journeys.md | 242 +++++++++++++++++++++++++++-------------------- 1 file changed, 138 insertions(+), 104 deletions(-) diff --git a/docs/journeys.md b/docs/journeys.md index 86446da8..0656934c 100644 --- a/docs/journeys.md +++ b/docs/journeys.md @@ -41,6 +41,8 @@ With `opt`, you can override the default options, which look like this: ```js { when: new Date(), + earlierThan: null, // ref to get journeys earlier than the last query + laterThan: null, // ref to get journeys later than the last query results: 5, // how many journeys? via: null, // let journeys pass this station passedStations: false, // return stations on the way? @@ -85,123 +87,127 @@ client.journeys('900000003201', '900000100008', { The response may look like this: ```js -[ { - legs: [ { - id: '1|31041|35|86|17122017', +[ + { + legs: [ { + id: '1|31041|35|86|17122017', + origin: { + type: 'station', + id: '900000003201', + name: 'S+U Berlin Hauptbahnhof', + location: { + type: 'location', + latitude: 52.52585, + longitude: 13.368928 + }, + products: { + suburban: true, + subway: true, + tram: true, + bus: true, + ferry: false, + express: true, + regional: true + } + }, + departure: '2017-12-17T19:07:00.000+01:00', + departurePlatform: '16', + destination: { + type: 'station', + id: '900000024101', + name: 'S Charlottenburg', + location: { + type: 'location', + latitude: 52.504806, + longitude: 13.303846 + }, + products: { + suburban: true, + subway: false, + tram: false, + bus: true, + ferry: false, + express: false, + regional: true + } + }, + arrival: '2017-12-17T19:47:00.000+01:00', + arrivalPlatform: '8', + arrivalDelay: 30, + line: { + type: 'line', + id: '16845', + name: 'S7', + public: true, + mode: 'train', + product: 'suburban', + symbol: 'S', + nr: 7, + metro: false, + express: false, + night: false, + productCode: 0, + operator: { + type: 'operator', + id: 's-bahn-berlin-gmbh', + name: 'S-Bahn Berlin GmbH' + } + }, + direction: 'S Potsdam Hauptbahnhof', + passed: [ { + station: { + type: 'station', + id: '900000003201', + name: 'S+U Berlin Hauptbahnhof', + location: { /* … */ }, + products: { /* … */ } + }, + arrival: null, + departure: null, + cancelled: true + }, { + station: { + type: 'station', + id: '900000003102', + name: 'S Bellevue', + location: { /* … */ }, + products: { /* … */ } + }, + arrival: '2017-12-17T19:09:00.000+01:00', + departure: '2017-12-17T19:09:00.000+01:00' + }, /* … */ { + station: { + type: 'station', + id: '900000024101', + name: 'S Charlottenburg', + location: { /* … */ }, + products: { /* … */ } + }, + arrival: '2017-12-17T19:17:00.000+01:00', + departure: '2017-12-17T19:17:00.000+01:00' + } ] + } ], origin: { type: 'station', id: '900000003201', name: 'S+U Berlin Hauptbahnhof', - location: { - type: 'location', - latitude: 52.52585, - longitude: 13.368928 - }, - products: { - suburban: true, - subway: true, - tram: true, - bus: true, - ferry: false, - express: true, - regional: true - } + location: { /* … */ }, + products: { /* … */ } }, departure: '2017-12-17T19:07:00.000+01:00', - departurePlatform: '16', destination: { type: 'station', id: '900000024101', name: 'S Charlottenburg', - location: { - type: 'location', - latitude: 52.504806, - longitude: 13.303846 - }, - products: { - suburban: true, - subway: false, - tram: false, - bus: true, - ferry: false, - express: false, - regional: true - } + location: { /* … */ }, + products: { /* … */ } }, arrival: '2017-12-17T19:47:00.000+01:00', - arrivalPlatform: '8', - arrivalDelay: 30, - line: { - type: 'line', - id: '16845', - name: 'S7', - public: true, - mode: 'train', - product: 'suburban', - symbol: 'S', - nr: 7, - metro: false, - express: false, - night: false, - productCode: 0, - operator: { - type: 'operator', - id: 's-bahn-berlin-gmbh', - name: 'S-Bahn Berlin GmbH' - } - }, - direction: 'S Potsdam Hauptbahnhof', - passed: [ { - station: { - type: 'station', - id: '900000003201', - name: 'S+U Berlin Hauptbahnhof', - location: { /* … */ }, - products: { /* … */ } - }, - arrival: null, - departure: null, - cancelled: true - }, { - station: { - type: 'station', - id: '900000003102', - name: 'S Bellevue', - location: { /* … */ }, - products: { /* … */ } - }, - arrival: '2017-12-17T19:09:00.000+01:00', - departure: '2017-12-17T19:09:00.000+01:00' - }, /* … */ { - station: { - type: 'station', - id: '900000024101', - name: 'S Charlottenburg', - location: { /* … */ }, - products: { /* … */ } - }, - arrival: '2017-12-17T19:17:00.000+01:00', - departure: '2017-12-17T19:17:00.000+01:00' - } ] - } ], - origin: { - type: 'station', - id: '900000003201', - name: 'S+U Berlin Hauptbahnhof', - location: { /* … */ }, - products: { /* … */ } + arrivalDelay: 30 }, - departure: '2017-12-17T19:07:00.000+01:00', - destination: { - type: 'station', - id: '900000024101', - name: 'S Charlottenburg', - location: { /* … */ }, - products: { /* … */ } - }, - arrival: '2017-12-17T19:47:00.000+01:00', - arrivalDelay: 30 -} ] + 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: @@ -242,3 +248,31 @@ Some [profiles](../p) are able to parse the ticket information, if returned by t ``` 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, use `journey.earlierRef`/`journey.laterRef` as follows: + +```js +const hbf = '900000003201' +const heinrichHeineStr = '900000100008' + +client.journeys(hbf, heinrichHeineStr) +.then((journeys) => { + const lastJourney = journeys[journeys.length - 1] + console.log('departure of last journey', lastJourney.departure) + + // get later journeys + return client.journeys(hbf, heinrichHeineStr, { + laterThan: journeys.laterRef + }) +}) +.then((laterourneys) => { + const firstJourney = laterourneys[laterourneys.length - 1] + console.log('departure of first (later) journey', firstJourney.departure) +}) +.catch(console.error) +``` + +``` +departure of last journey 2017-12-17T19:07:00.000+01:00 +departure of first (later) journey 2017-12-17T19:19:00.000+01:00 +``` From ca382f1c163dd78fd601900bdc19323f219f2cbd Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 5 Mar 2018 20:43:31 +0100 Subject: [PATCH 23/25] earlierThan/laterThan: clarify docs :memo: --- docs/journeys.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/journeys.md b/docs/journeys.md index 0656934c..2dc7fb49 100644 --- a/docs/journeys.md +++ b/docs/journeys.md @@ -249,7 +249,7 @@ Some [profiles](../p) are able to parse the ticket information, if returned by t 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, use `journey.earlierRef`/`journey.laterRef` as follows: +To get more journeys earlier/later than the current set of results, pass `journeys.earlierRef`/`journeys.laterRef` into `opt.earlierThan`/`opt.laterThan`. For example, query *later* journeys as follows: ```js const hbf = '900000003201' From 0c3e3cb0cfdd28d9b317a28ab76301e2412ad103 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Wed, 7 Mar 2018 13:32:40 +0100 Subject: [PATCH 24/25] journeys collection as an option --- index.js | 2 +- lib/default-profile.js | 1 + p/vbb/index.js | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 4861c6b4..158d0151 100644 --- a/index.js +++ b/index.js @@ -113,7 +113,6 @@ const createClient = (profile, request = _request) => { outDate: profile.formatDate(profile, when), outTime: profile.formatTime(profile, when), ctxScr: journeysRef, - // numF: opt.results, getPasslist: !!opt.passedStations, maxChg: opt.transfers, minChgTime: opt.transferTime, @@ -129,6 +128,7 @@ const createClient = (profile, request = _request) => { getIV: false, // todo: walk & bike as alternatives? getPolyline: false // todo: shape for displaying on a map? } + if (profile.journeysNumF) query.numF = opt.results return request(profile, { cfg: {polyEnc: 'GPA'}, diff --git a/lib/default-profile.js b/lib/default-profile.js index 7abcf60d..cf8493d8 100644 --- a/lib/default-profile.js +++ b/lib/default-profile.js @@ -59,6 +59,7 @@ const defaultProfile = { formatRectangle, filters, + journeysNumF: true, // `journeys()` method: support for `numF` field journeyLeg: false, radar: false } diff --git a/p/vbb/index.js b/p/vbb/index.js index 8ec996a2..307ba0f4 100644 --- a/p/vbb/index.js +++ b/p/vbb/index.js @@ -189,6 +189,7 @@ const vbbProfile = { formatStation, formatProducts, + journeysNumF: false, journeyLeg: true, radar: true } From 7ce65ca480c9f199f80065e20833ea502ee89f4b Mon Sep 17 00:00:00 2001 From: Jannis R Date: Thu, 8 Mar 2018 03:26:39 +0100 Subject: [PATCH 25/25] departures, journeys: canceled, former scheduled time see public-transport/friendly-public-transport-format#27 --- parse/departure.js | 4 ++++ parse/journey-leg.js | 6 ++++++ parse/journey.js | 9 +++++++++ 3 files changed, 19 insertions(+) diff --git a/parse/departure.js b/parse/departure.js index e8f2d9bf..b074b426 100644 --- a/parse/departure.js +++ b/parse/departure.js @@ -32,7 +32,11 @@ const createParseDeparture = (profile, stations, lines, remarks) => { // see also derhuerst/vbb-rest#19 if (d.stbStop.aCncl || d.stbStop.dCncl) { res.cancelled = true + Object.defineProperty(res, 'canceled', {value: true}) res.when = res.delay = null + + const when = profile.parseDateTime(profile, d.date, d.stbStop.dTimeS) + res.formerScheduledWhen = when.toISO() } return res diff --git a/parse/journey-leg.js b/parse/journey-leg.js index fac9ac44..3c0651bc 100644 --- a/parse/journey-leg.js +++ b/parse/journey-leg.js @@ -75,11 +75,17 @@ const createParseJourneyLeg = (profile, stations, lines, remarks) => { // see also derhuerst/vbb-rest#19 if (pt.arr.aCncl) { res.cancelled = true + Object.defineProperty(res, 'canceled', {value: true}) res.arrival = res.arrivalPlatform = res.arrivalDelay = null + const arr = profile.parseDateTime(profile, j.date, pt.arr.aTimeS) + res.formerScheduledArrival = arr.toISO() } if (pt.dep.dCncl) { res.cancelled = true + Object.defineProperty(res, 'canceled', {value: true}) res.departure = res.departurePlatform = res.departureDelay = null + const dep = profile.parseDateTime(profile, j.date, pt.dep.dTimeS) + res.formerScheduledDeparture = dep.toISO() } return res diff --git a/parse/journey.js b/parse/journey.js index 340991f4..b0a133e9 100644 --- a/parse/journey.js +++ b/parse/journey.js @@ -23,7 +23,16 @@ const createParseJourney = (profile, stations, lines, remarks) => { } if (legs.some(p => p.cancelled)) { res.cancelled = true + Object.defineProperty(res, 'canceled', {value: true}) res.departure = res.arrival = null + + const firstLeg = j.secL[0] + const dep = profile.parseDateTime(profile, j.date, firstLeg.dep.dTimeS) + res.formerScheduledDeparture = dep.toISO() + + const lastLeg = j.secL[j.secL.length - 1] + const arr = profile.parseDateTime(profile, j.date, lastLeg.arr.aTimeS) + res.formerScheduledArrival = arr.toISO() } return res