diff --git a/docs/journeys.md b/docs/journeys.md index 86446da8..2dc7fb49 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, pass `journeys.earlierRef`/`journeys.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((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/format/location-identifier.js b/format/location-identifier.js index b7529aec..87cfcde6 100644 --- a/format/location-identifier.js +++ b/format/location-identifier.js @@ -7,7 +7,7 @@ const formatLocationIdentifier = (data) => { for (let key in data) { if (!Object.prototype.hasOwnProperty.call(data, key)) continue - str += key + '=' + data[key] + sep // todo: escape + str += key + '=' + data[key] + sep // todo: escape, but how? } return str diff --git a/format/poi.js b/format/poi.js index b1577a01..22abe0f5 100644 --- a/format/poi.js +++ b/format/poi.js @@ -13,9 +13,9 @@ const formatPoi = (p) => { lid: formatLocationIdentifier({ A: '4', // POI O: p.name, + L: p.id, X: formatCoord(p.longitude), - Y: formatCoord(p.latitude), - L: p.id + Y: formatCoord(p.latitude) }) } } 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/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 diff --git a/index.js b/index.js index 91c9cf50..158d0151 100644 --- a/index.js +++ b/index.js @@ -7,12 +7,15 @@ 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 isNonEmptyString = str => 'string' === typeof str && str.length > 0 + 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.') @@ -49,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? @@ -75,40 +101,70 @@ 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), - 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 = { + outDate: profile.formatDate(profile, when), + outTime: profile.formatTime(profile, when), + ctxScr: journeysRef, + 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 (profile.journeysNumF) query.numF = opt.results - return request(profile, { - cfg: {polyEnc: 'GPA'}, - meth: 'TripSearch', - req: query - }) - .then((d) => { - if (!Array.isArray(d.outConL)) return [] - const parse = profile.parseJourney(profile, d.locations, d.lines, d.remarks) - return d.outConL.map(parse) - }) + 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 + + let latestDep = -Infinity + for (let j of d.outConL) { + 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 + } + + const when = new Date(latestDep) + return more(when, d.outCtxScrF) // otherwise continue + }) + } + + return more(opt.when, journeysRef) } const locations = (query, opt = {}) => { - if ('string' !== typeof query) throw new Error('query must be a string.') + if (!isNonEmptyString(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 +214,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 +256,12 @@ const createClient = (profile, request = _request) => { } const journeyLeg = (ref, lineName, opt = {}) => { + if (!isNonEmptyString(ref)) { + throw new Error('ref must be a non-empty string.') + } + if (!isNonEmptyString(lineName)) { + throw new Error('lineName must be a non-empty string.') + } opt = Object.assign({ passedStations: true // return stations on the way? }, opt) diff --git a/lib/default-profile.js b/lib/default-profile.js index ec2a09fe..cf8493d8 100644 --- a/lib/default-profile.js +++ b/lib/default-profile.js @@ -26,6 +26,10 @@ const filters = require('../format/filters') const id = x => x const defaultProfile = { + salt: null, + addChecksum: false, + addMicMac: false, + transformReqBody: id, transformReq: id, @@ -55,6 +59,7 @@ const defaultProfile = { formatRectangle, filters, + journeysNumF: true, // `journeys()` method: support for `numF` field journeyLeg: false, radar: false } diff --git a/lib/request.js b/lib/request.js index 52600212..96d5da5f 100644 --- a/lib/request.js +++ b/lib/request.js @@ -1,5 +1,6 @@ 'use strict' +const crypto = require('crypto') let captureStackTrace = () => {} if (process.env.NODE_ENV === 'dev') { captureStackTrace = require('capture-stack-trace') @@ -8,6 +9,8 @@ const {stringify} = require('query-string') const Promise = require('pinkie-promise') const {fetch} = require('fetch-ponyfill')({Promise}) +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({ @@ -19,14 +22,37 @@ 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 || profile.addMicMac) { + if (!Buffer.isBuffer(profile.salt)) { + throw new Error('profile.salt must be a Buffer.') + } + 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 micAsHex = Buffer.from(mic.toString('hex'), 'utf8') + const mac = md5(Buffer.concat([micAsHex, profile.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 err.request = body + err.url = url captureStackTrace(err) return fetch(url, req) 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, diff --git a/p/vbb/index.js b/p/vbb/index.js index 6af748f0..307ba0f4 100644 --- a/p/vbb/index.js +++ b/p/vbb/index.js @@ -20,10 +20,10 @@ const modes = require('./modes') const formatBitmask = createFormatBitmask(modes) const transformReqBody = (body) => { - body.client = {type: 'IPA', id: 'BVG', name: 'FahrInfo', v: '4070700'} - body.ext = 'BVG.1' - body.ver = '1.15' // todo: 1.16 with `mic` and `mac` query params - body.auth = {type: 'AID', aid: '1Rxs112shyHLatUX4fofnmdxK'} + body.client = {type: 'IPA', id: 'VBB', name: 'vbbPROD', v: '4010300'} + body.ext = 'VBB.1' + body.ver = '1.16' + body.auth = {type: 'AID', aid: 'hafas-vbb-apps'} return body } @@ -167,7 +167,13 @@ const formatProducts = (products) => { const vbbProfile = { locale: 'de-DE', timezone: 'Europe/Berlin', - endpoint: 'https://bvg-apps.hafas.de/bin/mgate.exe', + 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, @@ -183,6 +189,7 @@ const vbbProfile = { formatStation, formatProducts, + journeysNumF: false, journeyLeg: true, radar: true } 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", 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 2463c114..b0a133e9 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, @@ -22,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 diff --git a/readme.md b/readme.md index 91aa8a52..a0f71162 100644 --- a/readme.md +++ b/readme.md @@ -175,6 +175,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) diff --git a/test/db.js b/test/db.js index d8a7d640..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,14 +92,22 @@ 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 }) 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))) { @@ -152,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}) @@ -181,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}) @@ -210,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 @@ -228,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 }) @@ -250,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', @@ -306,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 90a7b350..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 }) @@ -120,6 +125,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 @@ -176,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, @@ -221,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)) @@ -253,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, @@ -272,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 }) @@ -299,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 }) @@ -363,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 8d562c81..29b41d63 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) @@ -146,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, @@ -158,12 +160,64 @@ 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() } })) +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 @@ -192,8 +246,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)) @@ -207,9 +262,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() @@ -219,7 +274,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}) @@ -234,7 +291,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)