From fd0dec26a51f33b009b5466d0b2f6212c39952b0 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sat, 11 Nov 2017 21:06:54 +0100 Subject: [PATCH 01/73] prepare for larger changes --- .travis.yml | 2 +- package.json | 7 ++++--- readme.md | 8 ++++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index d707c8bf..35211681 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,5 +2,5 @@ sudo: false language: node_js node_js: - 'stable' - - '7' + - '8' - '6' diff --git a/package.json b/package.json index 8ad11ad1..031d9334 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hafas-client", - "description": "JavaScript client for HAFAS mobile APIs.", - "version": "1.2.6", + "description": "JavaScript client for HAFAS public transport APIs.", + "version": "2.0.0", "main": "index.js", "files": [ "index.js", @@ -17,8 +17,9 @@ "hafas", "public", "transport", + "transit", "api", - "mgate" + "http" ], "engines": { "node": ">=6" diff --git a/readme.md b/readme.md index 36460fde..a9ac4058 100644 --- a/readme.md +++ b/readme.md @@ -1,11 +1,9 @@ # hafas-client -**A client for HAFAS mobile APIs**, providing the base for [vbb-hafas](https://github.com/derhuerst/vbb-hafas) and [db-hafas](https://github.com/derhuerst/db-hafas). +**A client for HAFAS public transport APIs**, providing the base for [vbb-hafas](https://github.com/derhuerst/vbb-hafas) and [db-hafas](https://github.com/derhuerst/db-hafas). [![npm version](https://img.shields.io/npm/v/hafas-client.svg)](https://www.npmjs.com/package/hafas-client) [![build status](https://img.shields.io/travis/derhuerst/hafas-client.svg)](https://travis-ci.org/derhuerst/hafas-client) -[![dependency status](https://img.shields.io/david/derhuerst/hafas-client.svg)](https://david-dm.org/derhuerst/hafas-client) -[![dev dependency status](https://img.shields.io/david/dev/derhuerst/hafas-client.svg)](https://david-dm.org/derhuerst/hafas-client#info=devDependencies) ![ISC-licensed](https://img.shields.io/github/license/derhuerst/hafas-client.svg) [![chat on gitter](https://badges.gitter.im/derhuerst.svg)](https://gitter.im/derhuerst) @@ -19,7 +17,9 @@ npm install hafas-client ## Usage -See [vbb-hafas](https://github.com/derhuerst/vbb-hafas/blob/master/lib/request.js). +``` +todo +``` ## Contributing From 79db55d99ce2d37320a59bf4cbf46736103a8c76 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sat, 11 Nov 2017 22:35:41 +0100 Subject: [PATCH 02/73] split parse.js --- index.js | 23 ++--- package.json | 2 +- parse.js | 223 --------------------------------------------- parse/date-time.js | 16 ++++ parse/departure.js | 37 ++++++++ parse/index.js | 13 +++ parse/journey.js | 108 ++++++++++++++++++++++ parse/line.js | 18 ++++ parse/location.js | 24 +++++ parse/movement.js | 49 ++++++++++ parse/nearby.js | 16 ++++ parse/operator.js | 13 +++ parse/remark.js | 7 ++ parse/stopover.js | 23 +++++ 14 files changed, 337 insertions(+), 235 deletions(-) delete mode 100644 parse.js create mode 100644 parse/date-time.js create mode 100644 parse/departure.js create mode 100644 parse/index.js create mode 100644 parse/journey.js create mode 100644 parse/line.js create mode 100644 parse/location.js create mode 100644 parse/movement.js create mode 100644 parse/nearby.js create mode 100644 parse/operator.js create mode 100644 parse/remark.js create mode 100644 parse/stopover.js diff --git a/index.js b/index.js index 51b58929..0953a860 100644 --- a/index.js +++ b/index.js @@ -4,7 +4,10 @@ const Promise = require('pinkie-promise') const {fetch} = require('fetch-ponyfill')({Promise}) const {stringify} = require('query-string') -const parse = require('./parse') +const parseLocation = require('./parse/location') +const parseLine = require('./parse/line') +const parseRemark = require('./parse/remark') +const parseOperator = require('./parse/operator') @@ -12,14 +15,12 @@ const id = (x) => x const defaults = { onBody: id, onReq: id, - onLocation: parse.location, - onLine: parse.line, - onRemark: parse.remark, - onOperator: parse.operator + parseLocation: parseLocation, + parseLine: parseLine, + parseRemark: parseRemark, + parseOperator: parseOperator } - - const hafasError = (err) => { err.isHafasError = true return err @@ -60,10 +61,10 @@ const createRequest = (opt) => { const d = b.svcResL[0].res const c = d.common || {} - if (Array.isArray(c.locL)) d.locations = c.locL.map(opt.onLocation) - if (Array.isArray(c.prodL)) d.lines = c.prodL.map(opt.onLine) - if (Array.isArray(c.remL)) d.remarks = c.remL.map(opt.onRemark) - if (Array.isArray(c.opL)) d.operators = c.opL.map(opt.onOperator) + if (Array.isArray(c.locL)) d.locations = c.locL.map(opt.parseLocation) + if (Array.isArray(c.prodL)) d.lines = c.prodL.map(opt.parseLine) + if (Array.isArray(c.remL)) d.remarks = c.remL.map(opt.parseRemark) + if (Array.isArray(c.opL)) d.operators = c.opL.map(opt.parseOperator) return d }) } diff --git a/package.json b/package.json index 031d9334..9ea12e46 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "files": [ "index.js", - "parse.js", + "parse", "stringify.js" ], "author": "Jannis R ", diff --git a/parse.js b/parse.js deleted file mode 100644 index f34040a1..00000000 --- a/parse.js +++ /dev/null @@ -1,223 +0,0 @@ -'use strict' - -const moment = require('moment-timezone') -const slugg = require('slugg') - - - -const dateTime = (tz, date, time) => { - let offset = 0 // in days - if (time.length > 6) { - offset = +time.slice(0, -6) - time = time.slice(-6) - } - return moment.tz(date + 'T' + time, tz) - .add(offset, 'days') -} - - - -const types = {P: 'poi', S: 'station', A: 'address'} -// todo: what is s.rRefL? -const location = (l) => { - const type = types[l.type] || 'unknown' - const result = { - type, - name: l.name, - coordinates: l.crd ? { - latitude: l.crd.y / 1000000, - longitude: l.crd.x / 1000000 - } : null - } - if (type === 'poi' || type === 'station') result.id = l.extId - if ('pCls' in l) result.products = l.pCls - return result -} - - - -// todo: what is p.number vs p.line? -// todo: what is p.icoX? -// todo: what is p.oprX? -const line = (p) => { - if (!p) return null - const result = {type: 'line', name: p.line || p.name} - if (p.cls) result.class = p.cls - if (p.prodCtx) { - result.productCode = +p.prodCtx.catCode - result.productName = p.prodCtx.catOutS - } - return result -} - - - -const remark = (r) => null // todo - - - -const operator = (a) => ({ - type: 'operator', - id: slugg(a.name), - name: a.name -}) - - - -// s = stations, ln = lines, r = remarks, c = connection -const stopover = (tz, s, ln, r, c) => (st) => { - const result = {station: s[parseInt(st.locX)]} - if (st.aTimeR || st.aTimeS) { - result.arrival = dateTime(tz, c.date, st.aTimeR || st.aTimeS).format() - } - if (st.dTimeR || st.dTimeS) { - result.departure = dateTime(tz, c.date, st.dTimeR || st.dTimeS).format() - } - return result -} - -// todo: finish parseRemark first -// s = stations, ln = lines, r = remarks, c = connection -const applyRemark = (s, ln, r, c) => (rm) => null - -// todo: pt.sDays -// todo: pt.dep.dProgType, pt.arr.dProgType -// todo: what is pt.jny.dirFlg? -// todo: how does pt.freq work? -// tz = timezone, s = stations, ln = lines, r = remarks, c = connection -const part = (tz, s, ln, r, c) => (pt) => { - const result = { - origin: Object.assign({}, s[parseInt(pt.dep.locX)]) - , destination: Object.assign({}, s[parseInt(pt.arr.locX)]) - , departure: dateTime(tz, c.date, pt.dep.dTimeR || pt.dep.dTimeS).format() - , arrival: dateTime(tz, c.date, pt.arr.aTimeR || pt.arr.aTimeS).format() - } - if (pt.dep.dTimeR && pt.dep.dTimeS) { - const realtime = dateTime(tz, c.date, pt.dep.dTimeR) - const planned = dateTime(tz, c.date, pt.dep.dTimeS) - result.delay = Math.round((realtime - planned) / 1000) - } - - if (pt.type === 'WALK') result.mode = 'walking' - else if (pt.type === 'JNY') { - result.id = pt.jny.jid - result.line = ln[parseInt(pt.jny.prodX)] - result.direction = pt.jny.dirTxt // todo: parse this - - if (pt.dep.dPlatfS) result.departurePlatform = pt.dep.dPlatfS - if (pt.arr.aPlatfS) result.arrivalPlatform = pt.arr.aPlatfS - - if (pt.jny.stopL) result.passed = pt.jny.stopL.map(stopover(tz, s, ln, r, c)) - if (Array.isArray(pt.jny.remL)) - pt.jny.remL.forEach(applyRemark(s, ln, r, c)) - - if (pt.jny.freq && pt.jny.freq.jnyL) - result.alternatives = pt.jny.freq.jnyL - .filter((a) => a.stopL[0].locX === pt.dep.locX) - .map((a) => ({ - line: ln[parseInt(a.prodX)], - when: dateTime(tz, c.date, a.stopL[0].dTimeS).format() - })) - } - return result -} - -// todo: c.sDays -// todo: c.dep.dProgType, c.arr.dProgType -// todo: c.conSubscr -// todo: c.trfRes x vbb-parse-ticket -// todo: use computed information from part -// s = stations, ln = lines, r = remarks, p = parsePart -const journey = (tz, s, ln, r, p = part) => (c) => { - const parts = c.secL.map(p(tz, s, ln, r, c)) - return { - parts - , origin: parts[0].origin - , destination: parts[parts.length - 1].destination - , departure: parts[0].departure - , arrival: parts[parts.length - 1].arrival - } -} - -// todos from derhuerst/hafas-client#2 -// - stdStop.dCncl -// - stdStop.dPlatfS, stdStop.dPlatfR -// todo: what is d.jny.dirFlg? -// todo: d.stbStop.dProgType -// tz = timezone, s = stations, ln = lines, r = remarks -const departure = (tz, s, ln, r) => (d) => { - const result = { - ref: d.jid - , station: s[parseInt(d.stbStop.locX)] - , when: dateTime(tz, d.date, d.stbStop.dTimeR || d.stbStop.dTimeS).format() - , direction: d.dirTxt - , line: ln[parseInt(d.prodX)] - , remarks: d.remL ? d.remL.map((rm) => r[parseInt(rm.remX)]) : null - , trip: +d.jid.split('|')[1] - } - if (d.stbStop.dTimeR && d.stbStop.dTimeS) { - const realtime = dateTime(tz, d.date, d.stbStop.dTimeR) - const planned = dateTime(tz, d.date, d.stbStop.dTimeS) - result.delay = Math.round((realtime - planned) / 1000) - } else result.delay = null - return result -} - -// todo: remarks -// todo: lines -// todo: what is s.pCls? -// todo: what is s.wt? -// todo: what is s.dur? -const nearby = (n) => { - const result = location(n) - result.distance = n.dist - return result -} - -// todo: what is m.dirGeo? maybe the speed? -// todo: what is m.stopL? -// todo: what is m.proc? wut? -// todo: what is m.pos? -// todo: what is m.ani.dirGeo[n]? maybe the speed? -// todo: what is m.ani.proc[n]? wut? -// todo: how does m.ani.poly work? -// tz = timezone, l = locations, ln = lines, r = remarks -const movement = (tz, l, ln, r) => (m) => { - const result = { - direction: m.dirTxt - , line: ln[m.prodX] - , coordinates: m.pos ? { - latitude: m.pos.y / 1000000, - longitude: m.pos.x / 1000000 - } : null - , nextStops: m.stopL.map((s) => ({ - station: l[s.locX] - , departure: s.dTimeR || s.dTimeS - ? dateTime(tz, m.date, s.dTimeR || s.dTimeS).format() - : null - , arrival: s.aTimeR || s.aTimeS - ? dateTime(tz, m.date, s.aTimeR || s.aTimeS).format() - : null - })) - , frames: [] - } - if (m.ani && Array.isArray(m.ani.mSec)) - for (let i = 0; i < m.ani.mSec.length; i++) - result.frames.push({ - origin: l[m.ani.fLocX[i]], - destination: l[m.ani.tLocX[i]], - t: m.ani.mSec[i] - }) - return result -} - - - -module.exports = { - dateTime, - location, line, remark, operator, - stopover, applyRemark, part, journey, - departure, - nearby, - movement -} diff --git a/parse/date-time.js b/parse/date-time.js new file mode 100644 index 00000000..bd616c5a --- /dev/null +++ b/parse/date-time.js @@ -0,0 +1,16 @@ +'use strict' + +const moment = require('moment-timezone') + +const parseDateTime = (tz, date, time) => { + let offset = 0 // in days + if (time.length > 6) { + offset = +time.slice(0, -6) + time = time.slice(-6) + } + + return moment.tz(date + 'T' + time, tz) + .add(offset, 'days') +} + +module.exports = parseDateTime diff --git a/parse/departure.js b/parse/departure.js new file mode 100644 index 00000000..a6932dcb --- /dev/null +++ b/parse/departure.js @@ -0,0 +1,37 @@ +'use strict' + +const parseDateTime = require('./date-time') + +// todos from derhuerst/hafas-client#2 +// - stdStop.dCncl +// - stdStop.dPlatfS, stdStop.dPlatfR +// todo: what is d.jny.dirFlg? +// todo: d.stbStop.dProgType + +// tz = timezone, s = stations, ln = lines, r = remarks +const createParseDeparture = (tz, s, ln, r) => { + const parseDeparture = (d) => { + const when = parseDateTime(tz, d.date, d.stbStop.dTimeR || d.stbStop.dTimeS) + const result = { + ref: d.jid + , station: s[parseInt(d.stbStop.locX)] + , when: when.format() + , direction: d.dirTxt + , line: ln[parseInt(d.prodX)] + , remarks: d.remL ? d.remL.map((rm) => r[parseInt(rm.remX)]) : null + , trip: +d.jid.split('|')[1] + } + + if (d.stbStop.dTimeR && d.stbStop.dTimeS) { + const realtime = parseDateTime(tz, d.date, d.stbStop.dTimeR) + const planned = parseDateTime(tz, d.date, d.stbStop.dTimeS) + result.delay = Math.round((realtime - planned) / 1000) + } else result.delay = null + + return result + } + + return parseDeparture +} + +module.exports = createParseDeparture diff --git a/parse/index.js b/parse/index.js new file mode 100644 index 00000000..7c9c35bb --- /dev/null +++ b/parse/index.js @@ -0,0 +1,13 @@ +'use strict' + +module.exports = { + dateTime: require('./date-time'), + location: require('./location'), + line: require('./line'), + remark: require('./remark'), + operator: require('./operator'), + stopover: require('./stopover'), + journey: require('./journey'), + nearby: require('./nearby'), + movement: require('./movement') +} diff --git a/parse/journey.js b/parse/journey.js new file mode 100644 index 00000000..fdfd8eea --- /dev/null +++ b/parse/journey.js @@ -0,0 +1,108 @@ +'use strict' + +const parseDateTime = require('./date-time') + +// s = stations, ln = lines, r = remarks, c = connection +const createParseStopover = (tz, s, ln, r, c) => { + const parseStopover = (st) => { + const res = { + station: s[parseInt(st.locX)] + } + if (st.aTimeR || st.aTimeS) { + const arr = parseDateTime(tz, c.date, st.aTimeR || st.aTimeS) + res.arrival = arr.format() + } + if (st.dTimeR || st.dTimeS) { + const dep = parseDateTime(tz, c.date, st.dTimeR || st.dTimeS) + res.departure = dep.format() + } + return res + } + + return parseStopover +} + +// s = stations, ln = lines, r = remarks, c = connection +const createApplyRemark = (s, ln, r, c) => { + // todo: finish parse/remark.js first + const applyRemark = (rm) => {} + return applyRemark +} + +// tz = timezone, s = stations, ln = lines, r = remarks, c = connection +const createParsePart = (tz, s, ln, r, c) => { + // todo: pt.sDays + // todo: pt.dep.dProgType, pt.arr.dProgType + // todo: what is pt.jny.dirFlg? + // todo: how does pt.freq work? + const parsePart = (pt) => { + const dep = parseDateTime(tz, c.date, pt.dep.dTimeR || pt.dep.dTimeS) + const arr = parseDateTime(tz, c.date, pt.arr.aTimeR || pt.arr.aTimeS) + const res = { + origin: Object.assign({}, s[parseInt(pt.dep.locX)]) // todo: what about null? + , destination: Object.assign({}, s[parseInt(pt.arr.locX)]) // todo: what about null? + , departure: dep.format() + , arrival: dep.format() + } + + if (pt.dep.dTimeR && pt.dep.dTimeS) { + const realtime = parseDateTime(tz, c.date, pt.dep.dTimeR) + const planned = parseDateTime(tz, c.date, pt.dep.dTimeS) + res.delay = Math.round((realtime - planned) / 1000) + } + + if (pt.type === 'WALK') { + res.mode = 'walking' + } else if (pt.type === 'JNY') { + res.id = pt.jny.jid + res.line = ln[parseInt(pt.jny.prodX)] // todo: default null + res.direction = pt.jny.dirTxt // todo: parse this + + if (pt.dep.dPlatfS) res.departurePlatform = pt.dep.dPlatfS + if (pt.arr.aPlatfS) res.arrivalPlatform = pt.arr.aPlatfS + + if (pt.jny.stopL) { + res.passed = pt.jny.stopL.map(createParseStopover(tz, s, ln, r, c)) + } + if (Array.isArray(pt.jny.remL)) { + pt.jny.remL.forEach(createApplyRemark(s, ln, r, c)) + } + + if (pt.jny.freq && pt.jny.freq.jnyL) { + const parseAlternative = (a) => ({ + line: ln[parseInt(a.prodX)], // todo: default null + when: parseDateTime(tz, c.date, a.stopL[0].dTimeS).format() // todo: realtime + }) + res.alternatives = pt.jny.freq.jnyL + .filter((a) => a.stopL[0].locX === pt.dep.locX) + .map(parseAlternative) + } + } + + return res + } + return parsePart +} + +// s = stations, ln = lines, r = remarks, p = createParsePart +const createParseJourney = (tz, s, ln, r, p = createParsePart) => { + // todo: c.sDays + // todo: c.dep.dProgType, c.arr.dProgType + // todo: c.conSubscr + // todo: c.trfRes x vbb-parse-ticket + // todo: use computed information from part + const parseJourney = (c) => { + const parts = c.secL.map(p(tz, s, ln, r, c)) + return { + parts + , origin: parts[0].origin + , destination: parts[parts.length - 1].destination + , departure: parts[0].departure + , arrival: parts[parts.length - 1].arrival + } + } + + return parseJourney +} + +module.exports = createParseJourney diff --git a/parse/line.js b/parse/line.js new file mode 100644 index 00000000..168a9d2f --- /dev/null +++ b/parse/line.js @@ -0,0 +1,18 @@ +'use strict' + +// todo: what is p.number vs p.line? +// todo: what is p.icoX? +// todo: what is p.oprX? +const parseLine = (p) => { + if (!p) return null + + const result = {type: 'line', name: p.line || p.name} + if (p.cls) result.class = p.cls + if (p.prodCtx) { + result.productCode = +p.prodCtx.catCode + result.productName = p.prodCtx.catOutS + } + return result +} + +module.exports = parseLine diff --git a/parse/location.js b/parse/location.js new file mode 100644 index 00000000..d4eff9e6 --- /dev/null +++ b/parse/location.js @@ -0,0 +1,24 @@ +'use strict' + +const types = Object.create(null) +types.P = 'poi' +types.S = 'station' +types.A = 'address' + +// todo: what is s.rRefL? +const parseLocation = (l) => { + const type = types[l.type] || 'unknown' + const result = { + type, + name: l.name, + coordinates: l.crd ? { + latitude: l.crd.y / 1000000, + longitude: l.crd.x / 1000000 + } : null + } + if (type === 'poi' || type === 'station') result.id = l.extId + if ('pCls' in l) result.products = l.pCls + return result +} + +module.exports = parseLocation diff --git a/parse/movement.js b/parse/movement.js new file mode 100644 index 00000000..eb701a38 --- /dev/null +++ b/parse/movement.js @@ -0,0 +1,49 @@ +'use strict' + +const parseDateTime = require('./date-time') + +// tz = timezone, l = locations, ln = lines, r = remarks +const createParseMovement = (tz, l, ln, r) => { + // todo: what is m.dirGeo? maybe the speed? + // todo: what is m.stopL? + // todo: what is m.proc? wut? + // todo: what is m.pos? + // todo: what is m.ani.dirGeo[n]? maybe the speed? + // todo: what is m.ani.proc[n]? wut? + // todo: how does m.ani.poly work? + const parseMovement = (m) => { + const res = { + direction: m.dirTxt + , line: ln[m.prodX] + , coordinates: m.pos ? { + latitude: m.pos.y / 1000000, + longitude: m.pos.x / 1000000 + } : null + , nextStops: m.stopL.map((s) => ({ + station: l[s.locX] + , departure: s.dTimeR || s.dTimeS + ? parseDateTime(tz, m.date, s.dTimeR || s.dTimeS).format() + : null + , arrival: s.aTimeR || s.aTimeS + ? parseDateTime(tz, m.date, s.aTimeR || s.aTimeS).format() + : null + })) + , frames: [] + } + + if (m.ani && Array.isArray(m.ani.mSec)) { + for (let i = 0; i < m.ani.mSec.length; i++) { + res.frames.push({ + origin: l[m.ani.fLocX[i]], + destination: l[m.ani.tLocX[i]], + t: m.ani.mSec[i] + }) + } + } + + return res + } + return parseMovement +} + +module.exports = createParseMovement diff --git a/parse/nearby.js b/parse/nearby.js new file mode 100644 index 00000000..896682c2 --- /dev/null +++ b/parse/nearby.js @@ -0,0 +1,16 @@ +'use strict' + +const parseLocation = require('./location') + +// todo: remarks +// todo: lines +// todo: what is s.pCls? +// todo: what is s.wt? +// todo: what is s.dur? +const parseNearby = (n) => { + const result = location(n) + result.distance = n.dist + return result +} + +module.exports = parseNearby diff --git a/parse/operator.js b/parse/operator.js new file mode 100644 index 00000000..a77f217b --- /dev/null +++ b/parse/operator.js @@ -0,0 +1,13 @@ +'use strict' + +const slugg = require('slugg') + +const parseOperator = (a) => { + return { + type: 'operator', + id: slugg(a.name), // todo: find a more reliable way + name: a.name + } +} + +module.exports = parseOperator diff --git a/parse/remark.js b/parse/remark.js new file mode 100644 index 00000000..d08b1500 --- /dev/null +++ b/parse/remark.js @@ -0,0 +1,7 @@ +'use strict' + +const parseRemark = (r) => { + return null // todo +} + +module.exports = parseRemark diff --git a/parse/stopover.js b/parse/stopover.js new file mode 100644 index 00000000..9a413f77 --- /dev/null +++ b/parse/stopover.js @@ -0,0 +1,23 @@ +'use strict' + +// s = stations, ln = lines, r = remarks, c = connection +const createParseStopover = (tz, s, ln, r, c) => { + const parseStopover = (st) => { + const res = { + station: s[parseInt(st.locX)] + } + if (st.aTimeR || st.aTimeS) { + const arr = parseDateTime(tz, c.date, st.aTimeR || st.aTimeS) + res.arrival = arr.format() + } + if (st.dTimeR || st.dTimeS) { + const dep = parseDateTime(tz, c.date, st.dTimeR || st.dTimeS) + res.departure = dep.format() + } + return res + } + + return parseStopover +} + +module.exports = createParseStopover From e0d9c28ef23efcd9d8cadd0f400ddaeb57f91dbf Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sat, 11 Nov 2017 22:49:04 +0100 Subject: [PATCH 03/73] move out request handling --- index.js | 82 ++++++++++++++++---------------------------------- lib/request.js | 53 ++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 80 insertions(+), 56 deletions(-) create mode 100644 lib/request.js diff --git a/index.js b/index.js index 0953a860..3d7abf43 100644 --- a/index.js +++ b/index.js @@ -1,75 +1,45 @@ 'use strict' -const Promise = require('pinkie-promise') -const {fetch} = require('fetch-ponyfill')({Promise}) -const {stringify} = require('query-string') - const parseLocation = require('./parse/location') const parseLine = require('./parse/line') const parseRemark = require('./parse/remark') const parseOperator = require('./parse/operator') +const request = require('./lib/request') +const id = x => x - -const id = (x) => x -const defaults = { - onBody: id, - onReq: id, +const defaultProfile = { + transformReqBody: id, + transformReq: id, parseLocation: parseLocation, parseLine: parseLine, parseRemark: parseRemark, parseOperator: parseOperator } -const hafasError = (err) => { - err.isHafasError = true - return err -} - -const createRequest = (opt) => { - opt = Object.assign({}, defaults, opt) - - const request = (data) => { - const body = opt.onBody({lang: 'en', svcReqL: [data]}) - const req = opt.onReq({ - method: 'post', - body: JSON.stringify(body), - headers: { - 'Content-Type': 'application/json', - 'Accept-Encoding': 'gzip, deflate', - 'user-agent': 'https://github.com/derhuerst/hafas-client' - }, - query: null - }) - const url = opt.endpoint + (req.query ? '?' + stringify(req.query) : '') - - return fetch(url, req) - .then((res) => { - if (!res.ok) { - const err = new Error(res.statusText) - err.statusCode = res.status - throw hafasError(err) - } - return res.json() - }) - .then((b) => { - if (b.err) throw hafasError(new Error(b.err)) - if (!b.svcResL || !b.svcResL[0]) throw new Error('invalid response') - if (b.svcResL[0].err !== 'OK') { - throw hafasError(new Error(b.svcResL[0].errTxt)) - } - const d = b.svcResL[0].res - const c = d.common || {} - - if (Array.isArray(c.locL)) d.locations = c.locL.map(opt.parseLocation) - if (Array.isArray(c.prodL)) d.lines = c.prodL.map(opt.parseLine) - if (Array.isArray(c.remL)) d.remarks = c.remL.map(opt.parseRemark) - if (Array.isArray(c.opL)) d.operators = c.opL.map(opt.parseOperator) - return d - }) +const createClient = (profile) => { + profile = Object.assign({}, defaultProfile, profile) + if ('function' !== profile.transformReqBody) { + throw new Error('profile.transformReqBody must be a function.') + } + if ('function' !== profile.transformReq) { + throw new Error('profile.transformReq must be a function.') + } + if ('function' !== profile.parseLocation) { + throw new Error('profile.parseLocation must be a function.') + } + if ('function' !== profile.parseLine) { + throw new Error('profile.parseLine must be a function.') + } + if ('function' !== profile.parseRemark) { + throw new Error('profile.parseRemark must be a function.') + } + if ('function' !== profile.parseOperator) { + throw new Error('profile.parseOperator must be a function.') } - return request + const client = data => request(profile, data) + return client } module.exports = createRequest diff --git a/lib/request.js b/lib/request.js new file mode 100644 index 00000000..6affd575 --- /dev/null +++ b/lib/request.js @@ -0,0 +1,53 @@ +'use strict' + +const Promise = require('pinkie-promise') +const {fetch} = require('fetch-ponyfill')({Promise}) +const {stringify} = require('query-string') + +const hafasError = (err) => { + err.isHafasError = true + return err +} + +const request = (profile, data) => { + const body = profile.transformReqBody({lang: 'en', svcReqL: [data]}) + const req = profile.transformReq({ + method: 'post', + // todo: CORS? referrer policy? + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json', + 'Accept-Encoding': 'gzip, deflate', + 'user-agent': 'https://github.com/derhuerst/hafas-client' + }, + query: null + }) + const url = profile.endpoint + (req.query ? '?' + stringify(req.query) : '') + + return fetch(url, req) + .then((res) => { + if (!res.ok) { + const err = new Error(res.statusText) + err.statusCode = res.status + throw hafasError(err) + } + return res.json() + }) + .then((b) => { + if (b.err) throw hafasError(new Error(b.err)) + if (!b.svcResL || !b.svcResL[0]) throw new Error('invalid response') + if (b.svcResL[0].err !== 'OK') { + throw hafasError(new Error(b.svcResL[0].errTxt)) + } + const d = b.svcResL[0].res + const c = d.common || {} + + if (Array.isArray(c.locL)) d.locations = c.locL.map(profile.parseLocation) + if (Array.isArray(c.prodL)) d.lines = c.prodL.map(profile.parseLine) + if (Array.isArray(c.remL)) d.remarks = c.remL.map(profile.parseRemark) + if (Array.isArray(c.opL)) d.operators = c.opL.map(profile.parseOperator) + return d + }) +} + +module.exports = request diff --git a/package.json b/package.json index 9ea12e46..720b6374 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "index.js", "files": [ "index.js", + "lib", "parse", "stringify.js" ], From fb43a4e43bfa805a586baceba1fb4e986bb3da50 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sat, 11 Nov 2017 23:56:09 +0100 Subject: [PATCH 04/73] code style :shirt: - rename variables - more consistent indentation - give crucial anonymous functions a name - add more TODOs --- parse/date-time.js | 4 +-- parse/departure.js | 33 ++++++++++++------------ parse/journey.js | 64 +++++++++++++++++++++++----------------------- parse/line.js | 13 +++++----- parse/location.js | 2 ++ parse/movement.js | 42 +++++++++++++++++------------- parse/nearby.js | 6 ++--- parse/stopover.js | 11 ++++---- 8 files changed, 93 insertions(+), 82 deletions(-) diff --git a/parse/date-time.js b/parse/date-time.js index bd616c5a..4c913e1c 100644 --- a/parse/date-time.js +++ b/parse/date-time.js @@ -2,14 +2,14 @@ const moment = require('moment-timezone') -const parseDateTime = (tz, date, time) => { +const parseDateTime = (timezone, date, time) => { let offset = 0 // in days if (time.length > 6) { offset = +time.slice(0, -6) time = time.slice(-6) } - return moment.tz(date + 'T' + time, tz) + return moment.tz(date + 'T' + time, timezone) .add(offset, 'days') } diff --git a/parse/departure.js b/parse/departure.js index a6932dcb..c01ea81d 100644 --- a/parse/departure.js +++ b/parse/departure.js @@ -8,27 +8,28 @@ const parseDateTime = require('./date-time') // todo: what is d.jny.dirFlg? // todo: d.stbStop.dProgType -// tz = timezone, s = stations, ln = lines, r = remarks -const createParseDeparture = (tz, s, ln, r) => { +const createParseDeparture = (timezone, stations, lines, remarks) => { + const findRemark = rm => remarks[parseInt(rm.remX)] || null + const parseDeparture = (d) => { - const when = parseDateTime(tz, d.date, d.stbStop.dTimeR || d.stbStop.dTimeS) - const result = { - ref: d.jid - , station: s[parseInt(d.stbStop.locX)] - , when: when.format() - , direction: d.dirTxt - , line: ln[parseInt(d.prodX)] - , remarks: d.remL ? d.remL.map((rm) => r[parseInt(rm.remX)]) : null - , trip: +d.jid.split('|')[1] + const when = parseDateTime(timezone, d.date, d.stbStop.dTimeR || d.stbStop.dTimeS) + const res = { + ref: d.jid, + station: stations[parseInt(d.stbStop.locX)], // todo: default to null + when: when.format(), + direction: d.dirTxt, + line: lines[parseInt(d.prodX)], // todo: default to null + remarks: d.remL ? d.remL.map(findRemark) : [], + trip: +d.jid.split('|')[1] // todo: this seems brittle } if (d.stbStop.dTimeR && d.stbStop.dTimeS) { - const realtime = parseDateTime(tz, d.date, d.stbStop.dTimeR) - const planned = parseDateTime(tz, d.date, d.stbStop.dTimeS) - result.delay = Math.round((realtime - planned) / 1000) - } else result.delay = null + const realtime = parseDateTime(timezone, d.date, d.stbStop.dTimeR) + const planned = parseDateTime(timezone, d.date, d.stbStop.dTimeS) + res.delay = Math.round((realtime - planned) / 1000) + } else res.delay = null - return result + return res } return parseDeparture diff --git a/parse/journey.js b/parse/journey.js index fdfd8eea..da56dcf1 100644 --- a/parse/journey.js +++ b/parse/journey.js @@ -2,18 +2,19 @@ const parseDateTime = require('./date-time') -// s = stations, ln = lines, r = remarks, c = connection -const createParseStopover = (tz, s, ln, r, c) => { +const clone = obj => Object.assign({}, obj) + +const createParseStopover = (tz, stations, lines, remarks, j) => { // j = journey const parseStopover = (st) => { const res = { - station: s[parseInt(st.locX)] + station: stations[parseInt(st.locX)] } if (st.aTimeR || st.aTimeS) { - const arr = parseDateTime(tz, c.date, st.aTimeR || st.aTimeS) + const arr = parseDateTime(tz, j.date, st.aTimeR || st.aTimeS) res.arrival = arr.format() } if (st.dTimeR || st.dTimeS) { - const dep = parseDateTime(tz, c.date, st.dTimeR || st.dTimeS) + const dep = parseDateTime(tz, j.date, st.dTimeR || st.dTimeS) res.departure = dep.format() } return res @@ -22,32 +23,31 @@ const createParseStopover = (tz, s, ln, r, c) => { return parseStopover } -// s = stations, ln = lines, r = remarks, c = connection -const createApplyRemark = (s, ln, r, c) => { +const createApplyRemark = (stations, lines, remarks, j) => { // j = journey // todo: finish parse/remark.js first const applyRemark = (rm) => {} return applyRemark } -// tz = timezone, s = stations, ln = lines, r = remarks, c = connection -const createParsePart = (tz, s, ln, r, c) => { +const createParsePart = (tz, stations, lines, remarks, j) => { // j = journey // todo: pt.sDays // todo: pt.dep.dProgType, pt.arr.dProgType // todo: what is pt.jny.dirFlg? // todo: how does pt.freq work? const parsePart = (pt) => { - const dep = parseDateTime(tz, c.date, pt.dep.dTimeR || pt.dep.dTimeS) - const arr = parseDateTime(tz, c.date, pt.arr.aTimeR || pt.arr.aTimeS) + const dep = parseDateTime(tz, j.date, pt.dep.dTimeR || pt.dep.dTimeS) + const arr = parseDateTime(tz, j.date, pt.arr.aTimeR || pt.arr.aTimeS) const res = { - origin: Object.assign({}, s[parseInt(pt.dep.locX)]) // todo: what about null? - , destination: Object.assign({}, s[parseInt(pt.arr.locX)]) // todo: what about null? - , departure: dep.format() - , arrival: dep.format() + // todo: what about null? + origin: clone(stations[parseInt(pt.dep.locX)]), + destination: clone(stations[parseInt(pt.arr.locX)]), + departure: dep.format(), + arrival: arr.format() } if (pt.dep.dTimeR && pt.dep.dTimeS) { - const realtime = parseDateTime(tz, c.date, pt.dep.dTimeR) - const planned = parseDateTime(tz, c.date, pt.dep.dTimeS) + const realtime = parseDateTime(tz, j.date, pt.dep.dTimeR) + const planned = parseDateTime(tz, j.date, pt.dep.dTimeS) res.delay = Math.round((realtime - planned) / 1000) } @@ -55,26 +55,27 @@ const createParsePart = (tz, s, ln, r, c) => { res.mode = 'walking' } else if (pt.type === 'JNY') { res.id = pt.jny.jid - res.line = ln[parseInt(pt.jny.prodX)] // todo: default null + res.line = lines[parseInt(pt.jny.prodX)] // todo: default null res.direction = pt.jny.dirTxt // todo: parse this if (pt.dep.dPlatfS) res.departurePlatform = pt.dep.dPlatfS if (pt.arr.aPlatfS) res.arrivalPlatform = pt.arr.aPlatfS if (pt.jny.stopL) { - res.passed = pt.jny.stopL.map(createParseStopover(tz, s, ln, r, c)) + const parseStopover = createParseStopover(tz, stations, lines, remarks, j) + res.passed = pt.jny.stopL.map(parseStopover) } if (Array.isArray(pt.jny.remL)) { - pt.jny.remL.forEach(createApplyRemark(s, ln, r, c)) + pt.jny.remL.forEach(createApplyRemark(stations, lines, remarks, j)) } if (pt.jny.freq && pt.jny.freq.jnyL) { const parseAlternative = (a) => ({ - line: ln[parseInt(a.prodX)], // todo: default null - when: parseDateTime(tz, c.date, a.stopL[0].dTimeS).format() // todo: realtime + line: lines[parseInt(a.prodX)], // todo: default null + when: parseDateTime(tz, j.date, a.stopL[0].dTimeS).format() // todo: realtime }) res.alternatives = pt.jny.freq.jnyL - .filter((a) => a.stopL[0].locX === pt.dep.locX) + .filter(a => a.stopL[0].locX === pt.dep.locX) .map(parseAlternative) } } @@ -84,21 +85,20 @@ const createParsePart = (tz, s, ln, r, c) => { return parsePart } -// s = stations, ln = lines, r = remarks, p = createParsePart -const createParseJourney = (tz, s, ln, r, p = createParsePart) => { +const createParseJourney = (tz, stations, lines, remarks, p = createParsePart) => { // todo: c.sDays // todo: c.dep.dProgType, c.arr.dProgType // todo: c.conSubscr // todo: c.trfRes x vbb-parse-ticket // todo: use computed information from part - const parseJourney = (c) => { - const parts = c.secL.map(p(tz, s, ln, r, c)) + const parseJourney = (journey) => { + const parts = journey.secL.map(p(tz, stations, lines, remarks, journey)) return { - parts - , origin: parts[0].origin - , destination: parts[parts.length - 1].destination - , departure: parts[0].departure - , arrival: parts[parts.length - 1].arrival + parts, + origin: parts[0].origin, + destination: parts[parts.length - 1].destination, + departure: parts[0].departure, + arrival: parts[parts.length - 1].arrival } } diff --git a/parse/line.js b/parse/line.js index 168a9d2f..7094acd6 100644 --- a/parse/line.js +++ b/parse/line.js @@ -4,15 +4,16 @@ // todo: what is p.icoX? // todo: what is p.oprX? const parseLine = (p) => { - if (!p) return null + if (!p) return null // todo: handle this upstream + const res = {type: 'line', name: p.line || p.name} - const result = {type: 'line', name: p.line || p.name} - if (p.cls) result.class = p.cls + if (p.cls) res.class = p.cls if (p.prodCtx) { - result.productCode = +p.prodCtx.catCode - result.productName = p.prodCtx.catOutS + res.productCode = +p.prodCtx.catCode + res.productName = p.prodCtx.catOutS } - return result + + return res } module.exports = parseLine diff --git a/parse/location.js b/parse/location.js index d4eff9e6..405498b8 100644 --- a/parse/location.js +++ b/parse/location.js @@ -16,8 +16,10 @@ const parseLocation = (l) => { longitude: l.crd.x / 1000000 } : null } + if (type === 'poi' || type === 'station') result.id = l.extId if ('pCls' in l) result.products = l.pCls + return result } diff --git a/parse/movement.js b/parse/movement.js index eb701a38..27ddce18 100644 --- a/parse/movement.js +++ b/parse/movement.js @@ -2,8 +2,7 @@ const parseDateTime = require('./date-time') -// tz = timezone, l = locations, ln = lines, r = remarks -const createParseMovement = (tz, l, ln, r) => { +const createParseMovement = (tz, locations, lines, remarks) => { // todo: what is m.dirGeo? maybe the speed? // todo: what is m.stopL? // todo: what is m.proc? wut? @@ -12,30 +11,37 @@ const createParseMovement = (tz, l, ln, r) => { // todo: what is m.ani.proc[n]? wut? // todo: how does m.ani.poly work? const parseMovement = (m) => { + const parseNextStop = (s) => { + const dep = s.dTimeR || s.dTimeS + ? parseDateTime(tz, m.date, s.dTimeR || s.dTimeS) + : null + const arr = s.aTimeR || s.aTimeS + ? parseDateTime(tz, m.date, s.aTimeR || s.aTimeS) + : null + + return { + station: locations[s.locX], + departure: dep ? dep.format() : null, + arrival: arr ? arr.format() : null + } + } + const res = { - direction: m.dirTxt - , line: ln[m.prodX] - , coordinates: m.pos ? { + direction: m.dirTxt, + line: lines[m.prodX], // default to null + coordinates: m.pos ? { latitude: m.pos.y / 1000000, longitude: m.pos.x / 1000000 - } : null - , nextStops: m.stopL.map((s) => ({ - station: l[s.locX] - , departure: s.dTimeR || s.dTimeS - ? parseDateTime(tz, m.date, s.dTimeR || s.dTimeS).format() - : null - , arrival: s.aTimeR || s.aTimeS - ? parseDateTime(tz, m.date, s.aTimeR || s.aTimeS).format() - : null - })) - , frames: [] + } : null, + nextStops: m.stopL.map(parseNextStop), + frames: [] } if (m.ani && Array.isArray(m.ani.mSec)) { for (let i = 0; i < m.ani.mSec.length; i++) { res.frames.push({ - origin: l[m.ani.fLocX[i]], - destination: l[m.ani.tLocX[i]], + origin: locations[m.ani.fLocX[i]], // todo: default to null + destination: locations[m.ani.tLocX[i]], // todo: default to null t: m.ani.mSec[i] }) } diff --git a/parse/nearby.js b/parse/nearby.js index 896682c2..2d96a5d1 100644 --- a/parse/nearby.js +++ b/parse/nearby.js @@ -8,9 +8,9 @@ const parseLocation = require('./location') // todo: what is s.wt? // todo: what is s.dur? const parseNearby = (n) => { - const result = location(n) - result.distance = n.dist - return result + const res = parseLocation(n) + res.distance = n.dist + return res } module.exports = parseNearby diff --git a/parse/stopover.js b/parse/stopover.js index 9a413f77..0dea85bf 100644 --- a/parse/stopover.js +++ b/parse/stopover.js @@ -1,17 +1,18 @@ 'use strict' -// s = stations, ln = lines, r = remarks, c = connection -const createParseStopover = (tz, s, ln, r, c) => { +const parseDateTime = require('./date-time') + +const createParseStopover = (tz, stations, lines, remarks, connection) => { const parseStopover = (st) => { const res = { - station: s[parseInt(st.locX)] + station: stations[parseInt(st.locX)] // default to null } if (st.aTimeR || st.aTimeS) { - const arr = parseDateTime(tz, c.date, st.aTimeR || st.aTimeS) + const arr = parseDateTime(tz, connection.date, st.aTimeR || st.aTimeS) res.arrival = arr.format() } if (st.dTimeR || st.dTimeS) { - const dep = parseDateTime(tz, c.date, st.dTimeR || st.dTimeS) + const dep = parseDateTime(tz, connection.date, st.dTimeR || st.dTimeS) res.departure = dep.format() } return res From 73d083f418287287c1031e16a014456a239d651f Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 12 Nov 2017 00:36:13 +0100 Subject: [PATCH 05/73] parse: default to null --- parse/departure.js | 4 ++-- parse/journey.js | 7 +++---- parse/movement.js | 6 +++--- parse/stopover.js | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/parse/departure.js b/parse/departure.js index c01ea81d..d017973a 100644 --- a/parse/departure.js +++ b/parse/departure.js @@ -15,10 +15,10 @@ const createParseDeparture = (timezone, stations, lines, remarks) => { const when = parseDateTime(timezone, d.date, d.stbStop.dTimeR || d.stbStop.dTimeS) const res = { ref: d.jid, - station: stations[parseInt(d.stbStop.locX)], // todo: default to null + station: stations[parseInt(d.stbStop.locX)] || null, when: when.format(), direction: d.dirTxt, - line: lines[parseInt(d.prodX)], // todo: default to null + line: lines[parseInt(d.prodX)] || null, remarks: d.remL ? d.remL.map(findRemark) : [], trip: +d.jid.split('|')[1] // todo: this seems brittle } diff --git a/parse/journey.js b/parse/journey.js index da56dcf1..343a4147 100644 --- a/parse/journey.js +++ b/parse/journey.js @@ -38,8 +38,7 @@ const createParsePart = (tz, stations, lines, remarks, j) => { // j = journey const dep = parseDateTime(tz, j.date, pt.dep.dTimeR || pt.dep.dTimeS) const arr = parseDateTime(tz, j.date, pt.arr.aTimeR || pt.arr.aTimeS) const res = { - // todo: what about null? - origin: clone(stations[parseInt(pt.dep.locX)]), + origin: clone(stations[parseInt(pt.dep.locX)]) || null, destination: clone(stations[parseInt(pt.arr.locX)]), departure: dep.format(), arrival: arr.format() @@ -55,7 +54,7 @@ const createParsePart = (tz, stations, lines, remarks, j) => { // j = journey res.mode = 'walking' } else if (pt.type === 'JNY') { res.id = pt.jny.jid - res.line = lines[parseInt(pt.jny.prodX)] // todo: default null + res.line = lines[parseInt(pt.jny.prodX)] || null res.direction = pt.jny.dirTxt // todo: parse this if (pt.dep.dPlatfS) res.departurePlatform = pt.dep.dPlatfS @@ -71,7 +70,7 @@ const createParsePart = (tz, stations, lines, remarks, j) => { // j = journey if (pt.jny.freq && pt.jny.freq.jnyL) { const parseAlternative = (a) => ({ - line: lines[parseInt(a.prodX)], // todo: default null + line: lines[parseInt(a.prodX)] || null, when: parseDateTime(tz, j.date, a.stopL[0].dTimeS).format() // todo: realtime }) res.alternatives = pt.jny.freq.jnyL diff --git a/parse/movement.js b/parse/movement.js index 27ddce18..8c44038e 100644 --- a/parse/movement.js +++ b/parse/movement.js @@ -28,7 +28,7 @@ const createParseMovement = (tz, locations, lines, remarks) => { const res = { direction: m.dirTxt, - line: lines[m.prodX], // default to null + line: lines[m.prodX] || null, coordinates: m.pos ? { latitude: m.pos.y / 1000000, longitude: m.pos.x / 1000000 @@ -40,8 +40,8 @@ const createParseMovement = (tz, locations, lines, remarks) => { if (m.ani && Array.isArray(m.ani.mSec)) { for (let i = 0; i < m.ani.mSec.length; i++) { res.frames.push({ - origin: locations[m.ani.fLocX[i]], // todo: default to null - destination: locations[m.ani.tLocX[i]], // todo: default to null + origin: locations[m.ani.fLocX[i]] || null, + destination: locations[m.ani.tLocX[i]] || null, t: m.ani.mSec[i] }) } diff --git a/parse/stopover.js b/parse/stopover.js index 0dea85bf..ab49c27f 100644 --- a/parse/stopover.js +++ b/parse/stopover.js @@ -5,7 +5,7 @@ const parseDateTime = require('./date-time') const createParseStopover = (tz, stations, lines, remarks, connection) => { const parseStopover = (st) => { const res = { - station: stations[parseInt(st.locX)] // default to null + station: stations[parseInt(st.locX)] || null } if (st.aTimeR || st.aTimeS) { const arr = parseDateTime(tz, connection.date, st.aTimeR || st.aTimeS) From e6982753cd560a8b99c7e550b6b04f0d33de0e88 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 12 Nov 2017 00:45:51 +0100 Subject: [PATCH 06/73] split stringify.js --- package.json | 2 +- stringify.js | 46 ------------------------------------ stringify/address.js | 17 +++++++++++++ stringify/coord.js | 5 ++++ stringify/date.js | 9 +++++++ stringify/filters.js | 11 +++++++++ stringify/index.js | 11 +++++++++ stringify/location-filter.js | 8 +++++++ stringify/poi.js | 18 ++++++++++++++ stringify/station.js | 5 ++++ stringify/time.js | 9 +++++++ 11 files changed, 94 insertions(+), 47 deletions(-) delete mode 100644 stringify.js create mode 100644 stringify/address.js create mode 100644 stringify/coord.js create mode 100644 stringify/date.js create mode 100644 stringify/filters.js create mode 100644 stringify/index.js create mode 100644 stringify/location-filter.js create mode 100644 stringify/poi.js create mode 100644 stringify/station.js create mode 100644 stringify/time.js diff --git a/package.json b/package.json index 720b6374..01e7ae0d 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "index.js", "lib", "parse", - "stringify.js" + "stringify" ], "author": "Jannis R ", "homepage": "https://github.com/derhuerst/hafas-client", diff --git a/stringify.js b/stringify.js deleted file mode 100644 index e7063b0c..00000000 --- a/stringify.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict' - -const moment = require('moment-timezone') - - - -const date = (tz, when) => moment(when).tz(tz).format('YYYYMMDD') -const time = (tz, when) => moment(when).tz(tz).format('HHmmss') - - - -// filters -const bike = {type: 'BC', mode: 'INC'} -const accessibility = { - none: {type: 'META', mode: 'INC', meta: 'notBarrierfree'} - , partial: {type: 'META', mode: 'INC', meta: 'limitedBarrierfree'} - , complete: {type: 'META', mode: 'INC', meta: 'completeBarrierfree'} -} - - - -const coord = (x) => Math.round(x * 1000000) -const station = (id) => ({type: 'S', lid: 'L=' + id}) -const address = (latitude, longitude, name) => { - if (!latitude || !longitude || !name) throw new Error('invalid address.') - return {type: 'A', name, crd: {x: coord(longitude), y: coord(latitude)}} -} -const poi = (latitude, longitude, id, name) => { - if (!latitude || !longitude || !id || !name) throw new Error('invalid poi.') - return {type: 'P', name, lid: 'L=' + id, crd: {x: coord(longitude), y: coord(latitude)}} -} - -const locationFilter = (stations, addresses, poi) => { - if (stations && addresses && poi) return 'ALL' - return (stations ? 'S' : '') - + (addresses ? 'A' : '') - + (poi ? 'P' : '') -} - - - -module.exports = { - date, time, - bike, accessibility, - coord, station, address, poi, locationFilter -} diff --git a/stringify/address.js b/stringify/address.js new file mode 100644 index 00000000..19edd8e9 --- /dev/null +++ b/stringify/address.js @@ -0,0 +1,17 @@ +'use strict' + +const stringifyCoord = require('./coord') + +const stringifyAddress = (latitude, longitude, name) => { + if (!latitude || !longitude || !name) throw new Error('invalid address.') + return { + type: 'A', + name, + crd: { + x: stringifyCoord(longitude), + y: stringifyCoord(latitude) + } + } +} + +module.exports = stringifyAddress diff --git a/stringify/coord.js b/stringify/coord.js new file mode 100644 index 00000000..67a4d9cb --- /dev/null +++ b/stringify/coord.js @@ -0,0 +1,5 @@ +'use strict' + +const stringifyCoord = x => Math.round(x * 1000000) + +module.exports = stringifyCoord diff --git a/stringify/date.js b/stringify/date.js new file mode 100644 index 00000000..0649e487 --- /dev/null +++ b/stringify/date.js @@ -0,0 +1,9 @@ +'use strict' + +const moment = require('moment-timezone') + +const stringifyDate = (tz, when) => { + moment(when).tz(tz).format('YYYYMMDD') +} + +module.exports = stringifyDate diff --git a/stringify/filters.js b/stringify/filters.js new file mode 100644 index 00000000..45a17f7c --- /dev/null +++ b/stringify/filters.js @@ -0,0 +1,11 @@ +'use strict' + +const bike = {type: 'BC', mode: 'INC'} + +const accessibility = { + none: {type: 'META', mode: 'INC', meta: 'notBarrierfree'}, + partial: {type: 'META', mode: 'INC', meta: 'limitedBarrierfree'}, + complete: {type: 'META', mode: 'INC', meta: 'completeBarrierfree'} +} + +module.exports = {bike, accessibility} diff --git a/stringify/index.js b/stringify/index.js new file mode 100644 index 00000000..90e2831b --- /dev/null +++ b/stringify/index.js @@ -0,0 +1,11 @@ +'use strict' + +module.exports = { + date: require('./date'), + time: require('./time'), + filters: require('./filters'), + station: require('./station'), + address: require('./address'), + poi: require('./poi'), + locationFilter: require('./location-filter') +} diff --git a/stringify/location-filter.js b/stringify/location-filter.js new file mode 100644 index 00000000..a147b14b --- /dev/null +++ b/stringify/location-filter.js @@ -0,0 +1,8 @@ +'use strict' + +const stringifyLocationFilter = (stations, addresses, poi) => { + if (stations && addresses && poi) return 'ALL' + return (stations ? 'S' : '') + (addresses ? 'A' : '') + (poi ? 'P' : '') +} + +module.exports = stringifyLocationFilter diff --git a/stringify/poi.js b/stringify/poi.js new file mode 100644 index 00000000..f0d9cb32 --- /dev/null +++ b/stringify/poi.js @@ -0,0 +1,18 @@ +'use strict' + +const stringifyCoord = require('./coord') + +const stringifyPoi = (latitude, longitude, id, name) => { + if (!latitude || !longitude || !id || !name) throw new Error('invalid poi.') + return { + type: 'P', + name, + lid: 'L=' + id, + crd: { + x: stringifyCoord(longitude), + y: stringifyCoord(latitude) + } + } +} + +module.exports = stringifyPoi diff --git a/stringify/station.js b/stringify/station.js new file mode 100644 index 00000000..09d916e1 --- /dev/null +++ b/stringify/station.js @@ -0,0 +1,5 @@ +'use strict' + +const stringifyStation = id => ({type: 'S', lid: 'L=' + id}) + +module.exports = stringifyStation diff --git a/stringify/time.js b/stringify/time.js new file mode 100644 index 00000000..a50e4f29 --- /dev/null +++ b/stringify/time.js @@ -0,0 +1,9 @@ +'use strict' + +const moment = require('moment-timezone') + +const stringifyTime = (tz, when) => { + return moment(when).tz(tz).format('HHmmss') +} + +module.exports = stringifyTime From 5b96c89b5b3998c2052b358d0502f4df5b8516b9 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 12 Nov 2017 00:53:34 +0100 Subject: [PATCH 07/73] rename stringify -> format --- format/address.js | 17 +++++++++++++++++ format/coord.js | 5 +++++ {stringify => format}/date.js | 4 ++-- {stringify => format}/filters.js | 0 {stringify => format}/index.js | 0 {stringify => format}/location-filter.js | 4 ++-- format/poi.js | 18 ++++++++++++++++++ format/station.js | 5 +++++ {stringify => format}/time.js | 4 ++-- package.json | 2 +- stringify/address.js | 17 ----------------- stringify/coord.js | 5 ----- stringify/poi.js | 18 ------------------ stringify/station.js | 5 ----- 14 files changed, 52 insertions(+), 52 deletions(-) create mode 100644 format/address.js create mode 100644 format/coord.js rename {stringify => format}/date.js (59%) rename {stringify => format}/filters.js (100%) rename {stringify => format}/index.js (100%) rename {stringify => format}/location-filter.js (56%) create mode 100644 format/poi.js create mode 100644 format/station.js rename {stringify => format}/time.js (60%) delete mode 100644 stringify/address.js delete mode 100644 stringify/coord.js delete mode 100644 stringify/poi.js delete mode 100644 stringify/station.js diff --git a/format/address.js b/format/address.js new file mode 100644 index 00000000..62ebe39f --- /dev/null +++ b/format/address.js @@ -0,0 +1,17 @@ +'use strict' + +const formatCoord = require('./coord') + +const formatAddress = (latitude, longitude, name) => { + if (!latitude || !longitude || !name) throw new Error('invalid address.') + return { + type: 'A', + name, + crd: { + x: formatCoord(longitude), + y: formatCoord(latitude) + } + } +} + +module.exports = formatAddress diff --git a/format/coord.js b/format/coord.js new file mode 100644 index 00000000..7d1ff695 --- /dev/null +++ b/format/coord.js @@ -0,0 +1,5 @@ +'use strict' + +const formatCoord = x => Math.round(x * 1000000) + +module.exports = formatCoord diff --git a/stringify/date.js b/format/date.js similarity index 59% rename from stringify/date.js rename to format/date.js index 0649e487..f3c7947b 100644 --- a/stringify/date.js +++ b/format/date.js @@ -2,8 +2,8 @@ const moment = require('moment-timezone') -const stringifyDate = (tz, when) => { +const formatDate = (tz, when) => { moment(when).tz(tz).format('YYYYMMDD') } -module.exports = stringifyDate +module.exports = formatDate diff --git a/stringify/filters.js b/format/filters.js similarity index 100% rename from stringify/filters.js rename to format/filters.js diff --git a/stringify/index.js b/format/index.js similarity index 100% rename from stringify/index.js rename to format/index.js diff --git a/stringify/location-filter.js b/format/location-filter.js similarity index 56% rename from stringify/location-filter.js rename to format/location-filter.js index a147b14b..79a79706 100644 --- a/stringify/location-filter.js +++ b/format/location-filter.js @@ -1,8 +1,8 @@ 'use strict' -const stringifyLocationFilter = (stations, addresses, poi) => { +const formatLocationFilter = (stations, addresses, poi) => { if (stations && addresses && poi) return 'ALL' return (stations ? 'S' : '') + (addresses ? 'A' : '') + (poi ? 'P' : '') } -module.exports = stringifyLocationFilter +module.exports = formatLocationFilter diff --git a/format/poi.js b/format/poi.js new file mode 100644 index 00000000..4af29ea9 --- /dev/null +++ b/format/poi.js @@ -0,0 +1,18 @@ +'use strict' + +const formatCoord = require('./coord') + +const formatPoi = (latitude, longitude, id, name) => { + if (!latitude || !longitude || !id || !name) throw new Error('invalid poi.') + return { + type: 'P', + name, + lid: 'L=' + id, + crd: { + x: formatCoord(longitude), + y: formatCoord(latitude) + } + } +} + +module.exports = formatPoi diff --git a/format/station.js b/format/station.js new file mode 100644 index 00000000..418e5181 --- /dev/null +++ b/format/station.js @@ -0,0 +1,5 @@ +'use strict' + +const formatStation = id => ({type: 'S', lid: 'L=' + id}) + +module.exports = formatStation diff --git a/stringify/time.js b/format/time.js similarity index 60% rename from stringify/time.js rename to format/time.js index a50e4f29..8d25907f 100644 --- a/stringify/time.js +++ b/format/time.js @@ -2,8 +2,8 @@ const moment = require('moment-timezone') -const stringifyTime = (tz, when) => { +const formatTime = (tz, when) => { return moment(when).tz(tz).format('HHmmss') } -module.exports = stringifyTime +module.exports = formatTime diff --git a/package.json b/package.json index 01e7ae0d..7b55ba5e 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "index.js", "lib", "parse", - "stringify" + "format" ], "author": "Jannis R ", "homepage": "https://github.com/derhuerst/hafas-client", diff --git a/stringify/address.js b/stringify/address.js deleted file mode 100644 index 19edd8e9..00000000 --- a/stringify/address.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict' - -const stringifyCoord = require('./coord') - -const stringifyAddress = (latitude, longitude, name) => { - if (!latitude || !longitude || !name) throw new Error('invalid address.') - return { - type: 'A', - name, - crd: { - x: stringifyCoord(longitude), - y: stringifyCoord(latitude) - } - } -} - -module.exports = stringifyAddress diff --git a/stringify/coord.js b/stringify/coord.js deleted file mode 100644 index 67a4d9cb..00000000 --- a/stringify/coord.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict' - -const stringifyCoord = x => Math.round(x * 1000000) - -module.exports = stringifyCoord diff --git a/stringify/poi.js b/stringify/poi.js deleted file mode 100644 index f0d9cb32..00000000 --- a/stringify/poi.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict' - -const stringifyCoord = require('./coord') - -const stringifyPoi = (latitude, longitude, id, name) => { - if (!latitude || !longitude || !id || !name) throw new Error('invalid poi.') - return { - type: 'P', - name, - lid: 'L=' + id, - crd: { - x: stringifyCoord(longitude), - y: stringifyCoord(latitude) - } - } -} - -module.exports = stringifyPoi diff --git a/stringify/station.js b/stringify/station.js deleted file mode 100644 index 09d916e1..00000000 --- a/stringify/station.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict' - -const stringifyStation = id => ({type: 'S', lid: 'L=' + id}) - -module.exports = stringifyStation From 3c9f3393bae09e0616c5439cc6b495fd8e0e21fc Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 12 Nov 2017 01:23:34 +0100 Subject: [PATCH 08/73] pass in parsers --- index.js | 64 +++++++++++++++++++++++++++++----------------- lib/request.js | 16 +++++++++--- parse/date-time.js | 4 +-- parse/departure.js | 11 ++++---- parse/journey.js | 50 +++++++++++++++++------------------- parse/line.js | 3 ++- parse/location.js | 11 ++++---- parse/movement.js | 8 +++--- parse/nearby.js | 6 ++--- parse/operator.js | 3 ++- parse/remark.js | 3 ++- parse/stopover.js | 8 +++--- 12 files changed, 105 insertions(+), 82 deletions(-) diff --git a/index.js b/index.js index 3d7abf43..607180b6 100644 --- a/index.js +++ b/index.js @@ -1,41 +1,59 @@ 'use strict' -const parseLocation = require('./parse/location') +const parseDateTime = require('./parse/date-time') +const parseDeparture = require('./parse/departure') +const parseJourney = require('./parse/journey') const parseLine = require('./parse/line') -const parseRemark = require('./parse/remark') +const parseLocation = require('./parse/location') +const parseMovement = require('./parse/movement') +const parseNearby = require('./parse/nearby') const parseOperator = require('./parse/operator') +const parseRemark = require('./parse/remark') +const parseStopover = require('./parse/stopover') + +const formatAddress = require('./format/address') +const formatCoord = require('./format/coord') +const formatDate = require('./format/date') +const filters = require('./format/filters') +const formatLocationFilter = require('./format/location-filter') +const formatPoi = require('./format/poi') +const formatStation = require('./format/station') +const formatTime = require('./format/time') + const request = require('./lib/request') const id = x => x +// todo: find out which are actually necessary const defaultProfile = { transformReqBody: id, transformReq: id, - parseLocation: parseLocation, - parseLine: parseLine, - parseRemark: parseRemark, - parseOperator: parseOperator + + parseDateTime, + parseDeparture, + parseJourney, + parseLine, + parseLocation, + parseMovement, + parseNearby, + parseOperator, + parseRemark, + parseStopover, + + formatAddress, + formatCoord, + formatDate, + filters, + formatLocationFilter, + formatPoi, + formatStation, + formatTime } const createClient = (profile) => { profile = Object.assign({}, defaultProfile, profile) - if ('function' !== profile.transformReqBody) { - throw new Error('profile.transformReqBody must be a function.') - } - if ('function' !== profile.transformReq) { - throw new Error('profile.transformReq must be a function.') - } - if ('function' !== profile.parseLocation) { - throw new Error('profile.parseLocation must be a function.') - } - if ('function' !== profile.parseLine) { - throw new Error('profile.parseLine must be a function.') - } - if ('function' !== profile.parseRemark) { - throw new Error('profile.parseRemark must be a function.') - } - if ('function' !== profile.parseOperator) { - throw new Error('profile.parseOperator must be a function.') + if ('string' !== typeof profile.timezone) { + throw new Error('profile.timezone must be a string.') } const client = data => request(profile, data) diff --git a/lib/request.js b/lib/request.js index 6affd575..c693c092 100644 --- a/lib/request.js +++ b/lib/request.js @@ -42,10 +42,18 @@ const request = (profile, data) => { const d = b.svcResL[0].res const c = d.common || {} - if (Array.isArray(c.locL)) d.locations = c.locL.map(profile.parseLocation) - if (Array.isArray(c.prodL)) d.lines = c.prodL.map(profile.parseLine) - if (Array.isArray(c.remL)) d.remarks = c.remL.map(profile.parseRemark) - if (Array.isArray(c.opL)) d.operators = c.opL.map(profile.parseOperator) + if (Array.isArray(c.locL)) { + d.locations = c.locL.map(loc => profile.parseLocation(profile, loc)) + } + if (Array.isArray(c.prodL)) { + d.lines = c.prodL.map(line => profile.parseLine(profile, line)) + } + if (Array.isArray(c.remL)) { + d.remarks = c.remL.map(rem => profile.parseRemark(profile, rem)) + } + if (Array.isArray(c.opL)) { + d.operators = c.opL.map(op => profile.parseOperator(profile, op)) + } return d }) } diff --git a/parse/date-time.js b/parse/date-time.js index 4c913e1c..7625d0a6 100644 --- a/parse/date-time.js +++ b/parse/date-time.js @@ -2,14 +2,14 @@ const moment = require('moment-timezone') -const parseDateTime = (timezone, date, time) => { +const parseDateTime = (profile, date, time) => { let offset = 0 // in days if (time.length > 6) { offset = +time.slice(0, -6) time = time.slice(-6) } - return moment.tz(date + 'T' + time, timezone) + return moment.tz(date + 'T' + time, profile.timezone) .add(offset, 'days') } diff --git a/parse/departure.js b/parse/departure.js index d017973a..f159a98e 100644 --- a/parse/departure.js +++ b/parse/departure.js @@ -1,18 +1,17 @@ 'use strict' -const parseDateTime = require('./date-time') - // todos from derhuerst/hafas-client#2 // - stdStop.dCncl // - stdStop.dPlatfS, stdStop.dPlatfR // todo: what is d.jny.dirFlg? // todo: d.stbStop.dProgType -const createParseDeparture = (timezone, stations, lines, remarks) => { +const createParseDeparture = (profile, stations, lines, remarks) => { + const tz = profile.timezone const findRemark = rm => remarks[parseInt(rm.remX)] || null const parseDeparture = (d) => { - const when = parseDateTime(timezone, d.date, d.stbStop.dTimeR || d.stbStop.dTimeS) + const when = profile.parseDateTime(tz, d.date, d.stbStop.dTimeR || d.stbStop.dTimeS) const res = { ref: d.jid, station: stations[parseInt(d.stbStop.locX)] || null, @@ -24,8 +23,8 @@ const createParseDeparture = (timezone, stations, lines, remarks) => { } if (d.stbStop.dTimeR && d.stbStop.dTimeS) { - const realtime = parseDateTime(timezone, d.date, d.stbStop.dTimeR) - const planned = parseDateTime(timezone, d.date, d.stbStop.dTimeS) + const realtime = profile.parseDateTime(tz, d.date, d.stbStop.dTimeR) + const planned = profile.parseDateTime(tz, d.date, d.stbStop.dTimeS) res.delay = Math.round((realtime - planned) / 1000) } else res.delay = null diff --git a/parse/journey.js b/parse/journey.js index 343a4147..cb8831e8 100644 --- a/parse/journey.js +++ b/parse/journey.js @@ -4,8 +4,10 @@ const parseDateTime = require('./date-time') const clone = obj => Object.assign({}, obj) -const createParseStopover = (tz, stations, lines, remarks, j) => { // j = journey - const parseStopover = (st) => { +const createParseJourney = (profile, stations, lines, remarks) => { + const tz = profile.timezone + + const parseStopover = (j, st) => { const res = { station: stations[parseInt(st.locX)] } @@ -17,26 +19,20 @@ const createParseStopover = (tz, stations, lines, remarks, j) => { // j = journe const dep = parseDateTime(tz, j.date, st.dTimeR || st.dTimeS) res.departure = dep.format() } + return res } - return parseStopover -} - -const createApplyRemark = (stations, lines, remarks, j) => { // j = journey // todo: finish parse/remark.js first - const applyRemark = (rm) => {} - return applyRemark -} + const applyRemark = (j, rm) => {} -const createParsePart = (tz, stations, lines, remarks, j) => { // j = journey // todo: pt.sDays // todo: pt.dep.dProgType, pt.arr.dProgType // todo: what is pt.jny.dirFlg? // todo: how does pt.freq work? - const parsePart = (pt) => { - const dep = parseDateTime(tz, j.date, pt.dep.dTimeR || pt.dep.dTimeS) - const arr = parseDateTime(tz, j.date, pt.arr.aTimeR || pt.arr.aTimeS) + const parsePart = (j, pt) => { // j = journey, pt = part + const dep = profile.parseDateTime(tz, j.date, pt.dep.dTimeR || pt.dep.dTimeS) + const arr = profile.parseDateTime(tz, j.date, pt.arr.aTimeR || pt.arr.aTimeS) const res = { origin: clone(stations[parseInt(pt.dep.locX)]) || null, destination: clone(stations[parseInt(pt.arr.locX)]), @@ -45,8 +41,8 @@ const createParsePart = (tz, stations, lines, remarks, j) => { // j = journey } if (pt.dep.dTimeR && pt.dep.dTimeS) { - const realtime = parseDateTime(tz, j.date, pt.dep.dTimeR) - const planned = parseDateTime(tz, j.date, pt.dep.dTimeS) + const realtime = profile.parseDateTime(tz, j.date, pt.dep.dTimeR) + const planned = profile.parseDateTime(tz, j.date, pt.dep.dTimeS) res.delay = Math.round((realtime - planned) / 1000) } @@ -61,18 +57,21 @@ const createParsePart = (tz, stations, lines, remarks, j) => { // j = journey if (pt.arr.aPlatfS) res.arrivalPlatform = pt.arr.aPlatfS if (pt.jny.stopL) { - const parseStopover = createParseStopover(tz, stations, lines, remarks, j) - res.passed = pt.jny.stopL.map(parseStopover) + res.passed = pt.jny.stopL.map(stopover => parseStopover(j, stopover)) } if (Array.isArray(pt.jny.remL)) { - pt.jny.remL.forEach(createApplyRemark(stations, lines, remarks, j)) + for (let remark of pt.jny.remL) applyRemark(j, remark) } if (pt.jny.freq && pt.jny.freq.jnyL) { - const parseAlternative = (a) => ({ - line: lines[parseInt(a.prodX)] || null, - when: parseDateTime(tz, j.date, a.stopL[0].dTimeS).format() // todo: realtime - }) + const parseAlternative = (a) => { + // todo: realtime + const when = profile.parseDateTime(tz, j.date, a.stopL[0].dTimeS) + return { + line: lines[parseInt(a.prodX)] || null, + when: when.format() + } + } res.alternatives = pt.jny.freq.jnyL .filter(a => a.stopL[0].locX === pt.dep.locX) .map(parseAlternative) @@ -81,17 +80,14 @@ const createParsePart = (tz, stations, lines, remarks, j) => { // j = journey return res } - return parsePart -} -const createParseJourney = (tz, stations, lines, remarks, p = createParsePart) => { // todo: c.sDays // todo: c.dep.dProgType, c.arr.dProgType // todo: c.conSubscr // todo: c.trfRes x vbb-parse-ticket // todo: use computed information from part - const parseJourney = (journey) => { - const parts = journey.secL.map(p(tz, stations, lines, remarks, journey)) + const parseJourney = (j) => { + const parts = j.secL.map(part => parsePart(j, part)) return { parts, origin: parts[0].origin, diff --git a/parse/line.js b/parse/line.js index 7094acd6..b9eb5e52 100644 --- a/parse/line.js +++ b/parse/line.js @@ -3,7 +3,8 @@ // todo: what is p.number vs p.line? // todo: what is p.icoX? // todo: what is p.oprX? -const parseLine = (p) => { +// todo: is passing in profile necessary? +const parseLine = (profile, p) => { if (!p) return null // todo: handle this upstream const res = {type: 'line', name: p.line || p.name} diff --git a/parse/location.js b/parse/location.js index 405498b8..70d59f81 100644 --- a/parse/location.js +++ b/parse/location.js @@ -6,9 +6,10 @@ types.S = 'station' types.A = 'address' // todo: what is s.rRefL? -const parseLocation = (l) => { +// todo: is passing in profile necessary? +const parseLocation = (profile, l) => { const type = types[l.type] || 'unknown' - const result = { + const res = { type, name: l.name, coordinates: l.crd ? { @@ -17,10 +18,10 @@ const parseLocation = (l) => { } : null } - if (type === 'poi' || type === 'station') result.id = l.extId - if ('pCls' in l) result.products = l.pCls + if (type === 'poi' || type === 'station') res.id = l.extId + if ('pCls' in l) res.products = l.pCls - return result + return res } module.exports = parseLocation diff --git a/parse/movement.js b/parse/movement.js index 8c44038e..e9662291 100644 --- a/parse/movement.js +++ b/parse/movement.js @@ -1,8 +1,8 @@ 'use strict' -const parseDateTime = require('./date-time') +const createParseMovement = (profile, locations, lines, remarks) => { + const tz = profile.timezone -const createParseMovement = (tz, locations, lines, remarks) => { // todo: what is m.dirGeo? maybe the speed? // todo: what is m.stopL? // todo: what is m.proc? wut? @@ -13,10 +13,10 @@ const createParseMovement = (tz, locations, lines, remarks) => { const parseMovement = (m) => { const parseNextStop = (s) => { const dep = s.dTimeR || s.dTimeS - ? parseDateTime(tz, m.date, s.dTimeR || s.dTimeS) + ? profile.parseDateTime(tz, m.date, s.dTimeR || s.dTimeS) : null const arr = s.aTimeR || s.aTimeS - ? parseDateTime(tz, m.date, s.aTimeR || s.aTimeS) + ? profile.parseDateTime(tz, m.date, s.aTimeR || s.aTimeS) : null return { diff --git a/parse/nearby.js b/parse/nearby.js index 2d96a5d1..e009b405 100644 --- a/parse/nearby.js +++ b/parse/nearby.js @@ -1,14 +1,12 @@ 'use strict' -const parseLocation = require('./location') - // todo: remarks // todo: lines // todo: what is s.pCls? // todo: what is s.wt? // todo: what is s.dur? -const parseNearby = (n) => { - const res = parseLocation(n) +const parseNearby = (profile, n) => { + const res = profile.parseLocation(profile, n) res.distance = n.dist return res } diff --git a/parse/operator.js b/parse/operator.js index a77f217b..b3682199 100644 --- a/parse/operator.js +++ b/parse/operator.js @@ -2,7 +2,8 @@ const slugg = require('slugg') -const parseOperator = (a) => { +// todo: is passing in profile necessary? +const parseOperator = (profile, a) => { return { type: 'operator', id: slugg(a.name), // todo: find a more reliable way diff --git a/parse/remark.js b/parse/remark.js index d08b1500..0b8c7a52 100644 --- a/parse/remark.js +++ b/parse/remark.js @@ -1,6 +1,7 @@ 'use strict' -const parseRemark = (r) => { +// todo: is passing in profile necessary? +const parseRemark = (profile, r) => { return null // todo } diff --git a/parse/stopover.js b/parse/stopover.js index ab49c27f..09b6a80b 100644 --- a/parse/stopover.js +++ b/parse/stopover.js @@ -1,18 +1,18 @@ 'use strict' -const parseDateTime = require('./date-time') +const createParseStopover = (profile, stations, lines, remarks, connection) => { + const tz = profile.timezone -const createParseStopover = (tz, stations, lines, remarks, connection) => { const parseStopover = (st) => { const res = { station: stations[parseInt(st.locX)] || null } if (st.aTimeR || st.aTimeS) { - const arr = parseDateTime(tz, connection.date, st.aTimeR || st.aTimeS) + const arr = profile.parseDateTime(tz, connection.date, st.aTimeR || st.aTimeS) res.arrival = arr.format() } if (st.dTimeR || st.dTimeS) { - const dep = parseDateTime(tz, connection.date, st.dTimeR || st.dTimeS) + const dep = profile.parseDateTime(tz, connection.date, st.dTimeR || st.dTimeS) res.departure = dep.format() } return res From eb98123e5bea1999af729b3fe4c715b3a4d480dd Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 12 Nov 2017 14:52:04 +0100 Subject: [PATCH 09/73] move out default profile, bugfixes :bug: --- format/date.js | 4 ++-- format/time.js | 4 ++-- index.js | 51 ++---------------------------------------- lib/default-profile.js | 51 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 53 deletions(-) create mode 100644 lib/default-profile.js diff --git a/format/date.js b/format/date.js index f3c7947b..37e910fa 100644 --- a/format/date.js +++ b/format/date.js @@ -2,8 +2,8 @@ const moment = require('moment-timezone') -const formatDate = (tz, when) => { - moment(when).tz(tz).format('YYYYMMDD') +const formatDate = (profile, when) => { + return moment(when).tz(profile.timezone).format('YYYYMMDD') } module.exports = formatDate diff --git a/format/time.js b/format/time.js index 8d25907f..348f402d 100644 --- a/format/time.js +++ b/format/time.js @@ -2,8 +2,8 @@ const moment = require('moment-timezone') -const formatTime = (tz, when) => { - return moment(when).tz(tz).format('HHmmss') +const formatTime = (profile, when) => { + return moment(when).tz(profile.timezone).format('HHmmss') } module.exports = formatTime diff --git a/index.js b/index.js index 607180b6..47aba5f9 100644 --- a/index.js +++ b/index.js @@ -1,55 +1,8 @@ 'use strict' -const parseDateTime = require('./parse/date-time') -const parseDeparture = require('./parse/departure') -const parseJourney = require('./parse/journey') -const parseLine = require('./parse/line') -const parseLocation = require('./parse/location') -const parseMovement = require('./parse/movement') -const parseNearby = require('./parse/nearby') -const parseOperator = require('./parse/operator') -const parseRemark = require('./parse/remark') -const parseStopover = require('./parse/stopover') - -const formatAddress = require('./format/address') -const formatCoord = require('./format/coord') -const formatDate = require('./format/date') -const filters = require('./format/filters') -const formatLocationFilter = require('./format/location-filter') -const formatPoi = require('./format/poi') -const formatStation = require('./format/station') -const formatTime = require('./format/time') - +const defaultProfile = require('./lib/default-profile') const request = require('./lib/request') -const id = x => x - -// todo: find out which are actually necessary -const defaultProfile = { - transformReqBody: id, - transformReq: id, - - parseDateTime, - parseDeparture, - parseJourney, - parseLine, - parseLocation, - parseMovement, - parseNearby, - parseOperator, - parseRemark, - parseStopover, - - formatAddress, - formatCoord, - formatDate, - filters, - formatLocationFilter, - formatPoi, - formatStation, - formatTime -} - const createClient = (profile) => { profile = Object.assign({}, defaultProfile, profile) if ('string' !== typeof profile.timezone) { @@ -60,4 +13,4 @@ const createClient = (profile) => { return client } -module.exports = createRequest +module.exports = createClient diff --git a/lib/default-profile.js b/lib/default-profile.js new file mode 100644 index 00000000..66995dba --- /dev/null +++ b/lib/default-profile.js @@ -0,0 +1,51 @@ +'use strict' + +const parseDateTime = require('../parse/date-time') +const parseDeparture = require('../parse/departure') +const parseJourney = require('../parse/journey') +const parseLine = require('../parse/line') +const parseLocation = require('../parse/location') +const parseMovement = require('../parse/movement') +const parseNearby = require('../parse/nearby') +const parseOperator = require('../parse/operator') +const parseRemark = require('../parse/remark') +const parseStopover = require('../parse/stopover') + +const formatAddress = require('../format/address') +const formatCoord = require('../format/coord') +const formatDate = require('../format/date') +const filters = require('../format/filters') +const formatLocationFilter = require('../format/location-filter') +const formatPoi = require('../format/poi') +const formatStation = require('../format/station') +const formatTime = require('../format/time') + +const id = x => x + +// todo: find out which are actually necessary +const defaultProfile = { + transformReqBody: id, + transformReq: id, + + parseDateTime, + parseDeparture, + parseJourney, + parseLine, + parseLocation, + parseMovement, + parseNearby, + parseOperator, + parseRemark, + parseStopover, + + formatAddress, + formatCoord, + formatDate, + filters, + formatLocationFilter, + formatPoi, + formatStation, + formatTime +} + +module.exports = defaultProfile From f86f908dcc2972c46b3021b4a98703c73e3f0ed7 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 12 Nov 2017 18:06:16 +0100 Subject: [PATCH 10/73] query departures --- index.js | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 47aba5f9..ff8b1401 100644 --- a/index.js +++ b/index.js @@ -9,8 +9,40 @@ const createClient = (profile) => { throw new Error('profile.timezone must be a string.') } - const client = data => request(profile, data) - return client + const departures = (station, opt = {}) => { + if ('string' !== typeof station) throw new Error('station must be a string.') + + opt = Object.assign({ + direction: null, // only show departures heading to this station + duration: 10 // show departures for the next n minutes + }, opt) + opt.when = opt.when || new Date() + const products = profile.formatProducts(opt.products || {}) + + const dir = opt.direction ? profile.formatStation(opt.direction) : null + return request(profile, { + meth: 'StationBoard', + req: { + type: 'DEP', + date: profile.formatDate(profile, opt.when), + time: profile.formatTime(profile, opt.when), + stbLoc: profile.formatStation(station), + dirLoc: dir, + jnyFltrL: [ + profile.formatProducts(opt.products) // todo + ], + dur: opt.duration, + getPasslist: false + } + }) + .then((d) => { + if (!Array.isArray(d.jnyL)) return [] // todo: throw err? + const parse = profile.parseDeparture(profile, d.locations, d.lines, d.remarks) + return d.jnyL.map(parse) + }) + } + + return {departures} } module.exports = createClient From c20fd35c67e432dc39cd4ef3f2483b1443780904 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 12 Nov 2017 19:15:13 +0100 Subject: [PATCH 11/73] formatters: add location, change address & poi --- format/address.js | 12 +++++++----- format/index.js | 1 + format/location.js | 14 ++++++++++++++ format/poi.js | 14 ++++++++------ lib/default-profile.js | 4 +++- 5 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 format/location.js diff --git a/format/address.js b/format/address.js index 62ebe39f..b629ecc6 100644 --- a/format/address.js +++ b/format/address.js @@ -2,14 +2,16 @@ const formatCoord = require('./coord') -const formatAddress = (latitude, longitude, name) => { - if (!latitude || !longitude || !name) throw new Error('invalid address.') +const formatAddress = (a) => { + // todo: type-checking, better error msgs + if (!a.latitude || !a.longitude || !a.name) throw new Error('invalid address.') + return { type: 'A', - name, + name: a.name, crd: { - x: formatCoord(longitude), - y: formatCoord(latitude) + x: formatCoord(a.longitude), + y: formatCoord(a.latitude) } } } diff --git a/format/index.js b/format/index.js index 90e2831b..75f69239 100644 --- a/format/index.js +++ b/format/index.js @@ -7,5 +7,6 @@ module.exports = { station: require('./station'), address: require('./address'), poi: require('./poi'), + location: require('./location'), locationFilter: require('./location-filter') } diff --git a/format/location.js b/format/location.js new file mode 100644 index 00000000..3401e32e --- /dev/null +++ b/format/location.js @@ -0,0 +1,14 @@ +'use strict' + +const formatLocation = (profile, l) => { + if ('string' === typeof l) return profile.formatStation(l) + if ('object' === typeof l) { + if (l.type === 'station') return profile.formatStation(l.id) + if (l.type === 'poi') return profile.formatPoi(l) + if (l.type === 'address') return profile.formatAddress(l) + throw new Error('invalid location type: ' + l.type) + } + throw new Error('valid station, address or poi required.') +} + +module.exports = formatLocation diff --git a/format/poi.js b/format/poi.js index 4af29ea9..cef0c55a 100644 --- a/format/poi.js +++ b/format/poi.js @@ -2,15 +2,17 @@ const formatCoord = require('./coord') -const formatPoi = (latitude, longitude, id, name) => { - if (!latitude || !longitude || !id || !name) throw new Error('invalid poi.') +const formatPoi = (p) => { + // todo: type-checking, better error msgs + if (!p.latitude || !p.longitude || !p.id || !p.name) throw new Error('invalid poi.') + return { type: 'P', - name, - lid: 'L=' + id, + name: p.name, + lid: 'L=' + p.id, crd: { - x: formatCoord(longitude), - y: formatCoord(latitude) + x: formatCoord(p.longitude), + y: formatCoord(p.latitude) } } } diff --git a/lib/default-profile.js b/lib/default-profile.js index 66995dba..c5257b82 100644 --- a/lib/default-profile.js +++ b/lib/default-profile.js @@ -19,6 +19,7 @@ const formatLocationFilter = require('../format/location-filter') const formatPoi = require('../format/poi') const formatStation = require('../format/station') const formatTime = require('../format/time') +const formatLocation = require('../format/location') const id = x => x @@ -45,7 +46,8 @@ const defaultProfile = { formatLocationFilter, formatPoi, formatStation, - formatTime + formatTime, + formatLocation } module.exports = defaultProfile From 2c2826217e45543e080d1a53cb1db500dea81651 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 12 Nov 2017 20:02:32 +0100 Subject: [PATCH 12/73] query journeys --- index.js | 57 +++++++++++++++++++++++++++++++++++++++--- lib/default-profile.js | 2 ++ 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index ff8b1401..73615013 100644 --- a/index.js +++ b/index.js @@ -28,9 +28,7 @@ const createClient = (profile) => { time: profile.formatTime(profile, opt.when), stbLoc: profile.formatStation(station), dirLoc: dir, - jnyFltrL: [ - profile.formatProducts(opt.products) // todo - ], + jnyFltrL: [products], dur: opt.duration, getPasslist: false } @@ -42,7 +40,58 @@ const createClient = (profile) => { }) } - return {departures} + const journeys = (from, to, opt = {}) => { + from = profile.formatLocation(profile, from) + to = profile.formatLocation(profile, to) + + opt = Object.assign({ + results: 5, // how many journeys? + via: null, // let journeys pass this station? + passedStations: false, // return stations on the way? + transfers: 5, // maximum of 5 transfers + transferTime: 0, // minimum time for a single transfer in minutes + // todo: does this work with every endpoint? + accessibility: 'none', // 'none', 'partial' or 'complete' + bike: false, // only bike-friendly journeys + }, opt) + if (opt.via) opt.via = profile.formatLocation(profile, opt.via) + opt.when = opt.when || new Date() + const products = profile.formatProducts(opt.products || {}) + + 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 ? [opt.via] : null, + arrLocL: [to], + jnyFltrL: [products], + + // todo: what is req.gisFltrL? + // todo: what are all these for? + getPT: true, + outFrwd: true, + getTariff: false, + getIV: false, // walk & bike as alternatives? + getPolyline: false // 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) + return d.outConL.map(parse) + }) + } + + return {departures, journeys} } module.exports = createClient diff --git a/lib/default-profile.js b/lib/default-profile.js index c5257b82..5b1681b8 100644 --- a/lib/default-profile.js +++ b/lib/default-profile.js @@ -28,6 +28,8 @@ const defaultProfile = { transformReqBody: id, transformReq: id, + transformJourneysQuery: id, + parseDateTime, parseDeparture, parseJourney, From 7c7fb53b55bd1144ccc8da029f4e5e9de25c8689 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 12 Nov 2017 20:19:33 +0100 Subject: [PATCH 13/73] query locations --- index.js | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 73615013..7d23af94 100644 --- a/index.js +++ b/index.js @@ -91,7 +91,37 @@ const createClient = (profile) => { }) } - return {departures, journeys} + const locations = (query, opt = {}) => { + if ('string' !== typeof query) throw new Error('query must be a string.') + opt = Object.assign({ + fuzzy: true, // find only exact matches? + results: 10, // how many search results? + stations: true, + addresses: true, + poi: true // points of interest + }, opt) + + const f = profile.formatLocationFilter(opt.stations, opt.addresses, opt.poi) + return request(profile, { + cfg: {polyEnc: 'GPA'}, + meth: 'LocMatch', + req: {input: { + loc: { + type: f, + name: opt.fuzzy ? query + '?' : query + }, + maxLoc: opt.results, + field: 'S' // todo: what is this? + }} + }) + .then((d) => { + if (!d.match || !Array.isArray(d.match.locL)) return [] + const parse = profile.parseLocation + return d.match.locL.map(loc => parse(profile, loc)) + }) + } + + return {departures, journeys, locations} } module.exports = createClient From cd8d2ec297aef3ff17d6c93cd32ee9543ddd57e0 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 12 Nov 2017 20:29:57 +0100 Subject: [PATCH 14/73] query nearby locations --- index.js | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 7d23af94..d190e036 100644 --- a/index.js +++ b/index.js @@ -121,7 +121,41 @@ const createClient = (profile) => { }) } - return {departures, journeys, locations} + const nearby = (latitude, longitude, opt = {}) => { + if ('number' !== typeof latitude) throw new Error('latitude must be a number.') + if ('number' !== typeof longitude) throw new Error('longitude must be a number.') + opt = Object.assign({ + results: 8, // maximum number of results + distance: null, // maximum walking distance in meters + poi: false, // return points of interest? + stations: true, // return stations? + }, opt) + + return request(profile, { + cfg: {polyEnc: 'GPA'}, + meth: 'LocGeoPos', + req: { + ring: { + cCrd: { + x: profile.formatCoord(longitude), + y: profile.formatCoord(latitude) + }, + maxDist: opt.distance || -1, + minDist: 0 + }, + getPOIs: !!opt.poi, + getStops: !!opt.stations, + maxLoc: opt.results + } + }) + .then((d) => { + if (!Array.isArray(d.locL)) return [] + const parse = profile.parseNearby + return d.locL.map(loc => parse(profile, loc)) + }) + } + + return {departures, journeys, locations, nearby} } module.exports = createClient From 0a5b1a0c68e2d277f4b2174de0fbc28680d97311 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 12 Nov 2017 21:03:24 +0100 Subject: [PATCH 15/73] copy tests from db-hafas :white_check_mark: --- package.json | 10 ++- test/db.js | 169 ++++++++++++++++++++++++++++++++++++++++++++++++++ test/index.js | 3 + test/util.js | 131 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 312 insertions(+), 1 deletion(-) create mode 100644 test/db.js create mode 100644 test/index.js create mode 100644 test/util.js diff --git a/package.json b/package.json index 7b55ba5e..7ff792c0 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,16 @@ "query-string": "^5.0.0", "slugg": "^1.2.0" }, + "devDependencies": { + "db-stations": "^1.25.0", + "floordate": "^3.0.0", + "is-roughly-equal": "^0.1.0", + "tap-spec": "^4.1.1", + "tape": "^4.8.0", + "tape-promise": "^2.0.1" + }, "scripts": { - "test": "node -e \"require('.')\"", + "test": "node test/index.js | tap-spec", "prepublishOnly": "npm test" } } diff --git a/test/db.js b/test/db.js new file mode 100644 index 00000000..63bbd389 --- /dev/null +++ b/test/db.js @@ -0,0 +1,169 @@ +'use strict' + +const tapePromise = require('tape-promise').default +const tape = require('tape') +const isRoughlyEqual = require('is-roughly-equal') + +const createClient = require('..') +const { + findStation, + assertValidStation, + assertValidPoi, + assertValidAddress, + assertValidLocation, + assertValidLine, + assertValidStopover, + isJungfernheide, assertIsJungfernheide, + when, isValidWhen +} = require('./util.js') + +const test = tapePromise(tape) +const client = createClient(dbProfile) + +test('Berlin Jungfernheide to München Hbf', async (t) => { + const journeys = await client.journeys('8011167', '8000261', { + when, passedStations: true + }) + + t.ok(Array.isArray(journeys)) + t.ok(journeys.length > 0, 'no journeys') + for (let journey of journeys) { + assertValidStation(t, journey.origin) + if (!await findStation(journey.origin.id)) { + console.error('unknown station', journey.origin.id, journey.origin.name) + } + t.ok(isValidWhen(journey.departure)) + + assertValidStation(t, journey.destination) + if (!await findStation(journey.origin.id)) { + console.error('unknown station', journey.destination.id, journey.destination.name) + } + t.ok(isValidWhen(journey.arrival)) + + t.ok(Array.isArray(journey.parts)) + t.ok(journey.parts.length > 0, 'no parts') + const part = journey.parts[0] + + assertValidStation(t, part.origin) + if (!await findStation(part.origin.id)) { + console.error('unknown station', part.origin.id, part.origin.name) + } + t.ok(isValidWhen(part.departure)) + t.equal(typeof part.departurePlatform, 'string') + + assertValidStation(t, part.destination) + if (!await findStation(part.destination.id)) { + console.error('unknown station', part.destination.id, part.destination.name) + } + t.ok(isValidWhen(part.arrival)) + t.equal(typeof part.arrivalPlatform, 'string') + + assertValidLine(t, part.line) + + t.ok(Array.isArray(part.passed)) + for (let stopover of part.passed) assertValidStopover(t, stopover) + } + + t.end() +}) + +test('Berlin Jungfernheide to Torfstraße 17', async (t) => { + const journeys = await client.journeys('8011167', { + type: 'address', name: 'Torfstraße 17', + latitude: 52.5416823, longitude: 13.3491223 + }, {when}) + + t.ok(Array.isArray(journeys)) + t.ok(journeys.length >= 1, 'no journeys') + const journey = journeys[0] + const part = journey.parts[journey.parts.length - 1] + + assertValidStation(t, part.origin) + if (!await findStation(part.origin.id)) { + console.error('unknown station', part.origin.id, part.origin.name) + } + t.ok(isValidWhen(part.departure)) + t.ok(isValidWhen(part.arrival)) + + const d = part.destination + assertValidAddress(t, d) + t.equal(d.name, 'Torfstraße 17') + t.ok(isRoughlyEqual(.0001, d.coordinates.latitude, 52.5416823)) + t.ok(isRoughlyEqual(.0001, d.coordinates.longitude, 13.3491223)) + + t.end() +}) + +test('Berlin Jungfernheide to ATZE Musiktheater', async (t) => { + const journeys = await client.journeys('8011167', { + type: 'poi', name: 'ATZE Musiktheater', id: '991598902', + latitude: 52.542417, longitude: 13.350437 + }, {when}) + + t.ok(Array.isArray(journeys)) + t.ok(journeys.length >= 1, 'no journeys') + const journey = journeys[0] + const part = journey.parts[journey.parts.length - 1] + + assertValidStation(t, part.origin) + if (!await findStation(part.origin.id)) { + console.error('unknown station', part.origin.id, part.origin.name) + } + t.ok(isValidWhen(part.departure)) + t.ok(isValidWhen(part.arrival)) + + const d = part.destination + assertValidPoi(t, d) + t.equal(d.name, 'ATZE Musiktheater') + t.ok(isRoughlyEqual(.0001, d.coordinates.latitude, 52.542399)) + t.ok(isRoughlyEqual(.0001, d.coordinates.longitude, 13.350402)) + + t.end() +}) + +test('departures at Berlin Jungfernheide', async (t) => { + const deps = await client.departures('8011167', { + duration: 5, when + }) + + t.ok(Array.isArray(deps)) + for (let dep of deps) { + assertValidStation(t, dep.station) + if (!await findStation(dep.station.id)) { + console.error('unknown station', dep.station.id, dep.station.name) + } + t.ok(isValidWhen(dep.when)) + } + + t.end() +}) + +test('nearby Berlin Jungfernheide', async (t) => { + const nearby = await client.nearby(52.530273, 13.299433, { + results: 2, distance: 400 + }) + + t.ok(Array.isArray(nearby)) + t.equal(nearby.length, 2) + + assertIsJungfernheide(t, nearby[0]) + t.ok(nearby[0].distance >= 0) + t.ok(nearby[0].distance <= 100) + + t.end() +}) + +test('locations named Jungfernheide', async (t) => { + const locations = await client.locations('Jungfernheide', { + results: 10 + }) + + t.ok(Array.isArray(locations)) + t.ok(locations.length > 0) + t.ok(locations.length <= 10) + + for (let location of locations) assertValidLocation(t, location) + t.ok(locations.find(isJungfernheide)) + + t.end() +}) diff --git a/test/index.js b/test/index.js new file mode 100644 index 00000000..30db69dc --- /dev/null +++ b/test/index.js @@ -0,0 +1,3 @@ +'use strict' + +require('./db') diff --git a/test/util.js b/test/util.js new file mode 100644 index 00000000..6a7a7621 --- /dev/null +++ b/test/util.js @@ -0,0 +1,131 @@ +'use strict' + +const isRoughlyEqual = require('is-roughly-equal') +const getStations = require('db-stations').full +const floor = require('floordate') + +const findStation = (id) => new Promise((yay, nay) => { + const stations = getStations() + stations + .once('error', nay) + .on('data', (s) => { + if ( + s.id === id || + (s.additionalIds && s.additionalIds.includes(id)) + ) { + yay(s) + stations.destroy() + } + }) + .once('end', yay) +}) + +const assertValidStation = (t, s) => { + t.equal(s.type, 'station') + t.equal(typeof s.id, 'string') +} + +const assertValidPoi = (t, p) => { + t.equal(p.type, 'poi') + t.equal(typeof p.id, 'string') +} + +const assertValidAddress = (t, a) => { + t.equal(a.type, 'address') +} + +const assertValidLocation = (t, l) => { + t.equal(typeof l.type, 'string') + if (l.type === 'station') assertValidStation(t, l) + else if (l.type === 'poi') assertValidPoi(t, l) + else if (l.type === 'address') assertValidAddress(t, l) + else t.fail('invalid type ' + l.type) + + t.equal(typeof s.name, 'string') + t.ok(s.coordinates) + t.equal(typeof s.coordinates.latitude, 'number') + t.equal(typeof s.coordinates.longitude, 'number') +} + +const isValidMode = (m) => { + return m === 'walking' || + m === 'train' || + m === 'bus' || + m === 'ferry' +} + +const assertValidLine = (t, l) => { + t.equal(l.type, 'line') + t.equal(typeof l.name, 'string') + t.ok(isValidMode(l.mode)) + t.equal(typeof l.product, 'string') +} + +const isValidDateTime = (w) => { + return !Number.isNaN(+new Date(w)) +} + +const assertValidStopover = (t, s) => { + if ('arrival' in s) t.ok(isValidDateTime(s.arrival)) + if ('departure' in s) t.ok(isValidDateTime(s.departure)) + if (!('arrival' in s) && !('departure' in s)) { + t.fail('stopover doesn\'t contain arrival or departure') + } + t.ok(s.station) + assertValidStation(t, s.station) +} + +const isJungfernheide = (s) => { + return s.type === 'station' && + s.id === '8011167' && + s.name === 'Berlin Jungfernheide' && + s.coordinates && + isRoughlyEqual(s.coordinates.latitude, 52.530408, .0005) && + isRoughlyEqual(s.coordinates.longitude, 13.299424, .0005) +} + +const assertIsJungfernheide = (t, s) => { + t.equal(s.type, 'station') + t.equal(s.id, '8011167') + t.equal(s.name, 'Berlin Jungfernheide') + t.ok(s.coordinates) + t.ok(isRoughlyEqual(s.coordinates.latitude, 52.530408, .0005)) + t.ok(isRoughlyEqual(s.coordinates.longitude, 13.299424, .0005)) +} + +const assertIsMünchenHbf = (s) => { + t.equal(s.type, 'station') + t.equal(s.id, '8000261') + t.equal(s.name, 'München Hbf') + t.ok(s.coordinates) + t.equal(s.coordinates.latitude, 48.140229) + t.equal(s.coordinates.longitude, 11.558339) +} + +const minute = 60 * 1000 +const hour = 60 * minute +const day = 24 * hour +const week = 7 * day + +// next Monday +const when = new Date(+floor(new Date(), 'week') + week + 10 * hour) +const isValidWhen = (w) => { + const ts = +new Date(w) + if (Number.isNaN(ts)) return false + return isRoughlyEqual(10 * hour, +when, ts) +} + +module.exports = { + findStation, + assertValidStation, + assertValidPoi, + assertValidAddress, + assertValidLocation, + isValidMode, + assertValidLine, + isValidDateTime, + assertValidStopover, + isJungfernheide, assertIsJungfernheide, + assertIsMünchenHbf, + when, isValidWhen +} From 47b5ef7ed59adabffeaa2c888145154e8cb08ec7 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 12 Nov 2017 21:23:29 +0100 Subject: [PATCH 16/73] bugfixes :bug: --- parse/line.js | 3 ++- test/db.js | 21 +++++++++++++++++++-- test/util.js | 39 +++++---------------------------------- 3 files changed, 26 insertions(+), 37 deletions(-) diff --git a/parse/line.js b/parse/line.js index b9eb5e52..6df7c6a5 100644 --- a/parse/line.js +++ b/parse/line.js @@ -3,7 +3,6 @@ // todo: what is p.number vs p.line? // todo: what is p.icoX? // todo: what is p.oprX? -// todo: is passing in profile necessary? const parseLine = (profile, p) => { if (!p) return null // todo: handle this upstream const res = {type: 'line', name: p.line || p.name} @@ -14,6 +13,8 @@ const parseLine = (profile, p) => { res.productName = p.prodCtx.catOutS } + // todo: parse mode, remove from profiles + return res } diff --git a/test/db.js b/test/db.js index 63bbd389..9841817d 100644 --- a/test/db.js +++ b/test/db.js @@ -13,10 +13,27 @@ const { assertValidLocation, assertValidLine, assertValidStopover, - isJungfernheide, assertIsJungfernheide, when, isValidWhen } = require('./util.js') +const isJungfernheide = (s) => { + return s.type === 'station' && + (s.id === '008011167' || s.id === '8011167') && + s.name === 'Berlin Jungfernheide' && + s.coordinates && + isRoughlyEqual(s.coordinates.latitude, 52.530408, .0005) && + isRoughlyEqual(s.coordinates.longitude, 13.299424, .0005) +} + +const assertIsJungfernheide = (t, s) => { + t.equal(s.type, 'station') + t.ok(s.id === '008011167' || s.id === '8011167', 'id should be 8011167') + t.equal(s.name, 'Berlin Jungfernheide') + t.ok(s.coordinates) + t.ok(isRoughlyEqual(s.coordinates.latitude, 52.530408, .0005)) + t.ok(isRoughlyEqual(s.coordinates.longitude, 13.299424, .0005)) +} + const test = tapePromise(tape) const client = createClient(dbProfile) @@ -163,7 +180,7 @@ test('locations named Jungfernheide', async (t) => { t.ok(locations.length <= 10) for (let location of locations) assertValidLocation(t, location) - t.ok(locations.find(isJungfernheide)) + t.ok(locations.some(isJungfernheide)) t.end() }) diff --git a/test/util.js b/test/util.js index 6a7a7621..e5d590fe 100644 --- a/test/util.js +++ b/test/util.js @@ -41,10 +41,10 @@ const assertValidLocation = (t, l) => { else if (l.type === 'address') assertValidAddress(t, l) else t.fail('invalid type ' + l.type) - t.equal(typeof s.name, 'string') - t.ok(s.coordinates) - t.equal(typeof s.coordinates.latitude, 'number') - t.equal(typeof s.coordinates.longitude, 'number') + t.equal(typeof l.name, 'string') + t.ok(l.coordinates) + t.equal(typeof l.coordinates.latitude, 'number') + t.equal(typeof l.coordinates.longitude, 'number') } const isValidMode = (m) => { @@ -57,7 +57,7 @@ const isValidMode = (m) => { const assertValidLine = (t, l) => { t.equal(l.type, 'line') t.equal(typeof l.name, 'string') - t.ok(isValidMode(l.mode)) + t.ok(isValidMode(l.mode), 'invalid mode ' + l.mode) t.equal(typeof l.product, 'string') } @@ -75,33 +75,6 @@ const assertValidStopover = (t, s) => { assertValidStation(t, s.station) } -const isJungfernheide = (s) => { - return s.type === 'station' && - s.id === '8011167' && - s.name === 'Berlin Jungfernheide' && - s.coordinates && - isRoughlyEqual(s.coordinates.latitude, 52.530408, .0005) && - isRoughlyEqual(s.coordinates.longitude, 13.299424, .0005) -} - -const assertIsJungfernheide = (t, s) => { - t.equal(s.type, 'station') - t.equal(s.id, '8011167') - t.equal(s.name, 'Berlin Jungfernheide') - t.ok(s.coordinates) - t.ok(isRoughlyEqual(s.coordinates.latitude, 52.530408, .0005)) - t.ok(isRoughlyEqual(s.coordinates.longitude, 13.299424, .0005)) -} - -const assertIsMünchenHbf = (s) => { - t.equal(s.type, 'station') - t.equal(s.id, '8000261') - t.equal(s.name, 'München Hbf') - t.ok(s.coordinates) - t.equal(s.coordinates.latitude, 48.140229) - t.equal(s.coordinates.longitude, 11.558339) -} - const minute = 60 * 1000 const hour = 60 * minute const day = 24 * hour @@ -125,7 +98,5 @@ module.exports = { assertValidLine, isValidDateTime, assertValidStopover, - isJungfernheide, assertIsJungfernheide, - assertIsMünchenHbf, when, isValidWhen } From 4356032308ac1551de612cd540e4ede847c4113d Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 12 Nov 2017 23:51:39 +0100 Subject: [PATCH 17/73] DB profile mostly copied from db-hafas --- p/db/index.js | 109 ++++++++++++++++++++++++++++++++++++++++ p/db/loyalty-cards.js | 30 +++++++++++ p/db/modes.js | 113 ++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +- test/db.js | 1 + 5 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 p/db/index.js create mode 100644 p/db/loyalty-cards.js create mode 100644 p/db/modes.js diff --git a/p/db/index.js b/p/db/index.js new file mode 100644 index 00000000..7c897f20 --- /dev/null +++ b/p/db/index.js @@ -0,0 +1,109 @@ +'use strict' + +const crypto = require('crypto') + +const _formatStation = require('../../format/station') +const _parseLine = require('../../parse/line') +const {accessibility, bike} = require('../../format/filters') + +const modes = require('./modes') +const formatLoyaltyCard = require('./loyalty-cards').format + +const transformReqBody = (body) => { + body.client = {id: 'DB', v: '16040000', type: 'IPH', name: 'DB Navigator'} + body.ext = 'DB.R15.12.a' + body.ver = '1.15' + body.auth = {type: 'AID', aid: 'n91dB8Z77MLdoR0K'} + + 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.accessibility && accessibility[opt.accessibility]) { + filters.push(accessibility[opt.accessibility]) + } + if (opt.bike) filters.push(bike) + + query.trfReq = { + jnyCl: 2, // todo + tvlrProf: [{ + type: 'E', + redtnCard: opt.loyaltyCard + ? formatLoyaltyCard(opt.loyaltyCard) + : null + }], + cType: 'PK' + } + + return query +} + +const parseLine = (profile, l) => { + const res = _parseLine(profile, l) + + res.mode = res.product = null + if ('class' in res) { + const data = modes.bitmasks[parseInt(res.class)] + if (data) { + res.mode = data.mode + res.product = data.product + } + } + + return res +} + +const isIBNR = /^\d{6,}$/ +const formatStation = (id) => { + if (!isIBNR.test(id)) throw new Error('station ID must be an IBNR.') + return _formatStation(id) +} + +const defaultProducts = { + suburban: true, + subway: true, + tram: true, + bus: true, + ferry: true, + national: true, + nationalExp: true, + regional: true, + regionalExp: true +} + +// todo: find option for absolute number of results + +const dbProfile = { + timezone: 'Europe/Berlin', + endpoint: 'https://reiseauskunft.bahn.de/bin/mgate.exe', + transformReqBody, + transformReq, + transformJourneysQuery, + + // todo: parseLocation + parseLine, + + formatStation, + formatProducts: (products) => { + products = Object.assign(Object.create(null), defaultProducts, products) + return { + type: 'PROD', + mode: 'INC', + value: modes.stringifyBitmask(products) + '' + } + } +} + +module.exports = dbProfile diff --git a/p/db/loyalty-cards.js b/p/db/loyalty-cards.js new file mode 100644 index 00000000..dfb583e9 --- /dev/null +++ b/p/db/loyalty-cards.js @@ -0,0 +1,30 @@ +'use strict' + +const c = { + NONE: Symbol('no loyaly card'), + BAHNCARD: Symbol('Bahncard'), + VORTEILSCARD: Symbol('VorteilsCard'), + HALBTAXABO: Symbol('HalbtaxAbo'), + VOORDEELURENABO: Symbol('Voordeelurenabo'), + SHCARD: Symbol('SH-Card'), + GENERALABONNEMENT: Symbol('General-Abonnement') +} + +// see https://gist.github.com/juliuste/202bb04f450a79f8fa12a2ec3abcd72d +const formatLoyaltyCard = (data) => { + if (data.type === c.BAHNCARD) { + if (data.discount === 25) return c.class === 1 ? 1 : 2 + if (data.discount === 50) return c.class === 1 ? 3 : 4 + } + if (data.type === c.VORTEILSCARD) return 9 + if (data.type === c.HALBTAXABO) return data.railplus ? 10 : 11 + if (data.type === c.VOORDEELURENABO) return data.railplus ? 12 : 13 + if (data.type === c.SHCARD) return 14 + if (data.type === c.GENERALABONNEMENT) return 15 + return 0 +} + +module.exports = { + data: c, + format: formatLoyaltyCard +} diff --git a/p/db/modes.js b/p/db/modes.js new file mode 100644 index 00000000..3b0a6a94 --- /dev/null +++ b/p/db/modes.js @@ -0,0 +1,113 @@ +'use strict' + +const m = { + nationalExp: { + bitmask: 1, + name: 'InterCityExpress', + short: 'ICE', + mode: 'train', + product: 'nationalExp' + }, + national: { + bitmask: 2, + name: 'InterCity & EuroCity', + short: 'IC/EC', + mode: 'train', + product: 'national' + }, + regionalExp: { + bitmask: 4, + name: 'InterRegio', + short: 'IR', + mode: 'train', + product: 'regionalExp' + }, + regional: { + bitmask: 8, + name: 'RegionalExpress & Regio', + short: 'RE/RB', + mode: 'train', + product: 'regional' + }, + suburban: { + bitmask: 16, + name: 'S-Bahn', + short: 'S', + mode: 'train', + product: 'suburban' + }, + bus: { + bitmask: 32, + name: 'Bus', + short: 'B', + mode: 'bus', + product: 'bus' + }, + ferry: { + bitmask: 64, + name: 'Ferry', + short: 'F', + mode: 'ferry', + product: 'ferry' + }, + subway: { + bitmask: 128, + name: 'U-Bahn', + short: 'U', + mode: 'train', + product: 'subway' + }, + tram: { + bitmask: 256, + name: 'Tram', + short: 'T', + mode: 'tram', + product: 'tram' + }, + taxi: { + bitmask: 512, + name: 'Group Taxi', + short: 'Taxi', + mode: null, // todo + product: 'taxi' + }, + unknown: { + bitmask: 0, + name: 'unknown', + short: '?', + product: 'unknown' + } +} + +m.bitmasks = [] +m.bitmasks[1] = m.nationalExp +m.bitmasks[2] = m.national +m.bitmasks[4] = m.regionalExp +m.bitmasks[8] = m.regional +m.bitmasks[16] = m.suburban +m.bitmasks[32] = m.bus +m.bitmasks[64] = m.ferry +m.bitmasks[128] = m.subway +m.bitmasks[256] = m.tram +m.bitmasks[512] = m.taxi + +// todo: move up +m.stringifyBitmask = (products) => { + let bitmask = 0 + for (let product in products) { + if (products[product] === true) bitmask += m[product].bitmask + } + return bitmask +} + +// todo: move up +m.parseBitmask = (bitmask) => { + let products = {}, i = 1 + do { + products[m.bitmasks[i].product] = !!(bitmask & i) + i *= 2 + } while (m.bitmasks[i] && m.bitmasks[i].product) + return products +} + +module.exports = m diff --git a/package.json b/package.json index 7ff792c0..a896a12a 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "index.js", "lib", "parse", - "format" + "format", + "p" ], "author": "Jannis R ", "homepage": "https://github.com/derhuerst/hafas-client", diff --git a/test/db.js b/test/db.js index 9841817d..0bb8fcf7 100644 --- a/test/db.js +++ b/test/db.js @@ -5,6 +5,7 @@ const tape = require('tape') const isRoughlyEqual = require('is-roughly-equal') const createClient = require('..') +const dbProfile = require('../p/db') const { findStation, assertValidStation, From 6b1f22cc65a7fffd6f7bd3553933b3353e2c7037 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 13 Nov 2017 00:30:14 +0100 Subject: [PATCH 18/73] parse product bitmasks --- p/db/index.js | 18 ++++++++++-------- parse/location.js | 2 +- test/db.js | 16 ++++++++++++++++ test/util.js | 1 + 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/p/db/index.js b/p/db/index.js index 7c897f20..6cf75c8a 100644 --- a/p/db/index.js +++ b/p/db/index.js @@ -82,6 +82,14 @@ const defaultProducts = { regional: true, regionalExp: true } +const formatProducts = (products) => { + products = Object.assign(Object.create(null), defaultProducts, products) + return { + type: 'PROD', + mode: 'INC', + value: modes.stringifyBitmask(products) + '' + } +} // todo: find option for absolute number of results @@ -94,16 +102,10 @@ const dbProfile = { // todo: parseLocation parseLine, + parseProducts: modes.parseBitmask, formatStation, - formatProducts: (products) => { - products = Object.assign(Object.create(null), defaultProducts, products) - return { - type: 'PROD', - mode: 'INC', - value: modes.stringifyBitmask(products) + '' - } - } + formatProducts } module.exports = dbProfile diff --git a/parse/location.js b/parse/location.js index 70d59f81..f692af91 100644 --- a/parse/location.js +++ b/parse/location.js @@ -19,7 +19,7 @@ const parseLocation = (profile, l) => { } if (type === 'poi' || type === 'station') res.id = l.extId - if ('pCls' in l) res.products = l.pCls + if ('pCls' in l) res.products = profile.parseProducts(l.pCls) return res } diff --git a/test/db.js b/test/db.js index 0bb8fcf7..3f238959 100644 --- a/test/db.js +++ b/test/db.js @@ -6,6 +6,7 @@ const isRoughlyEqual = require('is-roughly-equal') const createClient = require('..') const dbProfile = require('../p/db') +const modes = require('../p/db/modes') const { findStation, assertValidStation, @@ -35,6 +36,12 @@ const assertIsJungfernheide = (t, s) => { t.ok(isRoughlyEqual(s.coordinates.longitude, 13.299424, .0005)) } +const assertValidProducts = (t, p) => { + for (let k of Object.keys(modes)) { + t.ok('boolean', typeof modes[k], 'mode ' + k + ' must be a boolean') + } +} + const test = tapePromise(tape) const client = createClient(dbProfile) @@ -50,12 +57,18 @@ test('Berlin Jungfernheide to München Hbf', async (t) => { if (!await findStation(journey.origin.id)) { console.error('unknown station', journey.origin.id, journey.origin.name) } + if (journey.origin.products) { + assertValidProducts(t, journey.origin.products) + } t.ok(isValidWhen(journey.departure)) assertValidStation(t, journey.destination) if (!await findStation(journey.origin.id)) { console.error('unknown station', journey.destination.id, journey.destination.name) } + if (journey.destination.products) { + assertValidProducts(t, journey.destination.products) + } t.ok(isValidWhen(journey.arrival)) t.ok(Array.isArray(journey.parts)) @@ -100,6 +113,7 @@ test('Berlin Jungfernheide to Torfstraße 17', async (t) => { if (!await findStation(part.origin.id)) { console.error('unknown station', part.origin.id, part.origin.name) } + if (part.origin.products) assertValidProducts(t, part.origin.products) t.ok(isValidWhen(part.departure)) t.ok(isValidWhen(part.arrival)) @@ -127,6 +141,7 @@ test('Berlin Jungfernheide to ATZE Musiktheater', async (t) => { if (!await findStation(part.origin.id)) { console.error('unknown station', part.origin.id, part.origin.name) } + if (part.origin.products) assertValidProducts(t, part.origin.products) t.ok(isValidWhen(part.departure)) t.ok(isValidWhen(part.arrival)) @@ -150,6 +165,7 @@ test('departures at Berlin Jungfernheide', async (t) => { if (!await findStation(dep.station.id)) { console.error('unknown station', dep.station.id, dep.station.name) } + if (dep.station.products) assertValidProducts(t, dep.station.products) t.ok(isValidWhen(dep.when)) } diff --git a/test/util.js b/test/util.js index e5d590fe..85a0871b 100644 --- a/test/util.js +++ b/test/util.js @@ -57,6 +57,7 @@ const isValidMode = (m) => { const assertValidLine = (t, l) => { t.equal(l.type, 'line') t.equal(typeof l.name, 'string') + if (!isValidMode(l.mode)) console.error(l) t.ok(isValidMode(l.mode), 'invalid mode ' + l.mode) t.equal(typeof l.product, 'string') } From 2e66e647c9499e0d709a61ecaf7aa4bcf3f07f2b Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 13 Nov 2017 00:38:09 +0100 Subject: [PATCH 19/73] readme, example :memo: --- example.js | 16 +++++++ readme.md | 134 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 example.js diff --git a/example.js b/example.js new file mode 100644 index 00000000..57d13694 --- /dev/null +++ b/example.js @@ -0,0 +1,16 @@ +'use strict' + +const createClient = require('.') +const dbProfile = require('./p/db') + +const client = createClient(dbProfile) + +// Berlin Jungfernheide to München Hbf +client.journeys('8011167', '8000261', {results: 1}) +// client.departures('8011167', {duration: 1}) +// client.locations('Berlin Jungfernheide') +// client.locations('ATZE Musiktheater', {poi: true, addressses: false, fuzzy: false}) +// client.nearby(52.4751309, 13.3656537, {results: 1}) +.then((data) => { + console.log(require('util').inspect(data, {depth: null})) +}, console.error) diff --git a/readme.md b/readme.md index a9ac4058..98e2e808 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,8 @@ # hafas-client -**A client for HAFAS public transport APIs**, providing the base for [vbb-hafas](https://github.com/derhuerst/vbb-hafas) and [db-hafas](https://github.com/derhuerst/db-hafas). +**A client for HAFAS public transport APIs**. Sort of like [public-transport-enable](https://github.com/schildbach/public-transport-enabler), but with a smaller scope. It also has customisations for the following transport networks: + +- [Deutsche Bahn](https://en.wikipedia.org/wiki/Deutsche_Bahn) – [src](p/db/index.js) [![npm version](https://img.shields.io/npm/v/hafas-client.svg)](https://www.npmjs.com/package/hafas-client) [![build status](https://img.shields.io/travis/derhuerst/hafas-client.svg)](https://travis-ci.org/derhuerst/hafas-client) @@ -8,6 +10,13 @@ [![chat on gitter](https://badges.gitter.im/derhuerst.svg)](https://gitter.im/derhuerst) +## Background + +There's [a company called HaCon](http://hacon.de) that sells [a public transport management system called HAFAS](https://de.wikipedia.org/wiki/HAFAS). It is [used by companies all over Europe](https://gist.github.com/derhuerst/2b7ed83bfa5f115125a5) to server routing and departure information for apps. All those endpoints are similar, with the same terms and API routes, but have slightly different options, filters and enable features. + +`hafas-client` contains all logic for communicating with these, as well as serialising from and parsing to [FPTF](https://github.com/public-transport/friendly-public-transport-format). Endpoint-specific customisations increase the quality of the returned data. + + ## Installing ```shell @@ -18,7 +27,128 @@ npm install hafas-client ## Usage ``` -todo +const createClient = require('hafas-client') +const dbProfile = require('hafas-client/p/db') + +const client = createClient(dbProfile) + +// Berlin Jungfernheide to München Hbf +client.journeys('8011167', '8000261', {results: 1}) +.then(console.log) +.catch(console.error) +``` + +```js +[ { + origin: { + type: 'station', + id: '8089100', + name: 'Berlin Jungfernheide (S)', + coordinates: { + latitude: 52.530291, + longitude: 13.299451 + }, + products: { /* … */ } + }, + departure: '2017-11-13T01:00:00Z', + + destination: { + type: 'station', + id: '8000261', + name: 'München Hbf', + coordinates: { + latitude: 48.140364, + longitude: 11.558735 + }, + products: { /* … */ } + }, + arrival: '2017-11-13T09:39:00Z', + + parts: [ { + id: '1|1436339|0|80|12112017', + + origin: { + type: 'station', + id: '8089100', + name: 'Berlin Jungfernheide (S)', + coordinates: { + latitude: 52.530291, + longitude: 13.299451 + }, + products: { + nationalExp: false, + national: false, + regionalExp: false, + regional: true, + suburban: true, + bus: true, + ferry: false, + subway: true, + tram: false, + taxi: false + } + }, + departure: '2017-11-13T00:50:00Z', + departurePlatform: '6', + + destination: { + type: 'station', + id: '8089047', + name: 'Berlin Westkreuz', + coordinates: { + latitude: 52.500752, + longitude: 13.283854 + }, + products: { + nationalExp: false, + national: false, + regionalExp: false, + regional: true, + suburban: true, + bus: true, + ferry: false, + subway: false, + tram: false, + taxi: false + } + }, + arrival: '2017-11-13T00:57:00Z', + delay: 0, + + line: { + type: 'line', + name: 'S 42', + mode: 'train', + product: 'suburban', + class: 16, + productCode: 4, + productName: 's' + }, + direction: 'Ringbahn <-', + arrivalPlatform: '12' + }, { + id: '1|332491|0|80|12112017', + + origin: { /* … */ }, + departure: '2017-11-13T01:05:00Z', + departurePlatform: '3', + + destination: { /* … */ }, + arrival: '2017-11-13T01:18:00Z', + delay: 0, + + line: { /* … */ }, + direction: 'Berlin Ostbahnhof' + }, { + origin: { /* … */ }, + departure: '2017-11-13T01:18:00Z', + destination: { /* … */ }, + arrival: '2017-11-13T01:26:00Z', + mode: 'walking' + }, { + /* … */ + } ] +} ] ``` From c9739cf27dc37483c0c91c63c8cce7760d5ff560 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Tue, 14 Nov 2017 02:50:24 +0100 Subject: [PATCH 20/73] copy tests from vbb-hafas :white_check_mark: --- package.json | 4 +- test/db.js | 18 ++- test/index.js | 1 + test/util.js | 18 --- test/vbb.js | 329 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 350 insertions(+), 20 deletions(-) create mode 100644 test/vbb.js diff --git a/package.json b/package.json index a896a12a..f699a671 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,9 @@ "is-roughly-equal": "^0.1.0", "tap-spec": "^4.1.1", "tape": "^4.8.0", - "tape-promise": "^2.0.1" + "tape-promise": "^2.0.1", + "vbb-parse-line": "^0.2.5", + "vbb-stations-autocomplete": "^2.9.0" }, "scripts": { "test": "node test/index.js | tap-spec", diff --git a/test/db.js b/test/db.js index 3f238959..03ca7ef8 100644 --- a/test/db.js +++ b/test/db.js @@ -1,5 +1,6 @@ 'use strict' +const getStations = require('db-stations').full const tapePromise = require('tape-promise').default const tape = require('tape') const isRoughlyEqual = require('is-roughly-equal') @@ -8,7 +9,6 @@ const createClient = require('..') const dbProfile = require('../p/db') const modes = require('../p/db/modes') const { - findStation, assertValidStation, assertValidPoi, assertValidAddress, @@ -18,6 +18,22 @@ const { when, isValidWhen } = require('./util.js') +const findStation = (id) => new Promise((yay, nay) => { + const stations = getStations() + stations + .once('error', nay) + .on('data', (s) => { + if ( + s.id === id || + (s.additionalIds && s.additionalIds.includes(id)) + ) { + yay(s) + stations.destroy() + } + }) + .once('end', yay) +}) + const isJungfernheide = (s) => { return s.type === 'station' && (s.id === '008011167' || s.id === '8011167') && diff --git a/test/index.js b/test/index.js index 30db69dc..85c39e86 100644 --- a/test/index.js +++ b/test/index.js @@ -1,3 +1,4 @@ 'use strict' require('./db') +require('./vbb') diff --git a/test/util.js b/test/util.js index 85a0871b..99864715 100644 --- a/test/util.js +++ b/test/util.js @@ -1,25 +1,8 @@ 'use strict' const isRoughlyEqual = require('is-roughly-equal') -const getStations = require('db-stations').full const floor = require('floordate') -const findStation = (id) => new Promise((yay, nay) => { - const stations = getStations() - stations - .once('error', nay) - .on('data', (s) => { - if ( - s.id === id || - (s.additionalIds && s.additionalIds.includes(id)) - ) { - yay(s) - stations.destroy() - } - }) - .once('end', yay) -}) - const assertValidStation = (t, s) => { t.equal(s.type, 'station') t.equal(typeof s.id, 'string') @@ -90,7 +73,6 @@ const isValidWhen = (w) => { } module.exports = { - findStation, assertValidStation, assertValidPoi, assertValidAddress, diff --git a/test/vbb.js b/test/vbb.js new file mode 100644 index 00000000..d686d2c9 --- /dev/null +++ b/test/vbb.js @@ -0,0 +1,329 @@ +'use strict' + +const test = require('tape') +const a = require('assert') +const isRoughlyEqual = require('is-roughly-equal') +const stations = require('vbb-stations-autocomplete') +const floor = require('floordate') + +const createClient = require('..') +const vbbProfile = require('../p/vbb') +const modes = require('../p/vbb/modes') +const { + assertValidStation, assertValidFrameStation, + assertValidPoi, + assertValidAddress, + assertValidLocation, + assertValidLine, + assertValidPassed, + hour, when, + assertValidWhen +} = require('./util') + +const findStation = (query) => stations(query, true, false) + +const client = createClient(vbbProfile) + +test('journeys – station to station', (t) => { + // U Spichernstr. to U Amrumer Str. + client.journeys('900000042101', '900000009101', { + results: 3, when, passedStations: true + }) + .then((journeys) => { + t.ok(Array.isArray(journeys)) + t.strictEqual(journeys.length, 3) + + for (let journey of journeys) { + assertValidStation(t, journey.origin) + t.ok(journey.origin.name.indexOf('(Berlin)') === -1) + t.strictEqual(journey.origin.id, '900000042101') + assertValidWhen(t, journey.departure) + + assertValidStation(t, journey.destination) + t.strictEqual(journey.destination.id, '900000009101') + assertValidWhen(t, journey.arrival) + + t.ok(Array.isArray(journey.parts)) + t.strictEqual(journey.parts.length, 1) + const part = journey.parts[0] + + t.equal(typeof part.id, 'string') + t.ok(part.id) + assertValidStation(t, part.origin) + t.ok(part.origin.name.indexOf('(Berlin)') === -1) + t.strictEqual(part.origin.id, '900000042101') + assertValidWhen(t, part.departure) + + assertValidStation(t, part.destination) + t.strictEqual(part.destination.id, '900000009101') + assertValidWhen(t, part.arrival) + + assertValidLine(t, part.line) + t.ok(findStation(part.direction)) + t.ok(part.direction.indexOf('(Berlin)') === -1) + + t.ok(Array.isArray(part.passed)) + for (let passed of part.passed) assertValidPassed(t, passed) + } + }) + .catch(t.ifError) + .then(() => t.end()) +}) + +test('journeys – only subway', (t) => { + // U Spichernstr. to U Bismarckstr. + client.journeys('900000042101', '900000024201', { + results: 20, when, + products: { + suburban: false, + subway: true, + tram: false, + bus: false, + ferry: false, + express: false, + regional: false + } + }) + .then((journeys) => { + t.ok(Array.isArray(journeys)) + t.ok(journeys.length > 1) + + for (let journey of journeys) { + for (let part of journey.parts) { + if (part.line) { + t.equal(part.line.mode, 'train') + t.equal(part.line.product, 'subway') + t.equal(part.line.public, true) + } + } + } + }) + .catch(t.ifError) + .then(() => t.end()) +}) + +test('journeys – fails with no product', (t) => { + // U Spichernstr. to U Bismarckstr. + client.journeys('900000042101', '900000024201', { + when, + products: { + suburban: false, + subway: false, + tram: false, + bus: false, + ferry: false, + express: false, + regional: false + } + }) + .catch((err) => { + t.ok(err, 'error thrown') + t.end() + }) +}) + +test('journey part details', (t) => { + // U Spichernstr. to U Amrumer Str. + client.journeys('900000042101', '900000009101', {results: 1, when}) + .then((journeys) => { + const part = journeys[0].parts[0] + t.ok(part.id, 'precondition failed') + t.ok(part.line.name, 'precondition failed') + return client.journeyPart(part.id, part.line.name, {when}) + }) + .then((part) => { + t.equal(typeof part.id, 'string') + t.ok(part.id) + + assertValidLine(t, part.line) + + t.equal(typeof part.direction, 'string') + t.ok(part.direction) + + t.ok(Array.isArray(part.passed)) + for (let passed of part.passed) assertValidPassed(t, passed) + }) + .catch(t.ifError) + .then(() => t.end()) +}) + + + +test('journeys – station to address', (t) => { + // U Spichernstr. to Torfstraße 17 + client.journeys('900000042101', { + type: 'address', name: 'Torfstraße 17', + latitude: 52.5416823, longitude: 13.3491223 + }, {results: 1, when}) + .then((journeys) => { + t.ok(Array.isArray(journeys)) + t.strictEqual(journeys.length, 1) + const journey = journeys[0] + const part = journey.parts[journey.parts.length - 1] + + assertValidStation(t, part.origin) + assertValidWhen(t, part.departure) + + const dest = part.destination + assertValidAddress(t, dest) + t.strictEqual(dest.name, 'Torfstr. 17') + t.ok(isRoughlyEqual(.0001, dest.coordinates.latitude, 52.5416823)) + t.ok(isRoughlyEqual(.0001, dest.coordinates.longitude, 13.3491223)) + assertValidWhen(t, part.arrival) + }) + .catch(t.ifError) + .then(() => t.end()) +}) + + + +test('journeys – station to POI', (t) => { + // U Spichernstr. to ATZE Musiktheater + client.journeys('900000042101', { + type: 'poi', name: 'ATZE Musiktheater', id: 9980720, + latitude: 52.543333, longitude: 13.351686 + }, {results: 1, when}) + .then((journeys) => { + t.ok(Array.isArray(journeys)) + t.strictEqual(journeys.length, 1) + const journey = journeys[0] + const part = journey.parts[journey.parts.length - 1] + + assertValidStation(t, part.origin) + assertValidWhen(t, part.departure) + + const dest = part.destination + assertValidPoi(t, dest) + t.strictEqual(dest.name, 'ATZE Musiktheater') + t.ok(isRoughlyEqual(.0001, dest.coordinates.latitude, 52.543333)) + t.ok(isRoughlyEqual(.0001, dest.coordinates.longitude, 13.351686)) + assertValidWhen(t, part.arrival) + }) + .catch(t.ifError) + .then(() => t.end()) +}) + + + +test('departures', (t) => { + client.departures('900000042101', {duration: 5, when}) // U Spichernstr. + .then((deps) => { + t.ok(Array.isArray(deps)) + t.deepEqual(deps, deps.sort((a, b) => t.when > b.when)) + for (let dep of deps) { + t.equal(typeof dep.ref, 'string') + t.ok(dep.ref) + + t.equal(dep.station.name, 'U Spichernstr.') + assertValidStation(t, dep.station) + t.strictEqual(dep.station.id, '900000042101') + + assertValidWhen(t, dep.when) + t.ok(findStation(dep.direction)) + assertValidLine(t, dep.line) + } + }) + .catch(t.ifError) + .then(() => t.end()) +}) + +test('departures at 7-digit station', (t) => { + const eisenach = '8010097' // see derhuerst/vbb-hafas#22 + client.departures(eisenach, {when}) + .then(() => { + t.pass('did not fail') + t.end() + }) + .catch(t.ifError) +}) + + + +test('nearby', (t) => { + client.nearby(52.4873452,13.3310411, {distance: 200}) // Berliner Str./Bundesallee + .then((nearby) => { + t.ok(Array.isArray(nearby)) + for (let n of nearby) assertValidLocation(t, n, false) + + t.equal(nearby[0].id, '900000044201') + t.equal(nearby[0].name, 'U Berliner Str.') + t.ok(nearby[0].distance > 0) + t.ok(nearby[0].distance < 100) + + t.equal(nearby[1].id, '900000043252') + t.equal(nearby[1].name, 'Landhausstr.') + t.ok(nearby[1].distance > 100) + t.ok(nearby[1].distance < 200) + }) + .catch(t.ifError) + .then(() => t.end()) +}) + + + +test('locations', (t) => { + client.locations('Alexanderplatz', {results: 10}) + .then((locations) => { + t.ok(Array.isArray(locations)) + t.ok(locations.length > 0) + t.ok(locations.length <= 10) + for (let l of locations) assertValidLocation(t, l) + t.ok(locations.find((s) => s.type === 'station')) + t.ok(locations.find((s) => s.type === 'poi')) + t.ok(locations.find((s) => s.type === 'address')) + }) + .catch(t.ifError) + .then(() => t.end()) +}) + + + +test('radar', (t) => { + client.radar(52.52411, 13.41002, 52.51942, 13.41709, {duration: 5 * 60, when}) + .then((vehicles) => { + t.ok(Array.isArray(vehicles)) + t.ok(vehicles.length > 0) + for (let v of vehicles) { + + t.ok(findStation(v.direction)) + assertValidLine(t, v.line) + + t.equal(typeof v.coordinates.latitude, 'number') + t.ok(v.coordinates.latitude <= 55, 'vehicle is too far away') + t.ok(v.coordinates.latitude >= 45, 'vehicle is too far away') + t.equal(typeof v.coordinates.longitude, 'number') + t.ok(v.coordinates.longitude >= 9, 'vehicle is too far away') + t.ok(v.coordinates.longitude <= 15, 'vehicle is too far away') + + t.ok(Array.isArray(v.nextStops)) + for (let s of v.nextStops) { + assertValidFrameStation(t, s.station) + if (!s.arrival && !s.departure) + t.ifError(new Error('neither arrival nor departure return')) + if (s.arrival) { + t.equal(typeof s.arrival, 'string') + const arr = +new Date(s.arrival) + t.ok(!Number.isNaN(arr)) + // note that this can be an ICE train + t.ok(isRoughlyEqual(14 * hour, +when, arr)) + } + if (s.departure) { + t.equal(typeof s.departure, 'string') + const dep = +new Date(s.departure) + t.ok(!Number.isNaN(dep)) + // note that this can be an ICE train + t.ok(isRoughlyEqual(14 * hour, +when, dep)) + } + } + + t.ok(Array.isArray(v.frames)) + for (let f of v.frames) { + assertValidFrameStation(t, f.origin) + assertValidFrameStation(t, f.destination) + t.equal(typeof f.t, 'number') + } + } + }) + .catch(t.ifError) + .then(() => t.end()) +}) From 7cf126134709fcebec533e215f73862e1eb1393a Mon Sep 17 00:00:00 2001 From: Jannis R Date: Tue, 14 Nov 2017 02:59:17 +0100 Subject: [PATCH 21/73] vbb tests: code style :shirt: --- test/vbb.js | 427 ++++++++++++++++++++++++++-------------------------- 1 file changed, 210 insertions(+), 217 deletions(-) diff --git a/test/vbb.js b/test/vbb.js index d686d2c9..976b7d0b 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -1,10 +1,10 @@ 'use strict' -const test = require('tape') const a = require('assert') const isRoughlyEqual = require('is-roughly-equal') const stations = require('vbb-stations-autocomplete') -const floor = require('floordate') +const tapePromise = require('tape-promise').default +const tape = require('tape') const createClient = require('..') const vbbProfile = require('../p/vbb') @@ -20,59 +20,59 @@ const { assertValidWhen } = require('./util') +// todo const findStation = (query) => stations(query, true, false) +const test = tapePromise(tape) const client = createClient(vbbProfile) -test('journeys – station to station', (t) => { +test('journeys – station to station', async (t) => { // U Spichernstr. to U Amrumer Str. - client.journeys('900000042101', '900000009101', { + const journeys = await client.journeys('900000042101', '900000009101', { results: 3, when, passedStations: true }) - .then((journeys) => { - t.ok(Array.isArray(journeys)) - t.strictEqual(journeys.length, 3) - for (let journey of journeys) { - assertValidStation(t, journey.origin) - t.ok(journey.origin.name.indexOf('(Berlin)') === -1) - t.strictEqual(journey.origin.id, '900000042101') - assertValidWhen(t, journey.departure) + t.ok(Array.isArray(journeys)) + t.strictEqual(journeys.length, 3) - assertValidStation(t, journey.destination) - t.strictEqual(journey.destination.id, '900000009101') - assertValidWhen(t, journey.arrival) + for (let journey of journeys) { + assertValidStation(t, journey.origin) + t.ok(journey.origin.name.indexOf('(Berlin)') === -1) + t.strictEqual(journey.origin.id, '900000042101') + assertValidWhen(t, journey.departure) - t.ok(Array.isArray(journey.parts)) - t.strictEqual(journey.parts.length, 1) - const part = journey.parts[0] + assertValidStation(t, journey.destination) + t.strictEqual(journey.destination.id, '900000009101') + assertValidWhen(t, journey.arrival) - t.equal(typeof part.id, 'string') - t.ok(part.id) - assertValidStation(t, part.origin) - t.ok(part.origin.name.indexOf('(Berlin)') === -1) - t.strictEqual(part.origin.id, '900000042101') - assertValidWhen(t, part.departure) + t.ok(Array.isArray(journey.parts)) + t.strictEqual(journey.parts.length, 1) + const part = journey.parts[0] - assertValidStation(t, part.destination) - t.strictEqual(part.destination.id, '900000009101') - assertValidWhen(t, part.arrival) + t.equal(typeof part.id, 'string') + t.ok(part.id) + assertValidStation(t, part.origin) + t.ok(part.origin.name.indexOf('(Berlin)') === -1) + t.strictEqual(part.origin.id, '900000042101') + assertValidWhen(t, part.departure) - assertValidLine(t, part.line) - t.ok(findStation(part.direction)) - t.ok(part.direction.indexOf('(Berlin)') === -1) + assertValidStation(t, part.destination) + t.strictEqual(part.destination.id, '900000009101') + assertValidWhen(t, part.arrival) - t.ok(Array.isArray(part.passed)) - for (let passed of part.passed) assertValidPassed(t, passed) - } - }) - .catch(t.ifError) - .then(() => t.end()) + assertValidLine(t, part.line) + t.ok(findStation(part.direction)) + t.ok(part.direction.indexOf('(Berlin)') === -1) + + t.ok(Array.isArray(part.passed)) + for (let passed of part.passed) assertValidPassed(t, passed) + } + t.end() }) -test('journeys – only subway', (t) => { +test('journeys – only subway', async (t) => { // U Spichernstr. to U Bismarckstr. - client.journeys('900000042101', '900000024201', { + const journeys = await client.journeys('900000042101', '900000024201', { results: 20, when, products: { suburban: false, @@ -84,246 +84,239 @@ test('journeys – only subway', (t) => { regional: false } }) - .then((journeys) => { - t.ok(Array.isArray(journeys)) - t.ok(journeys.length > 1) - for (let journey of journeys) { - for (let part of journey.parts) { - if (part.line) { - t.equal(part.line.mode, 'train') - t.equal(part.line.product, 'subway') - t.equal(part.line.public, true) - } + t.ok(Array.isArray(journeys)) + t.ok(journeys.length > 1) + + for (let journey of journeys) { + for (let part of journey.parts) { + if (part.line) { + t.equal(part.line.mode, 'train') + t.equal(part.line.product, 'subway') + t.equal(part.line.public, true) } } - }) - .catch(t.ifError) - .then(() => t.end()) + } + t.end() }) -test('journeys – fails with no product', (t) => { - // U Spichernstr. to U Bismarckstr. - client.journeys('900000042101', '900000024201', { - when, - products: { - suburban: false, - subway: false, - tram: false, - bus: false, - ferry: false, - express: false, - regional: false - } - }) - .catch((err) => { +test('journeys – fails with no product', async (t) => { + try { + // U Spichernstr. to U Bismarckstr. + await client.journeys('900000042101', '900000024201', { + when, + products: { + suburban: false, + subway: false, + tram: false, + bus: false, + ferry: false, + express: false, + regional: false + } + }) + } catch (err) { t.ok(err, 'error thrown') t.end() - }) + } }) -test('journey part details', (t) => { +test('journey part details', async (t) => { // U Spichernstr. to U Amrumer Str. - client.journeys('900000042101', '900000009101', {results: 1, when}) - .then((journeys) => { - const part = journeys[0].parts[0] - t.ok(part.id, 'precondition failed') - t.ok(part.line.name, 'precondition failed') - return client.journeyPart(part.id, part.line.name, {when}) + const journeys = await client.journeys('900000042101', '900000009101', { + results: 1, when }) - .then((part) => { - t.equal(typeof part.id, 'string') - t.ok(part.id) - assertValidLine(t, part.line) + const p = journeys[0].parts[0] + t.ok(p.id, 'precondition failed') + t.ok(p.line.name, 'precondition failed') + const part = await client.journeyPart(p.id, p.line.name, {when}) - t.equal(typeof part.direction, 'string') - t.ok(part.direction) + t.equal(typeof part.id, 'string') + t.ok(part.id) - t.ok(Array.isArray(part.passed)) - for (let passed of part.passed) assertValidPassed(t, passed) - }) - .catch(t.ifError) - .then(() => t.end()) + assertValidLine(t, part.line) + + t.equal(typeof part.direction, 'string') + t.ok(part.direction) + + t.ok(Array.isArray(part.passed)) + for (let passed of part.passed) assertValidPassed(t, passed) + + t.end() }) -test('journeys – station to address', (t) => { +test('journeys – station to address', async (t) => { // U Spichernstr. to Torfstraße 17 - client.journeys('900000042101', { + const journeys = await client.journeys('900000042101', { type: 'address', name: 'Torfstraße 17', latitude: 52.5416823, longitude: 13.3491223 }, {results: 1, when}) - .then((journeys) => { - t.ok(Array.isArray(journeys)) - t.strictEqual(journeys.length, 1) - const journey = journeys[0] - const part = journey.parts[journey.parts.length - 1] - assertValidStation(t, part.origin) - assertValidWhen(t, part.departure) + t.ok(Array.isArray(journeys)) + t.strictEqual(journeys.length, 1) + const journey = journeys[0] + const part = journey.parts[journey.parts.length - 1] - const dest = part.destination - assertValidAddress(t, dest) - t.strictEqual(dest.name, 'Torfstr. 17') - t.ok(isRoughlyEqual(.0001, dest.coordinates.latitude, 52.5416823)) - t.ok(isRoughlyEqual(.0001, dest.coordinates.longitude, 13.3491223)) - assertValidWhen(t, part.arrival) - }) - .catch(t.ifError) - .then(() => t.end()) + assertValidStation(t, part.origin) + assertValidWhen(t, part.departure) + + const dest = part.destination + assertValidAddress(t, dest) + t.strictEqual(dest.name, 'Torfstr. 17') + t.ok(isRoughlyEqual(.0001, dest.coordinates.latitude, 52.5416823)) + t.ok(isRoughlyEqual(.0001, dest.coordinates.longitude, 13.3491223)) + assertValidWhen(t, part.arrival) + + t.end() }) -test('journeys – station to POI', (t) => { +test('journeys – station to POI', async (t) => { // U Spichernstr. to ATZE Musiktheater - client.journeys('900000042101', { + const journeys = await client.journeys('900000042101', { type: 'poi', name: 'ATZE Musiktheater', id: 9980720, latitude: 52.543333, longitude: 13.351686 }, {results: 1, when}) - .then((journeys) => { - t.ok(Array.isArray(journeys)) - t.strictEqual(journeys.length, 1) - const journey = journeys[0] - const part = journey.parts[journey.parts.length - 1] - assertValidStation(t, part.origin) - assertValidWhen(t, part.departure) + t.ok(Array.isArray(journeys)) + t.strictEqual(journeys.length, 1) + const journey = journeys[0] + const part = journey.parts[journey.parts.length - 1] - const dest = part.destination - assertValidPoi(t, dest) - t.strictEqual(dest.name, 'ATZE Musiktheater') - t.ok(isRoughlyEqual(.0001, dest.coordinates.latitude, 52.543333)) - t.ok(isRoughlyEqual(.0001, dest.coordinates.longitude, 13.351686)) - assertValidWhen(t, part.arrival) - }) - .catch(t.ifError) - .then(() => t.end()) + assertValidStation(t, part.origin) + assertValidWhen(t, part.departure) + + const dest = part.destination + assertValidPoi(t, dest) + t.strictEqual(dest.name, 'ATZE Musiktheater') + t.ok(isRoughlyEqual(.0001, dest.coordinates.latitude, 52.543333)) + t.ok(isRoughlyEqual(.0001, dest.coordinates.longitude, 13.351686)) + assertValidWhen(t, part.arrival) + + t.end() }) -test('departures', (t) => { - client.departures('900000042101', {duration: 5, when}) // U Spichernstr. - .then((deps) => { - t.ok(Array.isArray(deps)) - t.deepEqual(deps, deps.sort((a, b) => t.when > b.when)) - for (let dep of deps) { - t.equal(typeof dep.ref, 'string') - t.ok(dep.ref) +test('departures', async (t) => { + // U Spichernstr. + const deps = await client.departures('900000042101', {duration: 5, when}) - t.equal(dep.station.name, 'U Spichernstr.') - assertValidStation(t, dep.station) - t.strictEqual(dep.station.id, '900000042101') + t.ok(Array.isArray(deps)) + t.deepEqual(deps, deps.sort((a, b) => t.when > b.when)) + for (let dep of deps) { + t.equal(typeof dep.ref, 'string') + t.ok(dep.ref) - assertValidWhen(t, dep.when) - t.ok(findStation(dep.direction)) - assertValidLine(t, dep.line) - } - }) - .catch(t.ifError) - .then(() => t.end()) + t.equal(dep.station.name, 'U Spichernstr.') + assertValidStation(t, dep.station) + t.strictEqual(dep.station.id, '900000042101') + + assertValidWhen(t, dep.when) + t.ok(findStation(dep.direction)) + assertValidLine(t, dep.line) + } + t.end() }) -test('departures at 7-digit station', (t) => { +test('departures at 7-digit station', async (t) => { const eisenach = '8010097' // see derhuerst/vbb-hafas#22 - client.departures(eisenach, {when}) - .then(() => { - t.pass('did not fail') - t.end() - }) - .catch(t.ifError) + await client.departures(eisenach, {when}) + t.pass('did not fail') + + t.end() }) -test('nearby', (t) => { - client.nearby(52.4873452,13.3310411, {distance: 200}) // Berliner Str./Bundesallee - .then((nearby) => { - t.ok(Array.isArray(nearby)) - for (let n of nearby) assertValidLocation(t, n, false) +test('nearby', async (t) => { + // Berliner Str./Bundesallee + const nearby = await client.nearby(52.4873452,13.3310411, {distance: 200}) - t.equal(nearby[0].id, '900000044201') - t.equal(nearby[0].name, 'U Berliner Str.') - t.ok(nearby[0].distance > 0) - t.ok(nearby[0].distance < 100) + t.ok(Array.isArray(nearby)) + for (let n of nearby) assertValidLocation(t, n, false) - t.equal(nearby[1].id, '900000043252') - t.equal(nearby[1].name, 'Landhausstr.') - t.ok(nearby[1].distance > 100) - t.ok(nearby[1].distance < 200) - }) - .catch(t.ifError) - .then(() => t.end()) + t.equal(nearby[0].id, '900000044201') + t.equal(nearby[0].name, 'U Berliner Str.') + t.ok(nearby[0].distance > 0) + t.ok(nearby[0].distance < 100) + + t.equal(nearby[1].id, '900000043252') + t.equal(nearby[1].name, 'Landhausstr.') + t.ok(nearby[1].distance > 100) + t.ok(nearby[1].distance < 200) + + t.end() }) -test('locations', (t) => { - client.locations('Alexanderplatz', {results: 10}) - .then((locations) => { - t.ok(Array.isArray(locations)) - t.ok(locations.length > 0) - t.ok(locations.length <= 10) - for (let l of locations) assertValidLocation(t, l) - t.ok(locations.find((s) => s.type === 'station')) - t.ok(locations.find((s) => s.type === 'poi')) - t.ok(locations.find((s) => s.type === 'address')) - }) - .catch(t.ifError) - .then(() => t.end()) +test('locations', async (t) => { + const locations = await client.locations('Alexanderplatz', {results: 10}) + + t.ok(Array.isArray(locations)) + t.ok(locations.length > 0) + t.ok(locations.length <= 10) + for (let l of locations) assertValidLocation(t, l) + t.ok(locations.find((s) => s.type === 'station')) + t.ok(locations.find((s) => s.type === 'poi')) + t.ok(locations.find((s) => s.type === 'address')) + + t.end() }) -test('radar', (t) => { - client.radar(52.52411, 13.41002, 52.51942, 13.41709, {duration: 5 * 60, when}) - .then((vehicles) => { - t.ok(Array.isArray(vehicles)) - t.ok(vehicles.length > 0) - for (let v of vehicles) { +test('radar', async (t) => { + const vehicles = await client.radar(52.52411, 13.41002, 52.51942, 13.41709, { + duration: 5 * 60, when + }) - t.ok(findStation(v.direction)) - assertValidLine(t, v.line) + t.ok(Array.isArray(vehicles)) + t.ok(vehicles.length > 0) + for (let v of vehicles) { - t.equal(typeof v.coordinates.latitude, 'number') - t.ok(v.coordinates.latitude <= 55, 'vehicle is too far away') - t.ok(v.coordinates.latitude >= 45, 'vehicle is too far away') - t.equal(typeof v.coordinates.longitude, 'number') - t.ok(v.coordinates.longitude >= 9, 'vehicle is too far away') - t.ok(v.coordinates.longitude <= 15, 'vehicle is too far away') + t.ok(findStation(v.direction)) + assertValidLine(t, v.line) - t.ok(Array.isArray(v.nextStops)) - for (let s of v.nextStops) { - assertValidFrameStation(t, s.station) - if (!s.arrival && !s.departure) - t.ifError(new Error('neither arrival nor departure return')) - if (s.arrival) { - t.equal(typeof s.arrival, 'string') - const arr = +new Date(s.arrival) - t.ok(!Number.isNaN(arr)) - // note that this can be an ICE train - t.ok(isRoughlyEqual(14 * hour, +when, arr)) - } - if (s.departure) { - t.equal(typeof s.departure, 'string') - const dep = +new Date(s.departure) - t.ok(!Number.isNaN(dep)) - // note that this can be an ICE train - t.ok(isRoughlyEqual(14 * hour, +when, dep)) - } + t.equal(typeof v.coordinates.latitude, 'number') + t.ok(v.coordinates.latitude <= 55, 'vehicle is too far away') + t.ok(v.coordinates.latitude >= 45, 'vehicle is too far away') + t.equal(typeof v.coordinates.longitude, 'number') + t.ok(v.coordinates.longitude >= 9, 'vehicle is too far away') + t.ok(v.coordinates.longitude <= 15, 'vehicle is too far away') + + t.ok(Array.isArray(v.nextStops)) + for (let s of v.nextStops) { + assertValidFrameStation(t, s.station) + if (!s.arrival && !s.departure) + t.ifError(new Error('neither arrival nor departure return')) + if (s.arrival) { + t.equal(typeof s.arrival, 'string') + const arr = +new Date(s.arrival) + t.ok(!Number.isNaN(arr)) + // note that this can be an ICE train + t.ok(isRoughlyEqual(14 * hour, +when, arr)) } - - t.ok(Array.isArray(v.frames)) - for (let f of v.frames) { - assertValidFrameStation(t, f.origin) - assertValidFrameStation(t, f.destination) - t.equal(typeof f.t, 'number') + if (s.departure) { + t.equal(typeof s.departure, 'string') + const dep = +new Date(s.departure) + t.ok(!Number.isNaN(dep)) + // note that this can be an ICE train + t.ok(isRoughlyEqual(14 * hour, +when, dep)) } } - }) - .catch(t.ifError) - .then(() => t.end()) + + t.ok(Array.isArray(v.frames)) + for (let f of v.frames) { + assertValidFrameStation(t, f.origin) + assertValidFrameStation(t, f.destination) + t.equal(typeof f.t, 'number') + } + } + t.end() }) From ba9b7e84189e69c9697bc1a6a7229f3e5ea23fcd Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sat, 18 Nov 2017 10:14:17 +0100 Subject: [PATCH 22/73] minor stuff --- index.js | 1 + readme.md | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index d190e036..bc9fba6d 100644 --- a/index.js +++ b/index.js @@ -37,6 +37,7 @@ const createClient = (profile) => { if (!Array.isArray(d.jnyL)) return [] // todo: throw err? const parse = profile.parseDeparture(profile, d.locations, d.lines, d.remarks) return d.jnyL.map(parse) + .sort((a, b) => new Date(a.when) - new Date(b.when)) }) } diff --git a/readme.md b/readme.md index 98e2e808..de67f1d4 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ # hafas-client -**A client for HAFAS public transport APIs**. Sort of like [public-transport-enable](https://github.com/schildbach/public-transport-enabler), but with a smaller scope. It also has customisations for the following transport networks: +**A client for HAFAS public transport APIs**. Sort of like [public-transport-enable](https://github.com/schildbach/public-transport-enabler), but with a smaller scope. It also contains customisations for the following transport networks: - [Deutsche Bahn](https://en.wikipedia.org/wiki/Deutsche_Bahn) – [src](p/db/index.js) @@ -12,7 +12,7 @@ ## Background -There's [a company called HaCon](http://hacon.de) that sells [a public transport management system called HAFAS](https://de.wikipedia.org/wiki/HAFAS). It is [used by companies all over Europe](https://gist.github.com/derhuerst/2b7ed83bfa5f115125a5) to server routing and departure information for apps. All those endpoints are similar, with the same terms and API routes, but have slightly different options, filters and enable features. +There's [a company called HaCon](http://hacon.de) that sells [a public transport management system called HAFAS](https://de.wikipedia.org/wiki/HAFAS). It is [used by companies all over Europe](https://gist.github.com/derhuerst/2b7ed83bfa5f115125a5) to serve routing and departure information for apps. All those endpoints are similar, with the same terms and API routes, but have slightly different options, filters and enable features. `hafas-client` contains all logic for communicating with these, as well as serialising from and parsing to [FPTF](https://github.com/public-transport/friendly-public-transport-format). Endpoint-specific customisations increase the quality of the returned data. @@ -26,7 +26,7 @@ npm install hafas-client ## Usage -``` +```js const createClient = require('hafas-client') const dbProfile = require('hafas-client/p/db') From 6d4b20ecc1284f98750c4b791d9bf1caae130dca Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sat, 18 Nov 2017 17:53:12 +0100 Subject: [PATCH 23/73] VBB profile --- p/vbb/example.js | 16 ++++++ p/vbb/index.js | 78 +++++++++++++++++++++++++ p/vbb/modes.js | 146 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +- readme.md | 1 + test/vbb.js | 2 +- 6 files changed, 244 insertions(+), 2 deletions(-) create mode 100644 p/vbb/example.js create mode 100644 p/vbb/index.js create mode 100644 p/vbb/modes.js diff --git a/p/vbb/example.js b/p/vbb/example.js new file mode 100644 index 00000000..a347a4fc --- /dev/null +++ b/p/vbb/example.js @@ -0,0 +1,16 @@ +'use strict' + +const createClient = require('../..') +const vbbProfile = require('.') + +const client = createClient(vbbProfile) + +client.journeys('900000003201', '900000024101', {results: 1}) +// client.departures('900000013102', {duration: 1}) +// client.locations('Alexanderplatz', {results: 2}) +// client.nearby(52.5137344, 13.4744798, {distance: 60}) +// client.radar(52.52411, 13.41002, 52.51942, 13.41709, {results: 10}) +.then((data) => { + console.log(data) + // console.log(require('util').inspect(data, {depth: null})) +}, console.error) diff --git a/p/vbb/index.js b/p/vbb/index.js new file mode 100644 index 00000000..9bbfe0bc --- /dev/null +++ b/p/vbb/index.js @@ -0,0 +1,78 @@ +'use strict' + +const shorten = require('vbb-short-station-name') + +const _formatStation = require('../../format/station') +const _parseLine = require('../../parse/line') +const _parseLocation = require('../../parse/location') + +const modes = require('./modes') + +const transformReqBody = (body) => { + body.client = {type: 'IPA', id: 'BVG'} + body.ext = 'VBB.2' + body.ver = '1.11' + body.auth = {type: 'AID', aid: 'hafas-vbb-apps'} + + return body +} + +const parseLine = (profile, l) => { + const res = _parseLine(profile, l) + + res.mode = res.product = null + if ('class' in res) { + const data = modes.bitmasks[parseInt(res.class)] + if (data) { + res.mode = data.mode + res.product = data.product + } + } + + return res +} + +const parseLocation = (profile, l) => { + const res = _parseLocation(profile, l) + res.name = shorten(res.name) + return res +} + +const isIBNR = /^\d{9,}$/ +const formatStation = (id) => { + // todo: convert short to long IDs + if (!isIBNR.test(id)) throw new Error('station ID must be an IBNR.') + return _formatStation(id) +} + +const defaultProducts = { + suburban: true, + subway: true, + tram: true, + bus: true, + ferry: true, + express: true, + regional: true +} +const formatProducts = (products) => { + products = Object.assign(Object.create(null), defaultProducts, products) + return { + type: 'PROD', + mode: 'INC', + value: modes.stringifyBitmask(products) + '' + } +} + +const vbbProfile = { + timezone: 'Europe/Berlin', + endpoint: 'https://fahrinfo.vbb.de/bin/mgate.exe', + transformReqBody, + + parseLocation, + parseLine, + parseProducts: modes.parseBitmask, + + formatProducts +} + +module.exports = vbbProfile diff --git a/p/vbb/modes.js b/p/vbb/modes.js new file mode 100644 index 00000000..fbbb49f9 --- /dev/null +++ b/p/vbb/modes.js @@ -0,0 +1,146 @@ +'use strict' + +// todo: remove useless keys +const m = { + suburban: { + category: 0, + bitmask: 1, + name: 'S-Bahn', + mode: 'train', + short: 'S', + type: 'suburban', + color: '#008c4f', + unicode: '🚈', + ansi: ['green'] // `chalk` color code + }, + + subway: { + category: 1, + bitmask: 2, + name: 'U-Bahn', + mode: 'train', + short: 'U', + type: 'subway', + color: '#0067ac', + unicode: '🚇', + ansi: ['blue'] // `chalk` color code + }, + + tram: { + category: 2, + bitmask: 4, + name: 'Tram', + mode: 'train', + short: 'T', + type: 'tram', + color: '#e3001b', + unicode: '🚋', + ansi: ['red'] // `chalk` color code + }, + + bus: { + category: 3, + bitmask: 8, + name: 'Bus', + mode: 'train', + short: 'B', + type: 'bus', + color: '#922A7D', + unicode: '🚌', + ansi: ['dim', 'magenta'] // `chalk` color codes + }, + + ferry: { + category: 4, + bitmask: 16, + name: 'Fähre', + mode: 'train', + short: 'F', + type: 'ferry', + color: '#099bd6', + unicode: '🚢', + ansi: ['cyan'] // `chalk` color code + }, + + express: { + category: 5, + bitmask: 32, + name: 'IC/ICE', + mode: 'train', + short: 'E', + type: 'express', + color: '#f4e613', + unicode: '🚄', + ansi: ['yellow'] // `chalk` color code + }, + + regional: { + category: 6, + bitmask: 64, + name: 'RB/RE', + mode: 'train', + short: 'R', + type: 'regional', + color: '#D9222A', + unicode: '🚆', + ansi: ['red'] // `chalk` color code + }, + + unknown: { + category: null, + bitmask: 0, + name: 'unknown', + mode: null, + short: '?', + type: 'unknown', + color: '#555555', + unicode: '?', + ansi: ['gray'] // `chalk` color code + } +} + +m.bitmasks = [] +m.bitmasks[1] = m.suburban +m.bitmasks[2] = m.subway +m.bitmasks[4] = m.tram +m.bitmasks[8] = m.bus +m.bitmasks[16] = m.ferry +m.bitmasks[32] = m.express +m.bitmasks[64] = m.regional + + +m.categories = [ + m.suburban, + m.subway, + m.tram, + m.bus, + m.ferry, + m.express, + m.regional, + m.unknown +] + +// m.parseCategory = (category) => { +// return m.categories[parseInt(category)] || m.unknown +// } + +// todo: move up +m.stringifyBitmask = (types) => { + let bitmask = 0 + for (let type in types) { + if (types[type] === true) bitmask += m[type].bitmask + } + return bitmask +} + +// todo: move up +m.parseBitmask = (bitmask) => { + let types = {}, i = 1 + do { + types[m.bitmasks[i].type] = !!(bitmask & i) + i *= 2 + } while (m.bitmasks[i] && m.bitmasks[i].type) + return types +} + +module.exports = m diff --git a/package.json b/package.json index f699a671..c94c295a 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "moment-timezone": "^0.5.13", "pinkie-promise": "^2.0.1", "query-string": "^5.0.0", - "slugg": "^1.2.0" + "slugg": "^1.2.0", + "vbb-short-station-name": "^0.4.0" }, "devDependencies": { "db-stations": "^1.25.0", diff --git a/readme.md b/readme.md index de67f1d4..9807b0d3 100644 --- a/readme.md +++ b/readme.md @@ -3,6 +3,7 @@ **A client for HAFAS public transport APIs**. Sort of like [public-transport-enable](https://github.com/schildbach/public-transport-enabler), but with a smaller scope. It also contains customisations for the following transport networks: - [Deutsche Bahn](https://en.wikipedia.org/wiki/Deutsche_Bahn) – [src](p/db/index.js) +- [Berlin public transport](https://en.wikipedia.org/wiki/Verkehrsverbund_Berlin-Brandenburg) – [src](p/vbb/index.js) [![npm version](https://img.shields.io/npm/v/hafas-client.svg)](https://www.npmjs.com/package/hafas-client) [![build status](https://img.shields.io/travis/derhuerst/hafas-client.svg)](https://travis-ci.org/derhuerst/hafas-client) diff --git a/test/vbb.js b/test/vbb.js index 976b7d0b..f5f1c9e8 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -271,7 +271,7 @@ test('locations', async (t) => { -test('radar', async (t) => { +test.skip('radar', async (t) => { const vehicles = await client.radar(52.52411, 13.41002, 52.51942, 13.41709, { duration: 5 * 60, when }) From 541505e720b9e4cfb3a4250879f5c32486b5e54a Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 20 Nov 2017 01:05:48 +0100 Subject: [PATCH 24/73] VBB: 9<->12 digit station IDs --- p/vbb/index.js | 9 +++++++-- package.json | 3 ++- test/vbb.js | 39 ++++++++++++++++++--------------------- 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/p/vbb/index.js b/p/vbb/index.js index 9bbfe0bc..dc528c5b 100644 --- a/p/vbb/index.js +++ b/p/vbb/index.js @@ -1,6 +1,7 @@ 'use strict' const shorten = require('vbb-short-station-name') +const {to12Digit, to9Digit} = require('vbb-translate-ids') const _formatStation = require('../../format/station') const _parseLine = require('../../parse/line') @@ -34,14 +35,17 @@ const parseLine = (profile, l) => { const parseLocation = (profile, l) => { const res = _parseLocation(profile, l) - res.name = shorten(res.name) + if (res.type === 'station') { + res.id = to12Digit(res.id) + res.name = shorten(res.name) + } return res } const isIBNR = /^\d{9,}$/ const formatStation = (id) => { - // todo: convert short to long IDs if (!isIBNR.test(id)) throw new Error('station ID must be an IBNR.') + id = to9Digit(id) return _formatStation(id) } @@ -72,6 +76,7 @@ const vbbProfile = { parseLine, parseProducts: modes.parseBitmask, + formatStation, formatProducts } diff --git a/package.json b/package.json index c94c295a..ba054276 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "pinkie-promise": "^2.0.1", "query-string": "^5.0.0", "slugg": "^1.2.0", - "vbb-short-station-name": "^0.4.0" + "vbb-short-station-name": "^0.4.0", + "vbb-translate-ids": "^3.1.0" }, "devDependencies": { "db-stations": "^1.25.0", diff --git a/test/vbb.js b/test/vbb.js index f5f1c9e8..fd4cd809 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -26,9 +26,12 @@ const findStation = (query) => stations(query, true, false) const test = tapePromise(tape) const client = createClient(vbbProfile) +const amrumerStr = '900000009101' +const spichernstr = '900000042101' +const bismarckstr = '900000024201' + test('journeys – station to station', async (t) => { - // U Spichernstr. to U Amrumer Str. - const journeys = await client.journeys('900000042101', '900000009101', { + const journeys = await client.journeys(spichernstr, amrumerStr, { results: 3, when, passedStations: true }) @@ -38,11 +41,11 @@ test('journeys – station to station', async (t) => { for (let journey of journeys) { assertValidStation(t, journey.origin) t.ok(journey.origin.name.indexOf('(Berlin)') === -1) - t.strictEqual(journey.origin.id, '900000042101') + t.strictEqual(journey.origin.id, spichernstr) assertValidWhen(t, journey.departure) assertValidStation(t, journey.destination) - t.strictEqual(journey.destination.id, '900000009101') + t.strictEqual(journey.destination.id, amrumerStr) assertValidWhen(t, journey.arrival) t.ok(Array.isArray(journey.parts)) @@ -53,11 +56,11 @@ test('journeys – station to station', async (t) => { t.ok(part.id) assertValidStation(t, part.origin) t.ok(part.origin.name.indexOf('(Berlin)') === -1) - t.strictEqual(part.origin.id, '900000042101') + t.strictEqual(part.origin.id, spichernstr) assertValidWhen(t, part.departure) assertValidStation(t, part.destination) - t.strictEqual(part.destination.id, '900000009101') + t.strictEqual(part.destination.id, amrumerStr) assertValidWhen(t, part.arrival) assertValidLine(t, part.line) @@ -71,8 +74,7 @@ test('journeys – station to station', async (t) => { }) test('journeys – only subway', async (t) => { - // U Spichernstr. to U Bismarckstr. - const journeys = await client.journeys('900000042101', '900000024201', { + const journeys = await client.journeys(spichernstr, bismarckstr, { results: 20, when, products: { suburban: false, @@ -102,8 +104,7 @@ test('journeys – only subway', async (t) => { test('journeys – fails with no product', async (t) => { try { - // U Spichernstr. to U Bismarckstr. - await client.journeys('900000042101', '900000024201', { + await client.journeys(spichernstr, bismarckstr, { when, products: { suburban: false, @@ -122,8 +123,7 @@ test('journeys – fails with no product', async (t) => { }) test('journey part details', async (t) => { - // U Spichernstr. to U Amrumer Str. - const journeys = await client.journeys('900000042101', '900000009101', { + const journeys = await client.journeys(spichernstr, amrumerStr, { results: 1, when }) @@ -149,8 +149,7 @@ test('journey part details', async (t) => { test('journeys – station to address', async (t) => { - // U Spichernstr. to Torfstraße 17 - const journeys = await client.journeys('900000042101', { + const journeys = await client.journeys(spichernstr, { type: 'address', name: 'Torfstraße 17', latitude: 52.5416823, longitude: 13.3491223 }, {results: 1, when}) @@ -176,8 +175,7 @@ test('journeys – station to address', async (t) => { test('journeys – station to POI', async (t) => { - // U Spichernstr. to ATZE Musiktheater - const journeys = await client.journeys('900000042101', { + const journeys = await client.journeys(spichernstr, { type: 'poi', name: 'ATZE Musiktheater', id: 9980720, latitude: 52.543333, longitude: 13.351686 }, {results: 1, when}) @@ -202,9 +200,8 @@ test('journeys – station to POI', async (t) => { -test('departures', async (t) => { - // U Spichernstr. - const deps = await client.departures('900000042101', {duration: 5, when}) +test.only('departures', async (t) => { + const deps = await client.departures(spichernstr, {duration: 5, when}) t.ok(Array.isArray(deps)) t.deepEqual(deps, deps.sort((a, b) => t.when > b.when)) @@ -214,7 +211,7 @@ test('departures', async (t) => { t.equal(dep.station.name, 'U Spichernstr.') assertValidStation(t, dep.station) - t.strictEqual(dep.station.id, '900000042101') + t.strictEqual(dep.station.id, spichernstr) assertValidWhen(t, dep.when) t.ok(findStation(dep.direction)) @@ -235,7 +232,7 @@ test('departures at 7-digit station', async (t) => { test('nearby', async (t) => { // Berliner Str./Bundesallee - const nearby = await client.nearby(52.4873452,13.3310411, {distance: 200}) + const nearby = await client.nearby(52.4873452, 13.3310411, {distance: 200}) t.ok(Array.isArray(nearby)) for (let n of nearby) assertValidLocation(t, n, false) From e762f30f05f4af5d18f9c2d32ea9d687434abc7e Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 20 Nov 2017 15:08:30 +0100 Subject: [PATCH 25/73] VBB-related bugfixes and changes :bug: --- lib/default-profile.js | 1 + p/vbb/index.js | 9 +++++++-- p/vbb/modes.js | 1 - parse/journey.js | 2 +- parse/line.js | 6 +++++- test/util.js | 7 ++++++- test/vbb.js | 17 ++++++++++------- 7 files changed, 30 insertions(+), 13 deletions(-) diff --git a/lib/default-profile.js b/lib/default-profile.js index 5b1681b8..5de45fd7 100644 --- a/lib/default-profile.js +++ b/lib/default-profile.js @@ -34,6 +34,7 @@ const defaultProfile = { parseDeparture, parseJourney, parseLine, + parseStationName: id, parseLocation, parseMovement, parseNearby, diff --git a/p/vbb/index.js b/p/vbb/index.js index dc528c5b..973d531a 100644 --- a/p/vbb/index.js +++ b/p/vbb/index.js @@ -26,7 +26,7 @@ const parseLine = (profile, l) => { const data = modes.bitmasks[parseInt(res.class)] if (data) { res.mode = data.mode - res.product = data.product + res.product = data.type } } @@ -35,9 +35,13 @@ const parseLine = (profile, l) => { const parseLocation = (profile, l) => { const res = _parseLocation(profile, l) + + // todo: shorten has been made for stations, not any type of location + res.name = shorten(res.name) + if (res.type === 'station') { res.id = to12Digit(res.id) - res.name = shorten(res.name) + // todo: https://github.com/derhuerst/vbb-hafas/blob/1c64bfe42422e2648b21016d233c808460250308/lib/parse.js#L67-L75 } return res } @@ -72,6 +76,7 @@ const vbbProfile = { endpoint: 'https://fahrinfo.vbb.de/bin/mgate.exe', transformReqBody, + parseStationName: shorten, parseLocation, parseLine, parseProducts: modes.parseBitmask, diff --git a/p/vbb/modes.js b/p/vbb/modes.js index fbbb49f9..9b78cfd9 100644 --- a/p/vbb/modes.js +++ b/p/vbb/modes.js @@ -108,7 +108,6 @@ m.bitmasks[16] = m.ferry m.bitmasks[32] = m.express m.bitmasks[64] = m.regional - m.categories = [ m.suburban, m.subway, diff --git a/parse/journey.js b/parse/journey.js index cb8831e8..f622c151 100644 --- a/parse/journey.js +++ b/parse/journey.js @@ -51,7 +51,7 @@ const createParseJourney = (profile, stations, lines, remarks) => { } else if (pt.type === 'JNY') { res.id = pt.jny.jid res.line = lines[parseInt(pt.jny.prodX)] || null - res.direction = pt.jny.dirTxt // todo: parse this + res.direction = profile.parseStationName(pt.jny.dirTxt) if (pt.dep.dPlatfS) res.departurePlatform = pt.dep.dPlatfS if (pt.arr.aPlatfS) res.arrivalPlatform = pt.arr.aPlatfS diff --git a/parse/line.js b/parse/line.js index 6df7c6a5..01e7954d 100644 --- a/parse/line.js +++ b/parse/line.js @@ -5,7 +5,11 @@ // todo: what is p.oprX? const parseLine = (profile, p) => { if (!p) return null // todo: handle this upstream - const res = {type: 'line', name: p.line || p.name} + const res = { + type: 'line', + name: p.line || p.name, + public: true + } if (p.cls) res.class = p.cls if (p.prodCtx) { diff --git a/test/util.js b/test/util.js index 99864715..36805d1e 100644 --- a/test/util.js +++ b/test/util.js @@ -43,6 +43,7 @@ const assertValidLine = (t, l) => { if (!isValidMode(l.mode)) console.error(l) t.ok(isValidMode(l.mode), 'invalid mode ' + l.mode) t.equal(typeof l.product, 'string') + t.equal(l.public, true) } const isValidDateTime = (w) => { @@ -72,6 +73,10 @@ const isValidWhen = (w) => { return isRoughlyEqual(10 * hour, +when, ts) } +const assertValidWhen = (t, w) => { + t.ok(isValidWhen(w), 'invalid when') +} + module.exports = { assertValidStation, assertValidPoi, @@ -81,5 +86,5 @@ module.exports = { assertValidLine, isValidDateTime, assertValidStopover, - when, isValidWhen + when, isValidWhen, assertValidWhen } diff --git a/test/vbb.js b/test/vbb.js index fd4cd809..c13c0119 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -15,7 +15,7 @@ const { assertValidAddress, assertValidLocation, assertValidLine, - assertValidPassed, + assertValidStopover, hour, when, assertValidWhen } = require('./util') @@ -68,7 +68,7 @@ test('journeys – station to station', async (t) => { t.ok(part.direction.indexOf('(Berlin)') === -1) t.ok(Array.isArray(part.passed)) - for (let passed of part.passed) assertValidPassed(t, passed) + for (let passed of part.passed) assertValidStopover(t, passed) } t.end() }) @@ -93,9 +93,9 @@ test('journeys – only subway', async (t) => { for (let journey of journeys) { for (let part of journey.parts) { if (part.line) { + assertValidLine(t, part.line) t.equal(part.line.mode, 'train') t.equal(part.line.product, 'subway') - t.equal(part.line.public, true) } } } @@ -122,7 +122,8 @@ test('journeys – fails with no product', async (t) => { } }) -test('journey part details', async (t) => { +// todo +test.skip('journey part details', async (t) => { const journeys = await client.journeys(spichernstr, amrumerStr, { results: 1, when }) @@ -141,7 +142,7 @@ test('journey part details', async (t) => { t.ok(part.direction) t.ok(Array.isArray(part.passed)) - for (let passed of part.passed) assertValidPassed(t, passed) + for (let passed of part.passed) assertValidStopover(t, passed) t.end() }) @@ -200,7 +201,7 @@ test('journeys – station to POI', async (t) => { -test.only('departures', async (t) => { +test('departures', async (t) => { const deps = await client.departures(spichernstr, {duration: 5, when}) t.ok(Array.isArray(deps)) @@ -220,7 +221,8 @@ test.only('departures', async (t) => { t.end() }) -test('departures at 7-digit station', async (t) => { +// todo +test.skip('departures at 7-digit station', async (t) => { const eisenach = '8010097' // see derhuerst/vbb-hafas#22 await client.departures(eisenach, {when}) t.pass('did not fail') @@ -268,6 +270,7 @@ test('locations', async (t) => { +// todo test.skip('radar', async (t) => { const vehicles = await client.radar(52.52411, 13.41002, 52.51942, 13.41709, { duration: 5 * 60, when From 33417a6e619631a768a6c46893b5156106e0a632 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 20 Nov 2017 15:43:13 +0100 Subject: [PATCH 26/73] query journey parts --- index.js | 34 +++++++++++++++-- lib/default-profile.js | 2 + package.json | 1 + parse/index.js | 1 + parse/journey-part.js | 87 ++++++++++++++++++++++++++++++++++++++++++ parse/journey.js | 77 +------------------------------------ test/vbb.js | 3 +- 7 files changed, 125 insertions(+), 80 deletions(-) create mode 100644 parse/journey-part.js diff --git a/index.js b/index.js index bc9fba6d..c2227c61 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,8 @@ 'use strict' +const minBy = require('lodash/minBy') +const maxBy = require('lodash/maxBy') + const defaultProfile = require('./lib/default-profile') const request = require('./lib/request') @@ -23,14 +26,14 @@ const createClient = (profile) => { return request(profile, { meth: 'StationBoard', req: { - type: 'DEP', + type: 'DEP', date: profile.formatDate(profile, opt.when), time: profile.formatTime(profile, opt.when), stbLoc: profile.formatStation(station), dirLoc: dir, jnyFltrL: [products], dur: opt.duration, - getPasslist: false + getPasslist: false } }) .then((d) => { @@ -156,7 +159,32 @@ const createClient = (profile) => { }) } - return {departures, journeys, locations, nearby} + const journeyPart = (ref, lineName, opt = {}) => { + opt.when = opt.when || new Date() + + return request(profile, { + cfg: {polyEnc: 'GPA'}, + meth: 'JourneyDetails', + req: { + jid: ref, + name: lineName, + date: profile.formatDate(profile, opt.when) + } + }) + .then((d) => { + const parse = profile.parseJourneyPart(profile, d.locations, d.lines, d.remarks) + + const part = { // pretend the part is contained in a journey + type: 'JNY', + dep: minBy(d.journey.stopL, 'idx'), + arr: maxBy(d.journey.stopL, 'idx'), + jny: d.journey + } + return parse(d.journey, part) + }) + } + + return {departures, journeys, locations, nearby, journeyPart} } module.exports = createClient diff --git a/lib/default-profile.js b/lib/default-profile.js index 5de45fd7..0eff0a20 100644 --- a/lib/default-profile.js +++ b/lib/default-profile.js @@ -2,6 +2,7 @@ const parseDateTime = require('../parse/date-time') const parseDeparture = require('../parse/departure') +const parseJourneyPart = require('../parse/journey-part') const parseJourney = require('../parse/journey') const parseLine = require('../parse/line') const parseLocation = require('../parse/location') @@ -32,6 +33,7 @@ const defaultProfile = { parseDateTime, parseDeparture, + parseJourneyPart, parseJourney, parseLine, parseStationName: id, diff --git a/package.json b/package.json index ba054276..fd0f038e 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ }, "dependencies": { "fetch-ponyfill": "^4.1.0", + "lodash": "^4.17.4", "moment-timezone": "^0.5.13", "pinkie-promise": "^2.0.1", "query-string": "^5.0.0", diff --git a/parse/index.js b/parse/index.js index 7c9c35bb..af61cf55 100644 --- a/parse/index.js +++ b/parse/index.js @@ -7,6 +7,7 @@ module.exports = { remark: require('./remark'), operator: require('./operator'), stopover: require('./stopover'), + journeyPart: require('./journey-part'), journey: require('./journey'), nearby: require('./nearby'), movement: require('./movement') diff --git a/parse/journey-part.js b/parse/journey-part.js new file mode 100644 index 00000000..a86d40e8 --- /dev/null +++ b/parse/journey-part.js @@ -0,0 +1,87 @@ +'use strict' + +const parseDateTime = require('./date-time') + +const clone = obj => Object.assign({}, obj) + +const createParseJourneyPart = (profile, stations, lines, remarks) => { + const tz = profile.timezone + + const parseStopover = (j, st) => { + const res = { + station: stations[parseInt(st.locX)] + } + if (st.aTimeR || st.aTimeS) { + const arr = parseDateTime(tz, j.date, st.aTimeR || st.aTimeS) + res.arrival = arr.format() + } + if (st.dTimeR || st.dTimeS) { + const dep = parseDateTime(tz, j.date, st.dTimeR || st.dTimeS) + res.departure = dep.format() + } + + return res + } + + // todo: finish parse/remark.js first + const applyRemark = (j, rm) => {} + + // todo: pt.sDays + // todo: pt.dep.dProgType, pt.arr.dProgType + // todo: what is pt.jny.dirFlg? + // todo: how does pt.freq work? + const parseJourneyPart = (j, pt) => { // j = journey, pt = part + const dep = profile.parseDateTime(tz, j.date, pt.dep.dTimeR || pt.dep.dTimeS) + const arr = profile.parseDateTime(tz, j.date, pt.arr.aTimeR || pt.arr.aTimeS) + const res = { + origin: clone(stations[parseInt(pt.dep.locX)]) || null, + destination: clone(stations[parseInt(pt.arr.locX)]), + departure: dep.format(), + arrival: arr.format() + } + + if (pt.dep.dTimeR && pt.dep.dTimeS) { + const realtime = profile.parseDateTime(tz, j.date, pt.dep.dTimeR) + const planned = profile.parseDateTime(tz, j.date, pt.dep.dTimeS) + res.delay = Math.round((realtime - planned) / 1000) + } + + if (pt.type === 'WALK') { + res.mode = 'walking' + } else if (pt.type === 'JNY') { + res.id = pt.jny.jid + res.line = lines[parseInt(pt.jny.prodX)] || null + res.direction = profile.parseStationName(pt.jny.dirTxt) + + if (pt.dep.dPlatfS) res.departurePlatform = pt.dep.dPlatfS + if (pt.arr.aPlatfS) res.arrivalPlatform = pt.arr.aPlatfS + + if (pt.jny.stopL) { + res.passed = pt.jny.stopL.map(stopover => parseStopover(j, stopover)) + } + if (Array.isArray(pt.jny.remL)) { + for (let remark of pt.jny.remL) applyRemark(j, remark) + } + + if (pt.jny.freq && pt.jny.freq.jnyL) { + const parseAlternative = (a) => { + // todo: realtime + const when = profile.parseDateTime(tz, j.date, a.stopL[0].dTimeS) + return { + line: lines[parseInt(a.prodX)] || null, + when: when.format() + } + } + res.alternatives = pt.jny.freq.jnyL + .filter(a => a.stopL[0].locX === pt.dep.locX) + .map(parseAlternative) + } + } + + return res + } + + return parseJourneyPart +} + +module.exports = createParseJourneyPart diff --git a/parse/journey.js b/parse/journey.js index f622c151..11b1b56b 100644 --- a/parse/journey.js +++ b/parse/journey.js @@ -1,85 +1,12 @@ 'use strict' const parseDateTime = require('./date-time') +const createParseJourneyPart = require('./journey-part') const clone = obj => Object.assign({}, obj) const createParseJourney = (profile, stations, lines, remarks) => { - const tz = profile.timezone - - const parseStopover = (j, st) => { - const res = { - station: stations[parseInt(st.locX)] - } - if (st.aTimeR || st.aTimeS) { - const arr = parseDateTime(tz, j.date, st.aTimeR || st.aTimeS) - res.arrival = arr.format() - } - if (st.dTimeR || st.dTimeS) { - const dep = parseDateTime(tz, j.date, st.dTimeR || st.dTimeS) - res.departure = dep.format() - } - - return res - } - - // todo: finish parse/remark.js first - const applyRemark = (j, rm) => {} - - // todo: pt.sDays - // todo: pt.dep.dProgType, pt.arr.dProgType - // todo: what is pt.jny.dirFlg? - // todo: how does pt.freq work? - const parsePart = (j, pt) => { // j = journey, pt = part - const dep = profile.parseDateTime(tz, j.date, pt.dep.dTimeR || pt.dep.dTimeS) - const arr = profile.parseDateTime(tz, j.date, pt.arr.aTimeR || pt.arr.aTimeS) - const res = { - origin: clone(stations[parseInt(pt.dep.locX)]) || null, - destination: clone(stations[parseInt(pt.arr.locX)]), - departure: dep.format(), - arrival: arr.format() - } - - if (pt.dep.dTimeR && pt.dep.dTimeS) { - const realtime = profile.parseDateTime(tz, j.date, pt.dep.dTimeR) - const planned = profile.parseDateTime(tz, j.date, pt.dep.dTimeS) - res.delay = Math.round((realtime - planned) / 1000) - } - - if (pt.type === 'WALK') { - res.mode = 'walking' - } else if (pt.type === 'JNY') { - res.id = pt.jny.jid - res.line = lines[parseInt(pt.jny.prodX)] || null - res.direction = profile.parseStationName(pt.jny.dirTxt) - - if (pt.dep.dPlatfS) res.departurePlatform = pt.dep.dPlatfS - if (pt.arr.aPlatfS) res.arrivalPlatform = pt.arr.aPlatfS - - if (pt.jny.stopL) { - res.passed = pt.jny.stopL.map(stopover => parseStopover(j, stopover)) - } - if (Array.isArray(pt.jny.remL)) { - for (let remark of pt.jny.remL) applyRemark(j, remark) - } - - if (pt.jny.freq && pt.jny.freq.jnyL) { - const parseAlternative = (a) => { - // todo: realtime - const when = profile.parseDateTime(tz, j.date, a.stopL[0].dTimeS) - return { - line: lines[parseInt(a.prodX)] || null, - when: when.format() - } - } - res.alternatives = pt.jny.freq.jnyL - .filter(a => a.stopL[0].locX === pt.dep.locX) - .map(parseAlternative) - } - } - - return res - } + const parsePart = createParseJourneyPart(profile, stations, lines, remarks) // todo: c.sDays // todo: c.dep.dProgType, c.arr.dProgType diff --git a/test/vbb.js b/test/vbb.js index c13c0119..f0f1eca1 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -122,8 +122,7 @@ test('journeys – fails with no product', async (t) => { } }) -// todo -test.skip('journey part details', async (t) => { +test('journey part details', async (t) => { const journeys = await client.journeys(spichernstr, amrumerStr, { results: 1, when }) From 3fb0943680afde0cc5e28093005b34ebfcd9eab5 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 20 Nov 2017 17:37:08 +0100 Subject: [PATCH 27/73] query live movements in an area --- format/index.js | 3 ++- format/rectangle.js | 16 ++++++++++++++++ index.js | 43 +++++++++++++++++++++++++++++++++++++++++- lib/default-profile.js | 4 +++- parse/movement.js | 2 +- test/util.js | 42 ++++++++++++++++++++++++++++++----------- test/vbb.js | 31 +++++++++++++++--------------- 7 files changed, 110 insertions(+), 31 deletions(-) create mode 100644 format/rectangle.js diff --git a/format/index.js b/format/index.js index 75f69239..a119dd37 100644 --- a/format/index.js +++ b/format/index.js @@ -8,5 +8,6 @@ module.exports = { address: require('./address'), poi: require('./poi'), location: require('./location'), - locationFilter: require('./location-filter') + locationFilter: require('./location-filter'), + rectangle: require('./rectangle') } diff --git a/format/rectangle.js b/format/rectangle.js new file mode 100644 index 00000000..51f83e9e --- /dev/null +++ b/format/rectangle.js @@ -0,0 +1,16 @@ +'use strict' + +const formatRectangle = (profile, north, west, south, east) => { + return { + llCrd: { + x: profile.formatCoord(west), + y: profile.formatCoord(south) + }, + urCrd: { + x: profile.formatCoord(east), + y: profile.formatCoord(north) + } + } +} + +module.exports = formatRectangle diff --git a/index.js b/index.js index c2227c61..68559d43 100644 --- a/index.js +++ b/index.js @@ -184,7 +184,48 @@ const createClient = (profile) => { }) } - return {departures, journeys, locations, nearby, journeyPart} + const radar = (north, west, south, east, opt) => { + if ('number' !== typeof north) throw new Error('north must be a number.') + if ('number' !== typeof west) throw new Error('west must be a number.') + if ('number' !== typeof south) throw new Error('south must be a number.') + if ('number' !== typeof east) throw new Error('east must be a number.') + + opt = Object.assign({ + results: 256, // maximum number of vehicles + duration: 30, // compute frames for the next n seconds + frames: 3 // nr of frames to compute + }, opt || {}) + opt.when = opt.when || new Date() + + const durationPerStep = opt.duration / Math.max(opt.frames, 1) * 1000 + return request(profile, { + meth: 'JourneyGeoPos', + req: { + maxJny: opt.results, + onlyRT: false, // todo: does this mean "only realtime"? + date: profile.formatDate(profile, opt.when), + time: profile.formatTime(profile, opt.when), + // todo: would a ring work here as well? + rect: profile.formatRectangle(profile, north, west, south, east), + perSize: opt.duration * 1000, + perStep: Math.round(durationPerStep), + ageOfReport: true, // todo: what is this? + jnyFltrL: [ + // todo: use `profile.formatProducts(opt.products || {})` + {type: 'PROD', mode: 'INC', value: '127'} + ], + trainPosMode: 'CALC' // todo: what is this? what about realtime? + } + }) + .then((d) => { + if (!Array.isArray(d.jnyL)) return [] + + const parse = profile.parseMovement(profile, d.locations, d.lines, d.remarks) + return d.jnyL.map(parse) + }) + } + + return {departures, journeys, locations, nearby, journeyPart, radar} } module.exports = createClient diff --git a/lib/default-profile.js b/lib/default-profile.js index 0eff0a20..32561573 100644 --- a/lib/default-profile.js +++ b/lib/default-profile.js @@ -21,6 +21,7 @@ const formatPoi = require('../format/poi') const formatStation = require('../format/station') const formatTime = require('../format/time') const formatLocation = require('../format/location') +const formatRectangle = require('../format/rectangle') const id = x => x @@ -52,7 +53,8 @@ const defaultProfile = { formatPoi, formatStation, formatTime, - formatLocation + formatLocation, + formatRectangle } module.exports = defaultProfile diff --git a/parse/movement.js b/parse/movement.js index e9662291..67e3d591 100644 --- a/parse/movement.js +++ b/parse/movement.js @@ -27,7 +27,7 @@ const createParseMovement = (profile, locations, lines, remarks) => { } const res = { - direction: m.dirTxt, + direction: profile.parseStationName(m.dirTxt), line: lines[m.prodX] || null, coordinates: m.pos ? { latitude: m.pos.y / 1000000, diff --git a/test/util.js b/test/util.js index 36805d1e..bc023dc1 100644 --- a/test/util.js +++ b/test/util.js @@ -3,31 +3,52 @@ const isRoughlyEqual = require('is-roughly-equal') const floor = require('floordate') -const assertValidStation = (t, s) => { +const assertValidStation = (t, s, coordsOptional = false) => { + t.equal(typeof s.type, 'string') t.equal(s.type, 'station') t.equal(typeof s.id, 'string') + + t.equal(typeof s.name, 'string') + if (!coordsOptional) { + if (!s.coordinates) console.trace() + t.ok(s.coordinates) + } + if (s.coordinates) { + t.equal(typeof s.coordinates.latitude, 'number') + t.equal(typeof s.coordinates.longitude, 'number') + } } const assertValidPoi = (t, p) => { + t.equal(typeof p.type, 'string') t.equal(p.type, 'poi') t.equal(typeof p.id, 'string') + + t.equal(typeof p.name, 'string') + t.ok(p.coordinates) + if (s.coordinates) { + t.equal(typeof p.coordinates.latitude, 'number') + t.equal(typeof p.coordinates.longitude, 'number') + } } const assertValidAddress = (t, a) => { + t.equal(typeof a.type, 'string') t.equal(a.type, 'address') + + t.equal(typeof a.name, 'string') + t.ok(a.coordinates) + if (s.coordinates) { + t.equal(typeof a.coordinates.latitude, 'number') + t.equal(typeof a.coordinates.longitude, 'number') + } } const assertValidLocation = (t, l) => { - t.equal(typeof l.type, 'string') if (l.type === 'station') assertValidStation(t, l) else if (l.type === 'poi') assertValidPoi(t, l) else if (l.type === 'address') assertValidAddress(t, l) else t.fail('invalid type ' + l.type) - - t.equal(typeof l.name, 'string') - t.ok(l.coordinates) - t.equal(typeof l.coordinates.latitude, 'number') - t.equal(typeof l.coordinates.longitude, 'number') } const isValidMode = (m) => { @@ -40,7 +61,6 @@ const isValidMode = (m) => { const assertValidLine = (t, l) => { t.equal(l.type, 'line') t.equal(typeof l.name, 'string') - if (!isValidMode(l.mode)) console.error(l) t.ok(isValidMode(l.mode), 'invalid mode ' + l.mode) t.equal(typeof l.product, 'string') t.equal(l.public, true) @@ -50,14 +70,14 @@ const isValidDateTime = (w) => { return !Number.isNaN(+new Date(w)) } -const assertValidStopover = (t, s) => { +const assertValidStopover = (t, s, coordsOptional = false) => { if ('arrival' in s) t.ok(isValidDateTime(s.arrival)) if ('departure' in s) t.ok(isValidDateTime(s.departure)) if (!('arrival' in s) && !('departure' in s)) { t.fail('stopover doesn\'t contain arrival or departure') } t.ok(s.station) - assertValidStation(t, s.station) + assertValidStation(t, s.station, coordsOptional) } const minute = 60 * 1000 @@ -86,5 +106,5 @@ module.exports = { assertValidLine, isValidDateTime, assertValidStopover, - when, isValidWhen, assertValidWhen + hour, when, isValidWhen, assertValidWhen } diff --git a/test/vbb.js b/test/vbb.js index f0f1eca1..0bbd9331 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -269,8 +269,7 @@ test('locations', async (t) => { -// todo -test.skip('radar', async (t) => { +test('radar', async (t) => { const vehicles = await client.radar(52.52411, 13.41002, 52.51942, 13.41709, { duration: 5 * 60, when }) @@ -290,21 +289,19 @@ test.skip('radar', async (t) => { t.ok(v.coordinates.longitude <= 15, 'vehicle is too far away') t.ok(Array.isArray(v.nextStops)) - for (let s of v.nextStops) { - assertValidFrameStation(t, s.station) - if (!s.arrival && !s.departure) - t.ifError(new Error('neither arrival nor departure return')) - if (s.arrival) { - t.equal(typeof s.arrival, 'string') - const arr = +new Date(s.arrival) - t.ok(!Number.isNaN(arr)) + for (let st of v.nextStops) { + assertValidStopover(t, st, true) + t.strictEqual(st.station.name.indexOf('(Berlin)'), -1) + + if (st.arrival) { + t.equal(typeof st.arrival, 'string') + const arr = +new Date(st.arrival) // note that this can be an ICE train t.ok(isRoughlyEqual(14 * hour, +when, arr)) } - if (s.departure) { - t.equal(typeof s.departure, 'string') - const dep = +new Date(s.departure) - t.ok(!Number.isNaN(dep)) + if (st.departure) { + t.equal(typeof st.departure, 'string') + const dep = +new Date(st.departure) // note that this can be an ICE train t.ok(isRoughlyEqual(14 * hour, +when, dep)) } @@ -312,8 +309,10 @@ test.skip('radar', async (t) => { t.ok(Array.isArray(v.frames)) for (let f of v.frames) { - assertValidFrameStation(t, f.origin) - assertValidFrameStation(t, f.destination) + assertValidStation(t, f.origin, true) + t.strictEqual(f.origin.name.indexOf('(Berlin)'), -1) + assertValidStation(t, f.destination, true) + t.strictEqual(f.destination.name.indexOf('(Berlin)'), -1) t.equal(typeof f.t, 'number') } } From ed0dfd06dd5a4a1096545e97bf00686b8153d3c7 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 27 Nov 2017 19:39:18 +0100 Subject: [PATCH 28/73] pull out bitmask parsing from profiles --- format/products-bitmask.js | 14 ++++++++++++++ p/db/index.js | 8 ++++++-- p/db/modes.js | 19 ------------------- p/vbb/index.js | 8 ++++++-- p/vbb/modes.js | 19 ------------------- parse/products-bitmask.js | 16 ++++++++++++++++ 6 files changed, 42 insertions(+), 42 deletions(-) create mode 100644 format/products-bitmask.js create mode 100644 parse/products-bitmask.js diff --git a/format/products-bitmask.js b/format/products-bitmask.js new file mode 100644 index 00000000..9ec158e0 --- /dev/null +++ b/format/products-bitmask.js @@ -0,0 +1,14 @@ +'use strict' + +const createFormatBitmask = (modes) => { + const formatBitmask = (products) => { + let bitmask = 0 + for (let product in products) { + if (products[product] === true) bitmask += modes[product].bitmask + } + return bitmask + } + return formatBitmask +} + +module.exports = createFormatBitmask diff --git a/p/db/index.js b/p/db/index.js index 6cf75c8a..c68ea256 100644 --- a/p/db/index.js +++ b/p/db/index.js @@ -4,11 +4,15 @@ const crypto = require('crypto') const _formatStation = require('../../format/station') const _parseLine = require('../../parse/line') +const createParseBitmask = require('../../parse/products-bitmask') +const createFormatBitmask = require('../../format/products-bitmask') const {accessibility, bike} = require('../../format/filters') const modes = require('./modes') const formatLoyaltyCard = require('./loyalty-cards').format +const formatBitmask = createFormatBitmask(modes) + const transformReqBody = (body) => { body.client = {id: 'DB', v: '16040000', type: 'IPH', name: 'DB Navigator'} body.ext = 'DB.R15.12.a' @@ -87,7 +91,7 @@ const formatProducts = (products) => { return { type: 'PROD', mode: 'INC', - value: modes.stringifyBitmask(products) + '' + value: formatBitmask(products) + '' } } @@ -102,7 +106,7 @@ const dbProfile = { // todo: parseLocation parseLine, - parseProducts: modes.parseBitmask, + parseProducts: createParseBitmask(modes.bitmasks), formatStation, formatProducts diff --git a/p/db/modes.js b/p/db/modes.js index 3b0a6a94..9e91d505 100644 --- a/p/db/modes.js +++ b/p/db/modes.js @@ -91,23 +91,4 @@ m.bitmasks[128] = m.subway m.bitmasks[256] = m.tram m.bitmasks[512] = m.taxi -// todo: move up -m.stringifyBitmask = (products) => { - let bitmask = 0 - for (let product in products) { - if (products[product] === true) bitmask += m[product].bitmask - } - return bitmask -} - -// todo: move up -m.parseBitmask = (bitmask) => { - let products = {}, i = 1 - do { - products[m.bitmasks[i].product] = !!(bitmask & i) - i *= 2 - } while (m.bitmasks[i] && m.bitmasks[i].product) - return products -} - module.exports = m diff --git a/p/vbb/index.js b/p/vbb/index.js index 973d531a..f16e84ba 100644 --- a/p/vbb/index.js +++ b/p/vbb/index.js @@ -6,9 +6,13 @@ const {to12Digit, to9Digit} = require('vbb-translate-ids') const _formatStation = require('../../format/station') const _parseLine = require('../../parse/line') const _parseLocation = require('../../parse/location') +const createParseBitmask = require('../../parse/products-bitmask') +const createFormatBitmask = require('../../format/products-bitmask') const modes = require('./modes') +const formatBitmask = createFormatBitmask(modes) + const transformReqBody = (body) => { body.client = {type: 'IPA', id: 'BVG'} body.ext = 'VBB.2' @@ -67,7 +71,7 @@ const formatProducts = (products) => { return { type: 'PROD', mode: 'INC', - value: modes.stringifyBitmask(products) + '' + value: formatBitmask(products) + '' } } @@ -79,7 +83,7 @@ const vbbProfile = { parseStationName: shorten, parseLocation, parseLine, - parseProducts: modes.parseBitmask, + parseProducts: createParseBitmask(modes.bitmasks), formatStation, formatProducts diff --git a/p/vbb/modes.js b/p/vbb/modes.js index 9b78cfd9..0ce23156 100644 --- a/p/vbb/modes.js +++ b/p/vbb/modes.js @@ -123,23 +123,4 @@ m.categories = [ // return m.categories[parseInt(category)] || m.unknown // } -// todo: move up -m.stringifyBitmask = (types) => { - let bitmask = 0 - for (let type in types) { - if (types[type] === true) bitmask += m[type].bitmask - } - return bitmask -} - -// todo: move up -m.parseBitmask = (bitmask) => { - let types = {}, i = 1 - do { - types[m.bitmasks[i].type] = !!(bitmask & i) - i *= 2 - } while (m.bitmasks[i] && m.bitmasks[i].type) - return types -} - module.exports = m diff --git a/parse/products-bitmask.js b/parse/products-bitmask.js new file mode 100644 index 00000000..6f31530c --- /dev/null +++ b/parse/products-bitmask.js @@ -0,0 +1,16 @@ +'use strict' + +const createParseBitmask = (bitmasks) => { + const createBitmask = (bitmask) => { + const products = {} + let i = 1 + do { + products[bitmasks[i].product] = !!(bitmask & i) + i *= 2 + } while (bitmasks[i] && bitmasks[i].product) + return products + } + return createBitmask +} + +module.exports = createParseBitmask From 2e113d9520e8940f6d16615a937e8ccc4051dab1 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 27 Nov 2017 19:45:33 +0100 Subject: [PATCH 29/73] expose only supported methods, fix test helpers :bug: --- index.js | 5 ++++- lib/default-profile.js | 5 ++++- p/vbb/index.js | 5 ++++- test/util.js | 4 ++-- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index 68559d43..21ac30bb 100644 --- a/index.js +++ b/index.js @@ -225,7 +225,10 @@ const createClient = (profile) => { }) } - return {departures, journeys, locations, nearby, journeyPart, radar} + const client = {departures, journeys, locations, nearby} + if (profile.journeyPart) client.journeyPart = journeyPart + if (profile.radar) client.radar = radar + return client } module.exports = createClient diff --git a/lib/default-profile.js b/lib/default-profile.js index 32561573..8d673536 100644 --- a/lib/default-profile.js +++ b/lib/default-profile.js @@ -54,7 +54,10 @@ const defaultProfile = { formatStation, formatTime, formatLocation, - formatRectangle + formatRectangle, + + journeyPart: false, + radar: false } module.exports = defaultProfile diff --git a/p/vbb/index.js b/p/vbb/index.js index f16e84ba..9bfd1a9a 100644 --- a/p/vbb/index.js +++ b/p/vbb/index.js @@ -86,7 +86,10 @@ const vbbProfile = { parseProducts: createParseBitmask(modes.bitmasks), formatStation, - formatProducts + formatProducts, + + journeyPart: true, + radar: true } module.exports = vbbProfile diff --git a/test/util.js b/test/util.js index bc023dc1..309e84e7 100644 --- a/test/util.js +++ b/test/util.js @@ -26,7 +26,7 @@ const assertValidPoi = (t, p) => { t.equal(typeof p.name, 'string') t.ok(p.coordinates) - if (s.coordinates) { + if (p.coordinates) { t.equal(typeof p.coordinates.latitude, 'number') t.equal(typeof p.coordinates.longitude, 'number') } @@ -38,7 +38,7 @@ const assertValidAddress = (t, a) => { t.equal(typeof a.name, 'string') t.ok(a.coordinates) - if (s.coordinates) { + if (a.coordinates) { t.equal(typeof a.coordinates.latitude, 'number') t.equal(typeof a.coordinates.longitude, 'number') } From 42f90b790ec610bc41ce8f7182f0462dc2c1c1a8 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Wed, 29 Nov 2017 02:12:49 +0100 Subject: [PATCH 30/73] =?UTF-8?q?fix=20build=20=F0=9F=92=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 ++-- test/util.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index fd0f038e..7c4e25bf 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "vbb-stations-autocomplete": "^2.9.0" }, "scripts": { - "test": "node test/index.js | tap-spec", - "prepublishOnly": "npm test" + "test": "node test/index.js", + "prepublishOnly": "npm test | tap-spec" } } diff --git a/test/util.js b/test/util.js index 309e84e7..323a185e 100644 --- a/test/util.js +++ b/test/util.js @@ -90,7 +90,7 @@ const when = new Date(+floor(new Date(), 'week') + week + 10 * hour) const isValidWhen = (w) => { const ts = +new Date(w) if (Number.isNaN(ts)) return false - return isRoughlyEqual(10 * hour, +when, ts) + return isRoughlyEqual(12 * hour, +when, ts) } const assertValidWhen = (t, w) => { From 817f44ea392056c8865cccdd5723adb7277b90c7 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Wed, 29 Nov 2017 02:27:31 +0100 Subject: [PATCH 31/73] =?UTF-8?q?Node=206=20=F0=9F=92=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + test/db.js | 51 +++++++++++++++++++------------------- test/vbb.js | 69 ++++++++++++++++++++++++++-------------------------- 3 files changed, 62 insertions(+), 59 deletions(-) diff --git a/package.json b/package.json index 7c4e25bf..6f570e43 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "vbb-translate-ids": "^3.1.0" }, "devDependencies": { + "co": "^4.6.0", "db-stations": "^1.25.0", "floordate": "^3.0.0", "is-roughly-equal": "^0.1.0", diff --git a/test/db.js b/test/db.js index 03ca7ef8..9b7b927b 100644 --- a/test/db.js +++ b/test/db.js @@ -3,6 +3,7 @@ const getStations = require('db-stations').full const tapePromise = require('tape-promise').default const tape = require('tape') +const co = require('co') const isRoughlyEqual = require('is-roughly-equal') const createClient = require('..') @@ -61,8 +62,8 @@ const assertValidProducts = (t, p) => { const test = tapePromise(tape) const client = createClient(dbProfile) -test('Berlin Jungfernheide to München Hbf', async (t) => { - const journeys = await client.journeys('8011167', '8000261', { +test('Berlin Jungfernheide to München Hbf', co.wrap(function* (t) { + const journeys = yield client.journeys('8011167', '8000261', { when, passedStations: true }) @@ -70,7 +71,7 @@ test('Berlin Jungfernheide to München Hbf', async (t) => { t.ok(journeys.length > 0, 'no journeys') for (let journey of journeys) { assertValidStation(t, journey.origin) - if (!await findStation(journey.origin.id)) { + if (!(yield findStation(journey.origin.id))) { console.error('unknown station', journey.origin.id, journey.origin.name) } if (journey.origin.products) { @@ -79,7 +80,7 @@ test('Berlin Jungfernheide to München Hbf', async (t) => { t.ok(isValidWhen(journey.departure)) assertValidStation(t, journey.destination) - if (!await findStation(journey.origin.id)) { + if (!(yield findStation(journey.origin.id))) { console.error('unknown station', journey.destination.id, journey.destination.name) } if (journey.destination.products) { @@ -92,14 +93,14 @@ test('Berlin Jungfernheide to München Hbf', async (t) => { const part = journey.parts[0] assertValidStation(t, part.origin) - if (!await findStation(part.origin.id)) { + if (!(yield findStation(part.origin.id))) { console.error('unknown station', part.origin.id, part.origin.name) } t.ok(isValidWhen(part.departure)) t.equal(typeof part.departurePlatform, 'string') assertValidStation(t, part.destination) - if (!await findStation(part.destination.id)) { + if (!(yield findStation(part.destination.id))) { console.error('unknown station', part.destination.id, part.destination.name) } t.ok(isValidWhen(part.arrival)) @@ -112,10 +113,10 @@ test('Berlin Jungfernheide to München Hbf', async (t) => { } t.end() -}) +})) -test('Berlin Jungfernheide to Torfstraße 17', async (t) => { - const journeys = await client.journeys('8011167', { +test('Berlin Jungfernheide to Torfstraße 17', co.wrap(function* (t) { + const journeys = yield client.journeys('8011167', { type: 'address', name: 'Torfstraße 17', latitude: 52.5416823, longitude: 13.3491223 }, {when}) @@ -126,7 +127,7 @@ test('Berlin Jungfernheide to Torfstraße 17', async (t) => { const part = journey.parts[journey.parts.length - 1] assertValidStation(t, part.origin) - if (!await findStation(part.origin.id)) { + if (!(yield findStation(part.origin.id))) { console.error('unknown station', part.origin.id, part.origin.name) } if (part.origin.products) assertValidProducts(t, part.origin.products) @@ -140,10 +141,10 @@ test('Berlin Jungfernheide to Torfstraße 17', async (t) => { t.ok(isRoughlyEqual(.0001, d.coordinates.longitude, 13.3491223)) t.end() -}) +})) -test('Berlin Jungfernheide to ATZE Musiktheater', async (t) => { - const journeys = await client.journeys('8011167', { +test('Berlin Jungfernheide to ATZE Musiktheater', co.wrap(function* (t) { + const journeys = yield client.journeys('8011167', { type: 'poi', name: 'ATZE Musiktheater', id: '991598902', latitude: 52.542417, longitude: 13.350437 }, {when}) @@ -154,7 +155,7 @@ test('Berlin Jungfernheide to ATZE Musiktheater', async (t) => { const part = journey.parts[journey.parts.length - 1] assertValidStation(t, part.origin) - if (!await findStation(part.origin.id)) { + if (!(yield findStation(part.origin.id))) { console.error('unknown station', part.origin.id, part.origin.name) } if (part.origin.products) assertValidProducts(t, part.origin.products) @@ -168,17 +169,17 @@ test('Berlin Jungfernheide to ATZE Musiktheater', async (t) => { t.ok(isRoughlyEqual(.0001, d.coordinates.longitude, 13.350402)) t.end() -}) +})) -test('departures at Berlin Jungfernheide', async (t) => { - const deps = await client.departures('8011167', { +test('departures at Berlin Jungfernheide', co.wrap(function* (t) { + const deps = yield client.departures('8011167', { duration: 5, when }) t.ok(Array.isArray(deps)) for (let dep of deps) { assertValidStation(t, dep.station) - if (!await findStation(dep.station.id)) { + if (!(yield findStation(dep.station.id))) { console.error('unknown station', dep.station.id, dep.station.name) } if (dep.station.products) assertValidProducts(t, dep.station.products) @@ -186,10 +187,10 @@ test('departures at Berlin Jungfernheide', async (t) => { } t.end() -}) +})) -test('nearby Berlin Jungfernheide', async (t) => { - const nearby = await client.nearby(52.530273, 13.299433, { +test('nearby Berlin Jungfernheide', co.wrap(function* (t) { + const nearby = yield client.nearby(52.530273, 13.299433, { results: 2, distance: 400 }) @@ -201,10 +202,10 @@ test('nearby Berlin Jungfernheide', async (t) => { t.ok(nearby[0].distance <= 100) t.end() -}) +})) -test('locations named Jungfernheide', async (t) => { - const locations = await client.locations('Jungfernheide', { +test('locations named Jungfernheide', co.wrap(function* (t) { + const locations = yield client.locations('Jungfernheide', { results: 10 }) @@ -216,4 +217,4 @@ test('locations named Jungfernheide', async (t) => { t.ok(locations.some(isJungfernheide)) t.end() -}) +})) diff --git a/test/vbb.js b/test/vbb.js index 0bbd9331..53b8de79 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -5,6 +5,7 @@ const isRoughlyEqual = require('is-roughly-equal') const stations = require('vbb-stations-autocomplete') const tapePromise = require('tape-promise').default const tape = require('tape') +const co = require('co') const createClient = require('..') const vbbProfile = require('../p/vbb') @@ -30,8 +31,8 @@ const amrumerStr = '900000009101' const spichernstr = '900000042101' const bismarckstr = '900000024201' -test('journeys – station to station', async (t) => { - const journeys = await client.journeys(spichernstr, amrumerStr, { +test('journeys – station to station', co.wrap(function* (t) { + const journeys = yield client.journeys(spichernstr, amrumerStr, { results: 3, when, passedStations: true }) @@ -71,10 +72,10 @@ test('journeys – station to station', async (t) => { for (let passed of part.passed) assertValidStopover(t, passed) } t.end() -}) +})) -test('journeys – only subway', async (t) => { - const journeys = await client.journeys(spichernstr, bismarckstr, { +test('journeys – only subway', co.wrap(function* (t) { + const journeys = yield client.journeys(spichernstr, bismarckstr, { results: 20, when, products: { suburban: false, @@ -100,11 +101,11 @@ test('journeys – only subway', async (t) => { } } t.end() -}) +})) -test('journeys – fails with no product', async (t) => { +test('journeys – fails with no product', co.wrap(function* (t) { try { - await client.journeys(spichernstr, bismarckstr, { + yield client.journeys(spichernstr, bismarckstr, { when, products: { suburban: false, @@ -120,17 +121,17 @@ test('journeys – fails with no product', async (t) => { t.ok(err, 'error thrown') t.end() } -}) +})) -test('journey part details', async (t) => { - const journeys = await client.journeys(spichernstr, amrumerStr, { +test('journey part details', co.wrap(function* (t) { + const journeys = yield client.journeys(spichernstr, amrumerStr, { results: 1, when }) const p = journeys[0].parts[0] t.ok(p.id, 'precondition failed') t.ok(p.line.name, 'precondition failed') - const part = await client.journeyPart(p.id, p.line.name, {when}) + const part = yield client.journeyPart(p.id, p.line.name, {when}) t.equal(typeof part.id, 'string') t.ok(part.id) @@ -144,12 +145,12 @@ test('journey part details', async (t) => { for (let passed of part.passed) assertValidStopover(t, passed) t.end() -}) +})) -test('journeys – station to address', async (t) => { - const journeys = await client.journeys(spichernstr, { +test('journeys – station to address', co.wrap(function* (t) { + const journeys = yield client.journeys(spichernstr, { type: 'address', name: 'Torfstraße 17', latitude: 52.5416823, longitude: 13.3491223 }, {results: 1, when}) @@ -170,12 +171,12 @@ test('journeys – station to address', async (t) => { assertValidWhen(t, part.arrival) t.end() -}) +})) -test('journeys – station to POI', async (t) => { - const journeys = await client.journeys(spichernstr, { +test('journeys – station to POI', co.wrap(function* (t) { + const journeys = yield client.journeys(spichernstr, { type: 'poi', name: 'ATZE Musiktheater', id: 9980720, latitude: 52.543333, longitude: 13.351686 }, {results: 1, when}) @@ -196,12 +197,12 @@ test('journeys – station to POI', async (t) => { assertValidWhen(t, part.arrival) t.end() -}) +})) -test('departures', async (t) => { - const deps = await client.departures(spichernstr, {duration: 5, when}) +test('departures', co.wrap(function* (t) { + const deps = yield client.departures(spichernstr, {duration: 5, when}) t.ok(Array.isArray(deps)) t.deepEqual(deps, deps.sort((a, b) => t.when > b.when)) @@ -218,22 +219,22 @@ test('departures', async (t) => { assertValidLine(t, dep.line) } t.end() -}) +})) // todo -test.skip('departures at 7-digit station', async (t) => { +test.skip('departures at 7-digit station', co.wrap(function* (t) { const eisenach = '8010097' // see derhuerst/vbb-hafas#22 - await client.departures(eisenach, {when}) + yield client.departures(eisenach, {when}) t.pass('did not fail') t.end() -}) +})) -test('nearby', async (t) => { +test('nearby', co.wrap(function* (t) { // Berliner Str./Bundesallee - const nearby = await client.nearby(52.4873452, 13.3310411, {distance: 200}) + const nearby = yield client.nearby(52.4873452, 13.3310411, {distance: 200}) t.ok(Array.isArray(nearby)) for (let n of nearby) assertValidLocation(t, n, false) @@ -249,12 +250,12 @@ test('nearby', async (t) => { t.ok(nearby[1].distance < 200) t.end() -}) +})) -test('locations', async (t) => { - const locations = await client.locations('Alexanderplatz', {results: 10}) +test('locations', co.wrap(function* (t) { + const locations = yield client.locations('Alexanderplatz', {results: 10}) t.ok(Array.isArray(locations)) t.ok(locations.length > 0) @@ -265,12 +266,12 @@ test('locations', async (t) => { t.ok(locations.find((s) => s.type === 'address')) t.end() -}) +})) -test('radar', async (t) => { - const vehicles = await client.radar(52.52411, 13.41002, 52.51942, 13.41709, { +test('radar', co.wrap(function* (t) { + const vehicles = yield client.radar(52.52411, 13.41002, 52.51942, 13.41709, { duration: 5 * 60, when }) @@ -317,4 +318,4 @@ test('radar', async (t) => { } } t.end() -}) +})) From 658d67e3b2f7ee4134623f73fc4c5046994280d6 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Thu, 7 Dec 2017 20:21:14 +0100 Subject: [PATCH 32/73] mark journeys & journey parts as cancelled see derhuerst/vbb-gtfs#19 see public-transport/friend-public-transport-format#27 --- parse/journey-part.js | 9 +++++++++ parse/journey.js | 8 +++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/parse/journey-part.js b/parse/journey-part.js index a86d40e8..f2483277 100644 --- a/parse/journey-part.js +++ b/parse/journey-part.js @@ -78,6 +78,15 @@ const createParseJourneyPart = (profile, stations, lines, remarks) => { } } + // todo: follow public-transport/friendly-public-transport-format#27 here + // see also derhuerst/vbb-rest#19 + if (pt.arr.dCncl && pt.dep.dCncl) { + result.cancelled = true + result.departure = result.departurePlatform = null + result.arrival = result.arrivalPlatform = null + result.delay = null + } + return res } diff --git a/parse/journey.js b/parse/journey.js index 11b1b56b..21d0f124 100644 --- a/parse/journey.js +++ b/parse/journey.js @@ -15,13 +15,19 @@ const createParseJourney = (profile, stations, lines, remarks) => { // todo: use computed information from part const parseJourney = (j) => { const parts = j.secL.map(part => parsePart(j, part)) - return { + const res = { parts, origin: parts[0].origin, destination: parts[parts.length - 1].destination, departure: parts[0].departure, arrival: parts[parts.length - 1].arrival } + if (parts.some(p => p.cancelled)) { + res.cancelled = true + res.departure = res.arrival = null + } + + return res } return parseJourney From d77c450d11da9b7e659ee8470f6a9294c6fa6814 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 11 Dec 2017 14:41:28 +0100 Subject: [PATCH 33/73] fix products parsing :bug:, minor changes --- p/vbb/index.js | 2 +- p/vbb/modes.js | 44 +++++++++------------------------------ parse/departure.js | 1 + parse/products-bitmask.js | 4 ++-- test/db.js | 21 +++++++++++++++++++ test/vbb.js | 20 ++++++++++++++++++ 6 files changed, 55 insertions(+), 37 deletions(-) diff --git a/p/vbb/index.js b/p/vbb/index.js index 9bfd1a9a..4e0ddd72 100644 --- a/p/vbb/index.js +++ b/p/vbb/index.js @@ -30,7 +30,7 @@ const parseLine = (profile, l) => { const data = modes.bitmasks[parseInt(res.class)] if (data) { res.mode = data.mode - res.product = data.type + res.product = data.product } } diff --git a/p/vbb/modes.js b/p/vbb/modes.js index 0ce23156..65adc5be 100644 --- a/p/vbb/modes.js +++ b/p/vbb/modes.js @@ -8,10 +8,7 @@ const m = { name: 'S-Bahn', mode: 'train', short: 'S', - type: 'suburban', - color: '#008c4f', - unicode: '🚈', - ansi: ['green'] // `chalk` color code + product: 'suburban' }, subway: { @@ -20,10 +17,7 @@ const m = { name: 'U-Bahn', mode: 'train', short: 'U', - type: 'subway', - color: '#0067ac', - unicode: '🚇', - ansi: ['blue'] // `chalk` color code + product: 'subway' }, tram: { @@ -32,34 +26,25 @@ const m = { name: 'Tram', mode: 'train', short: 'T', - type: 'tram', - color: '#e3001b', - unicode: '🚋', - ansi: ['red'] // `chalk` color code + product: 'tram' }, bus: { category: 3, bitmask: 8, name: 'Bus', - mode: 'train', + mode: 'bus', short: 'B', - type: 'bus', - color: '#922A7D', - unicode: '🚌', - ansi: ['dim', 'magenta'] // `chalk` color codes + product: 'bus' }, ferry: { category: 4, bitmask: 16, name: 'Fähre', - mode: 'train', + mode: 'ferry', short: 'F', - type: 'ferry', - color: '#099bd6', - unicode: '🚢', - ansi: ['cyan'] // `chalk` color code + product: 'ferry' }, express: { @@ -68,10 +53,7 @@ const m = { name: 'IC/ICE', mode: 'train', short: 'E', - type: 'express', - color: '#f4e613', - unicode: '🚄', - ansi: ['yellow'] // `chalk` color code + product: 'express' }, regional: { @@ -80,10 +62,7 @@ const m = { name: 'RB/RE', mode: 'train', short: 'R', - type: 'regional', - color: '#D9222A', - unicode: '🚆', - ansi: ['red'] // `chalk` color code + product: 'regional' }, unknown: { @@ -92,10 +71,7 @@ const m = { name: 'unknown', mode: null, short: '?', - type: 'unknown', - color: '#555555', - unicode: '?', - ansi: ['gray'] // `chalk` color code + product: 'unknown' } } diff --git a/parse/departure.js b/parse/departure.js index f159a98e..ba897cf5 100644 --- a/parse/departure.js +++ b/parse/departure.js @@ -5,6 +5,7 @@ // - stdStop.dPlatfS, stdStop.dPlatfR // todo: what is d.jny.dirFlg? // todo: d.stbStop.dProgType +// todo: d.freq, d.freq.jnyL, see https://github.com/derhuerst/hafas-client/blob/9203ed1481f08baacca41ac5e3c19bf022f01b0b/parse.js#L115 const createParseDeparture = (profile, stations, lines, remarks) => { const tz = profile.timezone diff --git a/parse/products-bitmask.js b/parse/products-bitmask.js index 6f31530c..4880da7d 100644 --- a/parse/products-bitmask.js +++ b/parse/products-bitmask.js @@ -1,7 +1,7 @@ 'use strict' const createParseBitmask = (bitmasks) => { - const createBitmask = (bitmask) => { + const parseBitmask = (bitmask) => { const products = {} let i = 1 do { @@ -10,7 +10,7 @@ const createParseBitmask = (bitmasks) => { } while (bitmasks[i] && bitmasks[i].product) return products } - return createBitmask + return parseBitmask } module.exports = createParseBitmask diff --git a/test/db.js b/test/db.js index 9b7b927b..76ec2132 100644 --- a/test/db.js +++ b/test/db.js @@ -19,6 +19,20 @@ const { when, isValidWhen } = require('./util.js') +const assertValidStationProducts = (t, p) => { + t.ok(p) + t.equal(typeof p.nationalExp, 'boolean') + t.equal(typeof p.national, 'boolean') + t.equal(typeof p.regionalExp, 'boolean') + t.equal(typeof p.regional, 'boolean') + t.equal(typeof p.suburban, 'boolean') + t.equal(typeof p.bus, 'boolean') + t.equal(typeof p.ferry, 'boolean') + t.equal(typeof p.subway, 'boolean') + t.equal(typeof p.tram, 'boolean') + t.equal(typeof p.taxi, 'boolean') +} + const findStation = (id) => new Promise((yay, nay) => { const stations = getStations() stations @@ -71,6 +85,7 @@ test('Berlin Jungfernheide to München Hbf', co.wrap(function* (t) { t.ok(journeys.length > 0, 'no journeys') for (let journey of journeys) { assertValidStation(t, journey.origin) + assertValidStationProducts(t, journey.origin.products) if (!(yield findStation(journey.origin.id))) { console.error('unknown station', journey.origin.id, journey.origin.name) } @@ -80,6 +95,7 @@ test('Berlin Jungfernheide to München Hbf', co.wrap(function* (t) { t.ok(isValidWhen(journey.departure)) assertValidStation(t, journey.destination) + assertValidStationProducts(t, journey.origin.products) if (!(yield findStation(journey.origin.id))) { console.error('unknown station', journey.destination.id, journey.destination.name) } @@ -93,6 +109,7 @@ test('Berlin Jungfernheide to München Hbf', co.wrap(function* (t) { const part = journey.parts[0] assertValidStation(t, part.origin) + assertValidStationProducts(t, part.origin.products) if (!(yield findStation(part.origin.id))) { console.error('unknown station', part.origin.id, part.origin.name) } @@ -100,6 +117,7 @@ test('Berlin Jungfernheide to München Hbf', co.wrap(function* (t) { t.equal(typeof part.departurePlatform, 'string') assertValidStation(t, part.destination) + assertValidStationProducts(t, part.origin.products) if (!(yield findStation(part.destination.id))) { console.error('unknown station', part.destination.id, part.destination.name) } @@ -127,6 +145,7 @@ test('Berlin Jungfernheide to Torfstraße 17', co.wrap(function* (t) { const part = journey.parts[journey.parts.length - 1] assertValidStation(t, part.origin) + assertValidStationProducts(t, part.origin.products) if (!(yield findStation(part.origin.id))) { console.error('unknown station', part.origin.id, part.origin.name) } @@ -155,6 +174,7 @@ test('Berlin Jungfernheide to ATZE Musiktheater', co.wrap(function* (t) { const part = journey.parts[journey.parts.length - 1] assertValidStation(t, part.origin) + assertValidStationProducts(t, part.origin.products) if (!(yield findStation(part.origin.id))) { console.error('unknown station', part.origin.id, part.origin.name) } @@ -179,6 +199,7 @@ test('departures at Berlin Jungfernheide', co.wrap(function* (t) { t.ok(Array.isArray(deps)) for (let dep of deps) { assertValidStation(t, dep.station) + assertValidStationProducts(t, dep.station.products) if (!(yield findStation(dep.station.id))) { console.error('unknown station', dep.station.id, dep.station.name) } diff --git a/test/vbb.js b/test/vbb.js index 53b8de79..7e039dde 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -21,6 +21,17 @@ const { assertValidWhen } = require('./util') +const assertValidStationProducts = (t, p) => { + t.ok(p) + t.equal(typeof p.suburban, 'boolean') + t.equal(typeof p.subway, 'boolean') + t.equal(typeof p.tram, 'boolean') + t.equal(typeof p.bus, 'boolean') + t.equal(typeof p.ferry, 'boolean') + t.equal(typeof p.express, 'boolean') + t.equal(typeof p.regional, 'boolean') +} + // todo const findStation = (query) => stations(query, true, false) @@ -41,11 +52,13 @@ test('journeys – station to station', co.wrap(function* (t) { for (let journey of journeys) { assertValidStation(t, journey.origin) + assertValidStationProducts(t, journey.origin.products) t.ok(journey.origin.name.indexOf('(Berlin)') === -1) t.strictEqual(journey.origin.id, spichernstr) assertValidWhen(t, journey.departure) assertValidStation(t, journey.destination) + assertValidStationProducts(t, journey.destination.products) t.strictEqual(journey.destination.id, amrumerStr) assertValidWhen(t, journey.arrival) @@ -56,11 +69,13 @@ test('journeys – station to station', co.wrap(function* (t) { t.equal(typeof part.id, 'string') t.ok(part.id) assertValidStation(t, part.origin) + assertValidStationProducts(t, part.origin.products) t.ok(part.origin.name.indexOf('(Berlin)') === -1) t.strictEqual(part.origin.id, spichernstr) assertValidWhen(t, part.departure) assertValidStation(t, part.destination) + assertValidStationProducts(t, part.destination.products) t.strictEqual(part.destination.id, amrumerStr) assertValidWhen(t, part.arrival) @@ -161,6 +176,7 @@ test('journeys – station to address', co.wrap(function* (t) { const part = journey.parts[journey.parts.length - 1] assertValidStation(t, part.origin) + assertValidStationProducts(t, part.origin.products) assertValidWhen(t, part.departure) const dest = part.destination @@ -187,6 +203,7 @@ test('journeys – station to POI', co.wrap(function* (t) { const part = journey.parts[journey.parts.length - 1] assertValidStation(t, part.origin) + assertValidStationProducts(t, part.origin.products) assertValidWhen(t, part.departure) const dest = part.destination @@ -212,6 +229,7 @@ test('departures', co.wrap(function* (t) { t.equal(dep.station.name, 'U Spichernstr.') assertValidStation(t, dep.station) + assertValidStationProducts(t, dep.station.products) t.strictEqual(dep.station.id, spichernstr) assertValidWhen(t, dep.when) @@ -311,8 +329,10 @@ test('radar', co.wrap(function* (t) { t.ok(Array.isArray(v.frames)) for (let f of v.frames) { assertValidStation(t, f.origin, true) + assertValidStationProducts(t, f.origin.products) t.strictEqual(f.origin.name.indexOf('(Berlin)'), -1) assertValidStation(t, f.destination, true) + assertValidStationProducts(t, f.destination.products) t.strictEqual(f.destination.name.indexOf('(Berlin)'), -1) t.equal(typeof f.t, 'number') } From 275c9e54d75ce4217dcd094a6e4881758ba6851f Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 11 Dec 2017 14:51:09 +0100 Subject: [PATCH 34/73] more line info --- p/vbb/index.js | 8 ++++++++ parse/line.js | 5 +---- test/vbb.js | 11 ++++++++++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/p/vbb/index.js b/p/vbb/index.js index 4e0ddd72..3b750d81 100644 --- a/p/vbb/index.js +++ b/p/vbb/index.js @@ -2,6 +2,7 @@ const shorten = require('vbb-short-station-name') const {to12Digit, to9Digit} = require('vbb-translate-ids') +const parseLineName = require('vbb-parse-line') const _formatStation = require('../../format/station') const _parseLine = require('../../parse/line') @@ -34,6 +35,13 @@ const parseLine = (profile, l) => { } } + const details = parseLineName(l.name) + res.symbol = details.symbol + res.nr = details.nr + res.metro = details.metro + res.express = details.express + res.night = details.night + return res } diff --git a/parse/line.js b/parse/line.js index 01e7954d..af999382 100644 --- a/parse/line.js +++ b/parse/line.js @@ -12,10 +12,7 @@ const parseLine = (profile, p) => { } if (p.cls) res.class = p.cls - if (p.prodCtx) { - res.productCode = +p.prodCtx.catCode - res.productName = p.prodCtx.catOutS - } + if (p.prodCtx) res.productCode = +p.prodCtx.catCode // todo: parse mode, remove from profiles diff --git a/test/vbb.js b/test/vbb.js index 7e039dde..9a367c7d 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -15,7 +15,7 @@ const { assertValidPoi, assertValidAddress, assertValidLocation, - assertValidLine, + assertValidLine: _assertValidLine, assertValidStopover, hour, when, assertValidWhen @@ -32,6 +32,15 @@ const assertValidStationProducts = (t, p) => { t.equal(typeof p.regional, 'boolean') } +const assertValidLine = (t, l) => { + _assertValidLine(t, l) + if (l.symbol !== null) t.equal(typeof l.symbol, 'string') + if (l.nr !== null) t.equal(typeof l.nr, 'number') + if (l.metro !== null) t.equal(typeof l.metro, 'boolean') + if (l.express !== null) t.equal(typeof l.express, 'boolean') + if (l.night !== null) t.equal(typeof l.night, 'boolean') +} + // todo const findStation = (query) => stations(query, true, false) From 3ea4fd3e40b0dd9368feaf34ecd39b9a33b96c9e Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 11 Dec 2017 15:20:50 +0100 Subject: [PATCH 35/73] bugfixes :bug:, rename dep.ref -> dep.journeyId --- parse/date-time.js | 4 ++-- parse/departure.js | 4 ++-- test/db.js | 2 +- test/vbb.js | 14 ++++++++++---- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/parse/date-time.js b/parse/date-time.js index 7625d0a6..4c913e1c 100644 --- a/parse/date-time.js +++ b/parse/date-time.js @@ -2,14 +2,14 @@ const moment = require('moment-timezone') -const parseDateTime = (profile, date, time) => { +const parseDateTime = (timezone, date, time) => { let offset = 0 // in days if (time.length > 6) { offset = +time.slice(0, -6) time = time.slice(-6) } - return moment.tz(date + 'T' + time, profile.timezone) + return moment.tz(date + 'T' + time, timezone) .add(offset, 'days') } diff --git a/parse/departure.js b/parse/departure.js index ba897cf5..42978a35 100644 --- a/parse/departure.js +++ b/parse/departure.js @@ -14,10 +14,10 @@ const createParseDeparture = (profile, stations, lines, remarks) => { const parseDeparture = (d) => { const when = profile.parseDateTime(tz, d.date, d.stbStop.dTimeR || d.stbStop.dTimeS) const res = { - ref: d.jid, + journeyId: d.jid, station: stations[parseInt(d.stbStop.locX)] || null, when: when.format(), - direction: d.dirTxt, + direction: profile.parseStationName(d.dirTxt), line: lines[parseInt(d.prodX)] || null, remarks: d.remL ? d.remL.map(findRemark) : [], trip: +d.jid.split('|')[1] // todo: this seems brittle diff --git a/test/db.js b/test/db.js index 76ec2132..25cc61f0 100644 --- a/test/db.js +++ b/test/db.js @@ -16,7 +16,7 @@ const { assertValidLocation, assertValidLine, assertValidStopover, - when, isValidWhen + when, isValidWhen // todo: timezone } = require('./util.js') const assertValidStationProducts = (t, p) => { diff --git a/test/vbb.js b/test/vbb.js index 9a367c7d..4c5fbb00 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -6,21 +6,27 @@ const stations = require('vbb-stations-autocomplete') const tapePromise = require('tape-promise').default const tape = require('tape') const co = require('co') +const shorten = require('vbb-short-station-name') const createClient = require('..') const vbbProfile = require('../p/vbb') const modes = require('../p/vbb/modes') const { - assertValidStation, assertValidFrameStation, + assertValidStation: _assertValidStation, assertValidPoi, assertValidAddress, assertValidLocation, assertValidLine: _assertValidLine, assertValidStopover, hour, when, - assertValidWhen + assertValidWhen // todo: timezone } = require('./util') +const assertValidStation = (t, s, coordsOptional = false) => { + _assertValidStation(t, s, coordsOptional) + t.equal(s.name, shorten(s.name)) +} + const assertValidStationProducts = (t, p) => { t.ok(p) t.equal(typeof p.suburban, 'boolean') @@ -233,8 +239,8 @@ test('departures', co.wrap(function* (t) { t.ok(Array.isArray(deps)) t.deepEqual(deps, deps.sort((a, b) => t.when > b.when)) for (let dep of deps) { - t.equal(typeof dep.ref, 'string') - t.ok(dep.ref) + t.equal(typeof dep.journeyId, 'string') + t.ok(dep.journeyId) t.equal(dep.station.name, 'U Spichernstr.') assertValidStation(t, dep.station) From 36825d3e2a4676b4999cde26775980ff990618a9 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 11 Dec 2017 15:41:27 +0100 Subject: [PATCH 36/73] VBB: parse tickets, change client ID ported over from derhuerst/vbb-hafas@74680b7 --- example.js | 2 +- index.js | 3 ++- p/vbb/index.js | 39 ++++++++++++++++++++++++++++++++++++--- package.json | 1 + test/util.js | 34 +++++++++++++++++++++++++++++++++- test/vbb.js | 6 +++++- 6 files changed, 78 insertions(+), 7 deletions(-) diff --git a/example.js b/example.js index 57d13694..ed52a3fa 100644 --- a/example.js +++ b/example.js @@ -6,7 +6,7 @@ const dbProfile = require('./p/db') const client = createClient(dbProfile) // Berlin Jungfernheide to München Hbf -client.journeys('8011167', '8000261', {results: 1}) +client.journeys('8011167', '8000261', {results: 1, tickets: true}) // client.departures('8011167', {duration: 1}) // client.locations('Berlin Jungfernheide') // client.locations('ATZE Musiktheater', {poi: true, addressses: false, fuzzy: false}) diff --git a/index.js b/index.js index 21ac30bb..69ff8c21 100644 --- a/index.js +++ b/index.js @@ -57,6 +57,7 @@ const createClient = (profile) => { // todo: does this work with every endpoint? accessibility: 'none', // 'none', 'partial' or 'complete' bike: false, // only bike-friendly journeys + tickets: false, // return tickets? }, opt) if (opt.via) opt.via = profile.formatLocation(profile, opt.via) opt.when = opt.when || new Date() @@ -78,7 +79,7 @@ const createClient = (profile) => { // todo: what are all these for? getPT: true, outFrwd: true, - getTariff: false, + getTariff: !!opt.tickets, getIV: false, // walk & bike as alternatives? getPolyline: false // shape for displaying on a map? }, opt) diff --git a/p/vbb/index.js b/p/vbb/index.js index 3b750d81..ea9753fa 100644 --- a/p/vbb/index.js +++ b/p/vbb/index.js @@ -3,10 +3,12 @@ const shorten = require('vbb-short-station-name') const {to12Digit, to9Digit} = require('vbb-translate-ids') const parseLineName = require('vbb-parse-line') +const parseTicket = require('vbb-parse-ticket') -const _formatStation = require('../../format/station') const _parseLine = require('../../parse/line') const _parseLocation = require('../../parse/location') +const _createParseJourney = require('../../parse/journey') +const _formatStation = require('../../format/station') const createParseBitmask = require('../../parse/products-bitmask') const createFormatBitmask = require('../../format/products-bitmask') @@ -15,8 +17,8 @@ const modes = require('./modes') const formatBitmask = createFormatBitmask(modes) const transformReqBody = (body) => { - body.client = {type: 'IPA', id: 'BVG'} - body.ext = 'VBB.2' + body.client = {type: 'IPA', id: 'VBB', name: 'vbbPROD', v: '4010300'} + body.ext = 'VBB.1' body.ver = '1.11' body.auth = {type: 'AID', aid: 'hafas-vbb-apps'} @@ -58,6 +60,36 @@ const parseLocation = (profile, l) => { return res } +const createParseJourney = (profile, stations, lines, remarks) => { + const parseJourney = _createParseJourney(profile, stations, lines, remarks) + + const parseJourneyWithTickets = (j) => { + const res = parseJourney(j) + + if ( + j.trfRes && + Array.isArray(j.trfRes.fareSetL) && + j.trfRes.fareSetL[0] && + Array.isArray(j.trfRes.fareSetL[0].fareL) + ) { + res.tickets = [] + const sets = j.trfRes.fareSetL[0].fareL + for (let s of sets) { + if (!Array.isArray(s.ticketL) || s.ticketL.length === 0) continue + for (let t of s.ticketL) { + const ticket = parseTicket(t) + ticket.name = s.name + ' – ' + ticket.name + res.tickets.push(ticket) + } + } + } + + return res + } + + return parseJourneyWithTickets +} + const isIBNR = /^\d{9,}$/ const formatStation = (id) => { if (!isIBNR.test(id)) throw new Error('station ID must be an IBNR.') @@ -92,6 +124,7 @@ const vbbProfile = { parseLocation, parseLine, parseProducts: createParseBitmask(modes.bitmasks), + parseJourney: createParseJourney, formatStation, formatProducts, diff --git a/package.json b/package.json index 6f570e43..fdb0a12b 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "pinkie-promise": "^2.0.1", "query-string": "^5.0.0", "slugg": "^1.2.0", + "vbb-parse-ticket": "^0.2.1", "vbb-short-station-name": "^0.4.0", "vbb-translate-ids": "^3.1.0" }, diff --git a/test/util.js b/test/util.js index 323a185e..a8cda88e 100644 --- a/test/util.js +++ b/test/util.js @@ -97,6 +97,37 @@ const assertValidWhen = (t, w) => { t.ok(isValidWhen(w), 'invalid when') } +const assertValidTicket = (t, ti) => { + t.strictEqual(typeof ti.name, 'string') + t.ok(ti.name.length > 0) + if (ti.price !== null) { + t.strictEqual(typeof ti.price, 'number') + t.ok(ti.price > 0) + } + if (ti.amount !== null) { + t.strictEqual(typeof ti.amount, 'number') + t.ok(ti.amount > 0) + } + + if ('bike' in ti) t.strictEqual(typeof ti.bike, 'boolean') + if ('shortTrip' in ti) t.strictEqual(typeof ti.shortTrip, 'boolean') + if ('group' in ti) t.strictEqual(typeof ti.group, 'boolean') + if ('fullDay' in ti) t.strictEqual(typeof ti.fullDay, 'boolean') + + if (ti.tariff !== null) { + t.strictEqual(typeof ti.tariff, 'string') + t.ok(ti.tariff.length > 0) + } + if (ti.coverage !== null) { + t.strictEqual(typeof ti.coverage, 'string') + t.ok(ti.coverage.length > 0) + } + if (ti.variant !== null) { + t.strictEqual(typeof ti.variant, 'string') + t.ok(ti.variant.length > 0) + } +} + module.exports = { assertValidStation, assertValidPoi, @@ -106,5 +137,6 @@ module.exports = { assertValidLine, isValidDateTime, assertValidStopover, - hour, when, isValidWhen, assertValidWhen + hour, when, isValidWhen, assertValidWhen, + assertValidTicket } diff --git a/test/vbb.js b/test/vbb.js index 4c5fbb00..546f0633 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -19,7 +19,8 @@ const { assertValidLine: _assertValidLine, assertValidStopover, hour, when, - assertValidWhen // todo: timezone + assertValidWhen, // todo: timezone + assertValidTicket } = require('./util') const assertValidStation = (t, s, coordsOptional = false) => { @@ -100,6 +101,9 @@ test('journeys – station to station', co.wrap(function* (t) { t.ok(Array.isArray(part.passed)) for (let passed of part.passed) assertValidStopover(t, passed) + + t.ok(Array.isArray(journey.tickets)) + for (let ticket of journey.tickets) assertValidTicket(t, ticket) } t.end() })) From cd809d272603b80667ebf7d5bd514cdbce29ddae Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 11 Dec 2017 15:49:58 +0100 Subject: [PATCH 37/73] =?UTF-8?q?fix=20build=20=F0=9F=92=9A,=20add=20todo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- p/vbb/index.js | 2 +- test/vbb.js | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/p/vbb/index.js b/p/vbb/index.js index ea9753fa..355f1189 100644 --- a/p/vbb/index.js +++ b/p/vbb/index.js @@ -19,7 +19,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' + body.ver = '1.11' // todo: 1.16 with `mic` and `mac` query params body.auth = {type: 'AID', aid: 'hafas-vbb-apps'} return body diff --git a/test/vbb.js b/test/vbb.js index 546f0633..695a9199 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -102,8 +102,11 @@ test('journeys – station to station', co.wrap(function* (t) { t.ok(Array.isArray(part.passed)) for (let passed of part.passed) assertValidStopover(t, passed) - t.ok(Array.isArray(journey.tickets)) - for (let ticket of journey.tickets) assertValidTicket(t, ticket) + // todo: find a journey where there ticket info is always available + if (journey.tickets) { + t.ok(Array.isArray(journey.tickets)) + for (let ticket of journey.tickets) assertValidTicket(t, ticket) + } } t.end() })) From 3c9c8433dc22eae512713ea3ad6859146f980481 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 11 Dec 2017 16:06:37 +0100 Subject: [PATCH 38/73] DB: find cheapest price --- p/db/index.js | 40 +++++++++++++++++++++++++++++++++++++++- test/db.js | 14 ++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/p/db/index.js b/p/db/index.js index c68ea256..d87fbd23 100644 --- a/p/db/index.js +++ b/p/db/index.js @@ -2,8 +2,9 @@ const crypto = require('crypto') -const _formatStation = require('../../format/station') const _parseLine = require('../../parse/line') +const _createParseJourney = require('../../parse/journey') +const _formatStation = require('../../format/station') const createParseBitmask = require('../../parse/products-bitmask') const createFormatBitmask = require('../../format/products-bitmask') const {accessibility, bike} = require('../../format/filters') @@ -69,6 +70,42 @@ const parseLine = (profile, l) => { return res } +const createParseJourney = (profile, stations, lines, remarks) => { + const parseJourney = _createParseJourney(profile, stations, lines, remarks) + + const parseJourneyWithPrice = (j) => { + const res = parseJourney(j) + + // todo: find cheapest, find discounts + // todo: write a parser like vbb-parse-ticket + // [ { + // prc: 15000, + // isFromPrice: true, + // isBookable: true, + // isUpsell: false, + // targetCtx: 'D', + // buttonText: 'To offer selection' + // } ] + res.price = {amount: null, hint: 'No pricing information available.'} + if ( + j.trfRes && + Array.isArray(j.trfRes.fareSetL) && + j.trfRes.fareSetL[0] && + Array.isArray(j.trfRes.fareSetL[0].fareL) && + j.trfRes.fareSetL[0].fareL[0] + ) { + const tariff = j.trfRes.fareSetL[0].fareL[0] + if (tariff.prc >= 0) { // wat + res.price = {amount: tariff.prc / 100, hint: null} + } + } + + return res + } + + return parseJourneyWithPrice +} + const isIBNR = /^\d{6,}$/ const formatStation = (id) => { if (!isIBNR.test(id)) throw new Error('station ID must be an IBNR.') @@ -107,6 +144,7 @@ const dbProfile = { // todo: parseLocation parseLine, parseProducts: createParseBitmask(modes.bitmasks), + parseJourney: createParseJourney, formatStation, formatProducts diff --git a/test/db.js b/test/db.js index 25cc61f0..aa4a9c12 100644 --- a/test/db.js +++ b/test/db.js @@ -73,6 +73,18 @@ const assertValidProducts = (t, p) => { } } +const assertValidPrice = (t, p) => { + t.ok(p) + if (p.amount !== null) { + t.equal(typeof p.amount, 'number') + t.ok(p.amount > 0) + } + if (p.hint !== null) { + t.equal(typeof p.hint, 'string') + t.ok(p.hint) + } +} + const test = tapePromise(tape) const client = createClient(dbProfile) @@ -128,6 +140,8 @@ test('Berlin Jungfernheide to München Hbf', co.wrap(function* (t) { t.ok(Array.isArray(part.passed)) for (let stopover of part.passed) assertValidStopover(t, stopover) + + if (journey.price) assertValidPrice(t, journey.price) } t.end() From 130ffeccf9108b7141765138a7580d43b639b3c1 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 11 Dec 2017 16:33:17 +0100 Subject: [PATCH 39/73] =?UTF-8?q?fix=20tests=20point=20in=20time=20?= =?UTF-8?q?=F0=9F=92=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- test/util.js | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index fdb0a12b..e50170d0 100644 --- a/package.json +++ b/package.json @@ -40,8 +40,8 @@ "devDependencies": { "co": "^4.6.0", "db-stations": "^1.25.0", - "floordate": "^3.0.0", "is-roughly-equal": "^0.1.0", + "luxon": "^0.2.7", "tap-spec": "^4.1.1", "tape": "^4.8.0", "tape-promise": "^2.0.1", diff --git a/test/util.js b/test/util.js index a8cda88e..2d7acaf2 100644 --- a/test/util.js +++ b/test/util.js @@ -1,7 +1,7 @@ 'use strict' const isRoughlyEqual = require('is-roughly-equal') -const floor = require('floordate') +const {DateTime} = require('luxon') const assertValidStation = (t, s, coordsOptional = false) => { t.equal(typeof s.type, 'string') @@ -80,13 +80,12 @@ const assertValidStopover = (t, s, coordsOptional = false) => { assertValidStation(t, s.station, coordsOptional) } -const minute = 60 * 1000 -const hour = 60 * minute -const day = 24 * hour -const week = 7 * day +const hour = 60 * 60 * 1000 +const week = 7 * 24 * hour -// next Monday -const when = new Date(+floor(new Date(), 'week') + week + 10 * hour) +// next Monday 10 am +const dt = DateTime.local().startOf('week').plus({weeks: 1, hours: 10}) +const when = new Date(dt.toISO()) const isValidWhen = (w) => { const ts = +new Date(w) if (Number.isNaN(ts)) return false From 14cdc77c0f2de61f6249facb95e2362e6967a0c5 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 11 Dec 2017 17:21:50 +0100 Subject: [PATCH 40/73] moment-timezone -> luxon, parse DateTimes with locale --- format/date.js | 7 +++++-- format/time.js | 7 +++++-- p/db/index.js | 1 + p/vbb/index.js | 1 + package.json | 3 +-- parse/date-time.js | 28 ++++++++++++++++++++-------- parse/departure.js | 7 +++---- parse/journey-part.js | 26 ++++++++++++-------------- parse/journey.js | 1 - parse/movement.js | 6 ++---- parse/stopover.js | 6 ++---- 11 files changed, 52 insertions(+), 41 deletions(-) diff --git a/format/date.js b/format/date.js index 37e910fa..45091a34 100644 --- a/format/date.js +++ b/format/date.js @@ -1,9 +1,12 @@ 'use strict' -const moment = require('moment-timezone') +const {DateTime} = require('luxon') const formatDate = (profile, when) => { - return moment(when).tz(profile.timezone).format('YYYYMMDD') + return DateTime.fromMillis(+when, { + locale: profile.locale, + zone: profile.timezone + }).toFormat('yyyyMMdd') } module.exports = formatDate diff --git a/format/time.js b/format/time.js index 348f402d..d16b2e8c 100644 --- a/format/time.js +++ b/format/time.js @@ -1,9 +1,12 @@ 'use strict' -const moment = require('moment-timezone') +const {DateTime} = require('luxon') const formatTime = (profile, when) => { - return moment(when).tz(profile.timezone).format('HHmmss') + return DateTime.fromMillis(+when, { + locale: profile.locale, + zone: profile.timezone + }).toFormat('HHmmss') } module.exports = formatTime diff --git a/p/db/index.js b/p/db/index.js index d87fbd23..02210b9d 100644 --- a/p/db/index.js +++ b/p/db/index.js @@ -135,6 +135,7 @@ const formatProducts = (products) => { // todo: find option for absolute number of results const dbProfile = { + locale: 'de-DE', timezone: 'Europe/Berlin', endpoint: 'https://reiseauskunft.bahn.de/bin/mgate.exe', transformReqBody, diff --git a/p/vbb/index.js b/p/vbb/index.js index 355f1189..9cb44899 100644 --- a/p/vbb/index.js +++ b/p/vbb/index.js @@ -116,6 +116,7 @@ const formatProducts = (products) => { } const vbbProfile = { + locale: 'de-DE', timezone: 'Europe/Berlin', endpoint: 'https://fahrinfo.vbb.de/bin/mgate.exe', transformReqBody, diff --git a/package.json b/package.json index e50170d0..546888c9 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "dependencies": { "fetch-ponyfill": "^4.1.0", "lodash": "^4.17.4", - "moment-timezone": "^0.5.13", + "luxon": "^0.2.7", "pinkie-promise": "^2.0.1", "query-string": "^5.0.0", "slugg": "^1.2.0", @@ -41,7 +41,6 @@ "co": "^4.6.0", "db-stations": "^1.25.0", "is-roughly-equal": "^0.1.0", - "luxon": "^0.2.7", "tap-spec": "^4.1.1", "tape": "^4.8.0", "tape-promise": "^2.0.1", diff --git a/parse/date-time.js b/parse/date-time.js index 4c913e1c..4ff6900d 100644 --- a/parse/date-time.js +++ b/parse/date-time.js @@ -1,16 +1,28 @@ 'use strict' -const moment = require('moment-timezone') +const {DateTime} = require('luxon') -const parseDateTime = (timezone, date, time) => { - let offset = 0 // in days - if (time.length > 6) { - offset = +time.slice(0, -6) - time = time.slice(-6) +const validDate = /^(\d{4})-(\d{2})-(\d{2})$/ + +const parseDateTime = (profile, date, time) => { + const pDate = [date.substr(-8, 4), date.substr(-4, 2), date.substr(-2, 2)] + if (!pDate[0] || !pDate[1] || !pDate[2]) { + throw new Error('invalid date format: ' + date) } - return moment.tz(date + 'T' + time, timezone) - .add(offset, 'days') + const pTime = [time.substr(-6, 2), time.substr(-4, 2), time.substr(-2, 2)] + if (!pTime[0] || !pTime[1] || !pTime[2]) { + throw new Error('invalid time format: ' + time) + } + + const offset = time.length > 6 ? parseInt(time.slice(0, -6)) : 0 + + console.error(pDate, pTime, offset) + const dt = DateTime.fromISO(pDate.join('-') + 'T' + pTime.join(':'), { + locale: profile.locale, + zone: profile.timezone + }) + return offset > 0 ? dt.plus({days: offset}) : dt } module.exports = parseDateTime diff --git a/parse/departure.js b/parse/departure.js index 42978a35..7ded8c06 100644 --- a/parse/departure.js +++ b/parse/departure.js @@ -8,11 +8,10 @@ // todo: d.freq, d.freq.jnyL, see https://github.com/derhuerst/hafas-client/blob/9203ed1481f08baacca41ac5e3c19bf022f01b0b/parse.js#L115 const createParseDeparture = (profile, stations, lines, remarks) => { - const tz = profile.timezone const findRemark = rm => remarks[parseInt(rm.remX)] || null const parseDeparture = (d) => { - const when = profile.parseDateTime(tz, d.date, d.stbStop.dTimeR || d.stbStop.dTimeS) + const when = profile.parseDateTime(profile, d.date, d.stbStop.dTimeR || d.stbStop.dTimeS) const res = { journeyId: d.jid, station: stations[parseInt(d.stbStop.locX)] || null, @@ -24,8 +23,8 @@ const createParseDeparture = (profile, stations, lines, remarks) => { } if (d.stbStop.dTimeR && d.stbStop.dTimeS) { - const realtime = profile.parseDateTime(tz, d.date, d.stbStop.dTimeR) - const planned = profile.parseDateTime(tz, d.date, d.stbStop.dTimeS) + const realtime = profile.parseDateTime(profile, d.date, d.stbStop.dTimeR) + const planned = profile.parseDateTime(profile, d.date, d.stbStop.dTimeS) res.delay = Math.round((realtime - planned) / 1000) } else res.delay = null diff --git a/parse/journey-part.js b/parse/journey-part.js index f2483277..a2468067 100644 --- a/parse/journey-part.js +++ b/parse/journey-part.js @@ -5,19 +5,17 @@ const parseDateTime = require('./date-time') const clone = obj => Object.assign({}, obj) const createParseJourneyPart = (profile, stations, lines, remarks) => { - const tz = profile.timezone - const parseStopover = (j, st) => { const res = { station: stations[parseInt(st.locX)] } if (st.aTimeR || st.aTimeS) { - const arr = parseDateTime(tz, j.date, st.aTimeR || st.aTimeS) - res.arrival = arr.format() + const arr = parseDateTime(profile, j.date, st.aTimeR || st.aTimeS) + res.arrival = arr.toISO() } if (st.dTimeR || st.dTimeS) { - const dep = parseDateTime(tz, j.date, st.dTimeR || st.dTimeS) - res.departure = dep.format() + const dep = parseDateTime(profile, j.date, st.dTimeR || st.dTimeS) + res.departure = dep.toISO() } return res @@ -31,18 +29,18 @@ const createParseJourneyPart = (profile, stations, lines, remarks) => { // todo: what is pt.jny.dirFlg? // todo: how does pt.freq work? const parseJourneyPart = (j, pt) => { // j = journey, pt = part - const dep = profile.parseDateTime(tz, j.date, pt.dep.dTimeR || pt.dep.dTimeS) - const arr = profile.parseDateTime(tz, j.date, pt.arr.aTimeR || pt.arr.aTimeS) + const dep = profile.parseDateTime(profile, j.date, pt.dep.dTimeR || pt.dep.dTimeS) + const arr = profile.parseDateTime(profile, j.date, pt.arr.aTimeR || pt.arr.aTimeS) const res = { origin: clone(stations[parseInt(pt.dep.locX)]) || null, destination: clone(stations[parseInt(pt.arr.locX)]), - departure: dep.format(), - arrival: arr.format() + departure: dep.toISO(), + arrival: arr.toISO() } if (pt.dep.dTimeR && pt.dep.dTimeS) { - const realtime = profile.parseDateTime(tz, j.date, pt.dep.dTimeR) - const planned = profile.parseDateTime(tz, j.date, pt.dep.dTimeS) + const realtime = profile.parseDateTime(profile, j.date, pt.dep.dTimeR) + const planned = profile.parseDateTime(profile, j.date, pt.dep.dTimeS) res.delay = Math.round((realtime - planned) / 1000) } @@ -66,10 +64,10 @@ const createParseJourneyPart = (profile, stations, lines, remarks) => { if (pt.jny.freq && pt.jny.freq.jnyL) { const parseAlternative = (a) => { // todo: realtime - const when = profile.parseDateTime(tz, j.date, a.stopL[0].dTimeS) + const when = profile.parseDateTime(profile, j.date, a.stopL[0].dTimeS) return { line: lines[parseInt(a.prodX)] || null, - when: when.format() + when: when.toISO() } } res.alternatives = pt.jny.freq.jnyL diff --git a/parse/journey.js b/parse/journey.js index 21d0f124..7e66d5d3 100644 --- a/parse/journey.js +++ b/parse/journey.js @@ -1,6 +1,5 @@ 'use strict' -const parseDateTime = require('./date-time') const createParseJourneyPart = require('./journey-part') const clone = obj => Object.assign({}, obj) diff --git a/parse/movement.js b/parse/movement.js index 67e3d591..0cd4d1ba 100644 --- a/parse/movement.js +++ b/parse/movement.js @@ -1,8 +1,6 @@ 'use strict' const createParseMovement = (profile, locations, lines, remarks) => { - const tz = profile.timezone - // todo: what is m.dirGeo? maybe the speed? // todo: what is m.stopL? // todo: what is m.proc? wut? @@ -13,10 +11,10 @@ const createParseMovement = (profile, locations, lines, remarks) => { const parseMovement = (m) => { const parseNextStop = (s) => { const dep = s.dTimeR || s.dTimeS - ? profile.parseDateTime(tz, m.date, s.dTimeR || s.dTimeS) + ? profile.parseDateTime(profile, m.date, s.dTimeR || s.dTimeS) : null const arr = s.aTimeR || s.aTimeS - ? profile.parseDateTime(tz, m.date, s.aTimeR || s.aTimeS) + ? profile.parseDateTime(profile, m.date, s.aTimeR || s.aTimeS) : null return { diff --git a/parse/stopover.js b/parse/stopover.js index 09b6a80b..3a0e05d8 100644 --- a/parse/stopover.js +++ b/parse/stopover.js @@ -1,18 +1,16 @@ 'use strict' const createParseStopover = (profile, stations, lines, remarks, connection) => { - const tz = profile.timezone - const parseStopover = (st) => { const res = { station: stations[parseInt(st.locX)] || null } if (st.aTimeR || st.aTimeS) { - const arr = profile.parseDateTime(tz, connection.date, st.aTimeR || st.aTimeS) + const arr = profile.parseDateTime(profile, connection.date, st.aTimeR || st.aTimeS) res.arrival = arr.format() } if (st.dTimeR || st.dTimeS) { - const dep = profile.parseDateTime(tz, connection.date, st.dTimeR || st.dTimeS) + const dep = profile.parseDateTime(profile, connection.date, st.dTimeR || st.dTimeS) res.departure = dep.format() } return res From 3811b4553cc57f609c905ec57ee6a595863e8506 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 11 Dec 2017 19:25:29 +0100 Subject: [PATCH 41/73] FPTF location objects see public-transport/friendly-public-transport-format@6481dee --- package.json | 1 + parse/location.js | 34 ++++++++++++++---------- parse/movement.js | 3 ++- test/db.js | 20 +++++++------- test/util.js | 66 +++++++++++++++++++++++++---------------------- test/vbb.js | 20 +++++++------- 6 files changed, 78 insertions(+), 66 deletions(-) diff --git a/package.json b/package.json index 546888c9..9dd80ca3 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "devDependencies": { "co": "^4.6.0", "db-stations": "^1.25.0", + "is-coordinates": "^2.0.2", "is-roughly-equal": "^0.1.0", "tap-spec": "^4.1.1", "tape": "^4.8.0", diff --git a/parse/location.js b/parse/location.js index f692af91..1b7be2f3 100644 --- a/parse/location.js +++ b/parse/location.js @@ -1,25 +1,31 @@ 'use strict' -const types = Object.create(null) -types.P = 'poi' -types.S = 'station' -types.A = 'address' +const POI = 'P' +const STATION = 'S' +const ADDRESS = 'A' // todo: what is s.rRefL? // todo: is passing in profile necessary? const parseLocation = (profile, l) => { - const type = types[l.type] || 'unknown' - const res = { - type, - name: l.name, - coordinates: l.crd ? { - latitude: l.crd.y / 1000000, - longitude: l.crd.x / 1000000 - } : null + const res = {type: 'location'} + if (l.crd) { + res.latitude = l.crd.y / 1000000 + res.longitude = l.crd.x / 1000000 } - if (type === 'poi' || type === 'station') res.id = l.extId - if ('pCls' in l) res.products = profile.parseProducts(l.pCls) + if (l.type === STATION) { + const station = { + type: 'station', + id: l.extId, + location: res + } + if ('pCls' in l) station.products = profile.parseProducts(l.pCls) + return station + } + + if (type === POI) res.id = l.extId + else if (l.type === ADDRESS) res.address = l.name + else res.name = l.name return res } diff --git a/parse/movement.js b/parse/movement.js index 0cd4d1ba..f5b032ef 100644 --- a/parse/movement.js +++ b/parse/movement.js @@ -27,7 +27,8 @@ const createParseMovement = (profile, locations, lines, remarks) => { const res = { direction: profile.parseStationName(m.dirTxt), line: lines[m.prodX] || null, - coordinates: m.pos ? { + location: m.pos ? { + type: 'location', latitude: m.pos.y / 1000000, longitude: m.pos.x / 1000000 } : null, diff --git a/test/db.js b/test/db.js index aa4a9c12..b9634e45 100644 --- a/test/db.js +++ b/test/db.js @@ -53,18 +53,18 @@ const isJungfernheide = (s) => { return s.type === 'station' && (s.id === '008011167' || s.id === '8011167') && s.name === 'Berlin Jungfernheide' && - s.coordinates && - isRoughlyEqual(s.coordinates.latitude, 52.530408, .0005) && - isRoughlyEqual(s.coordinates.longitude, 13.299424, .0005) + s.location && + isRoughlyEqual(s.location.latitude, 52.530408, .0005) && + isRoughlyEqual(s.location.longitude, 13.299424, .0005) } const assertIsJungfernheide = (t, s) => { t.equal(s.type, 'station') t.ok(s.id === '008011167' || s.id === '8011167', 'id should be 8011167') t.equal(s.name, 'Berlin Jungfernheide') - t.ok(s.coordinates) - t.ok(isRoughlyEqual(s.coordinates.latitude, 52.530408, .0005)) - t.ok(isRoughlyEqual(s.coordinates.longitude, 13.299424, .0005)) + t.ok(s.location) + t.ok(isRoughlyEqual(s.location.latitude, 52.530408, .0005)) + t.ok(isRoughlyEqual(s.location.longitude, 13.299424, .0005)) } const assertValidProducts = (t, p) => { @@ -170,8 +170,8 @@ test('Berlin Jungfernheide to Torfstraße 17', co.wrap(function* (t) { const d = part.destination assertValidAddress(t, d) t.equal(d.name, 'Torfstraße 17') - t.ok(isRoughlyEqual(.0001, d.coordinates.latitude, 52.5416823)) - t.ok(isRoughlyEqual(.0001, d.coordinates.longitude, 13.3491223)) + t.ok(isRoughlyEqual(.0001, d.latitude, 52.5416823)) + t.ok(isRoughlyEqual(.0001, d.longitude, 13.3491223)) t.end() })) @@ -199,8 +199,8 @@ test('Berlin Jungfernheide to ATZE Musiktheater', co.wrap(function* (t) { const d = part.destination assertValidPoi(t, d) t.equal(d.name, 'ATZE Musiktheater') - t.ok(isRoughlyEqual(.0001, d.coordinates.latitude, 52.542399)) - t.ok(isRoughlyEqual(.0001, d.coordinates.longitude, 13.350402)) + t.ok(isRoughlyEqual(.0001, d.latitude, 52.542399)) + t.ok(isRoughlyEqual(.0001, d.longitude, 13.350402)) t.end() })) diff --git a/test/util.js b/test/util.js index 2d7acaf2..1d9b2047 100644 --- a/test/util.js +++ b/test/util.js @@ -2,53 +2,57 @@ const isRoughlyEqual = require('is-roughly-equal') const {DateTime} = require('luxon') +const isValidWGS84 = require('is-coordinates') const assertValidStation = (t, s, coordsOptional = false) => { - t.equal(typeof s.type, 'string') t.equal(s.type, 'station') t.equal(typeof s.id, 'string') - + t.ok(s.id) t.equal(typeof s.name, 'string') - if (!coordsOptional) { - if (!s.coordinates) console.trace() - t.ok(s.coordinates) - } - if (s.coordinates) { - t.equal(typeof s.coordinates.latitude, 'number') - t.equal(typeof s.coordinates.longitude, 'number') + t.ok(s.name) + + if (!coordsOptional || (s.location !== null && s.location !== undefined)) { + t.ok(s.location) + assertValidLocation(t, s.location, coordsOptional) } } const assertValidPoi = (t, p) => { - t.equal(typeof p.type, 'string') - t.equal(p.type, 'poi') t.equal(typeof p.id, 'string') - t.equal(typeof p.name, 'string') - t.ok(p.coordinates) - if (p.coordinates) { - t.equal(typeof p.coordinates.latitude, 'number') - t.equal(typeof p.coordinates.longitude, 'number') - } + t.equal(typeof a.address, 'string') // todo: do POIs always have an address? + assertValidLocation(t, a, true) // todo: do POIs always have coords? } const assertValidAddress = (t, a) => { - t.equal(typeof a.type, 'string') - t.equal(a.type, 'address') - - t.equal(typeof a.name, 'string') - t.ok(a.coordinates) - if (a.coordinates) { - t.equal(typeof a.coordinates.latitude, 'number') - t.equal(typeof a.coordinates.longitude, 'number') - } + t.equal(typeof a.address, 'string') + assertValidLocation(t, a, true) // todo: do addresses always have coords? } -const assertValidLocation = (t, l) => { - if (l.type === 'station') assertValidStation(t, l) - else if (l.type === 'poi') assertValidPoi(t, l) - else if (l.type === 'address') assertValidAddress(t, l) - else t.fail('invalid type ' + l.type) +const assertValidLocation = (t, l, coordsOptional = false) => { + t.equal(l.type, 'location') + if (l.name !== null && l.name !== undefined) { + t.equal(typeof l.name, 'string') + t.ok(l.name) + } + + if (l.address !== null && l.address !== undefined) { + t.equal(typeof l.address, 'string') + t.ok(l.address) + } + + const hasLatitude = l.latitude !== null && l.latitude !== undefined + const hasLongitude = l.longitude !== null && l.longitude !== undefined + if (!coordsOptional && hasLatitude) t.equal(typeof l.latitude, 'number') + if (!coordsOptional && hasLongitude) t.equal(typeof l.longitude, 'number') + if ((hasLongitude && !hasLatitude) || (hasLatitude && !hasLongitude)) { + t.fail('should have both .latitude and .longitude') + } + if (hasLatitude && hasLongitude) isValidWGS84([l.longitude, l.latitude]) + + if (!coordsOptional && l.altitude !== null && l.altitude !== undefined) { + t.equal(typeof l.altitude, 'number') + } } const isValidMode = (m) => { diff --git a/test/vbb.js b/test/vbb.js index 695a9199..2bbf1ac7 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -204,8 +204,8 @@ test('journeys – station to address', co.wrap(function* (t) { const dest = part.destination assertValidAddress(t, dest) t.strictEqual(dest.name, 'Torfstr. 17') - t.ok(isRoughlyEqual(.0001, dest.coordinates.latitude, 52.5416823)) - t.ok(isRoughlyEqual(.0001, dest.coordinates.longitude, 13.3491223)) + t.ok(isRoughlyEqual(.0001, dest.latitude, 52.5416823)) + t.ok(isRoughlyEqual(.0001, dest.longitude, 13.3491223)) assertValidWhen(t, part.arrival) t.end() @@ -231,8 +231,8 @@ test('journeys – station to POI', co.wrap(function* (t) { const dest = part.destination assertValidPoi(t, dest) t.strictEqual(dest.name, 'ATZE Musiktheater') - t.ok(isRoughlyEqual(.0001, dest.coordinates.latitude, 52.543333)) - t.ok(isRoughlyEqual(.0001, dest.coordinates.longitude, 13.351686)) + t.ok(isRoughlyEqual(.0001, dest.latitude, 52.543333)) + t.ok(isRoughlyEqual(.0001, dest.longitude, 13.351686)) assertValidWhen(t, part.arrival) t.end() @@ -322,12 +322,12 @@ test('radar', co.wrap(function* (t) { t.ok(findStation(v.direction)) assertValidLine(t, v.line) - t.equal(typeof v.coordinates.latitude, 'number') - t.ok(v.coordinates.latitude <= 55, 'vehicle is too far away') - t.ok(v.coordinates.latitude >= 45, 'vehicle is too far away') - t.equal(typeof v.coordinates.longitude, 'number') - t.ok(v.coordinates.longitude >= 9, 'vehicle is too far away') - t.ok(v.coordinates.longitude <= 15, 'vehicle is too far away') + t.equal(typeof v.location.latitude, 'number') + t.ok(v.location.latitude <= 55, 'vehicle is too far away') + t.ok(v.location.latitude >= 45, 'vehicle is too far away') + t.equal(typeof v.location.longitude, 'number') + t.ok(v.location.longitude >= 9, 'vehicle is too far away') + t.ok(v.location.longitude <= 15, 'vehicle is too far away') t.ok(Array.isArray(v.nextStops)) for (let st of v.nextStops) { From 608a91989fda5602b40f8373ecc12f531ecc93c2 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 11 Dec 2017 19:40:46 +0100 Subject: [PATCH 42/73] bugfixes :bug:, even more todos --- p/db/index.js | 2 ++ p/vbb/index.js | 4 +--- parse/date-time.js | 1 - parse/location.js | 3 ++- parse/movement.js | 4 ++-- parse/stopover.js | 4 ++-- test/db.js | 4 +++- test/util.js | 1 + test/vbb.js | 2 +- 9 files changed, 14 insertions(+), 11 deletions(-) diff --git a/p/db/index.js b/p/db/index.js index 02210b9d..9217e7a2 100644 --- a/p/db/index.js +++ b/p/db/index.js @@ -73,6 +73,8 @@ const parseLine = (profile, l) => { const createParseJourney = (profile, stations, lines, remarks) => { const parseJourney = _createParseJourney(profile, stations, lines, remarks) + // todo: j.sotRating, j.conSubscr, j.isSotCon, j.showARSLink, k.sotCtxt + // todo: j.conSubscr, j.showARSLink, j.useableTime const parseJourneyWithPrice = (j) => { const res = parseJourney(j) diff --git a/p/vbb/index.js b/p/vbb/index.js index 9cb44899..9fe551b6 100644 --- a/p/vbb/index.js +++ b/p/vbb/index.js @@ -50,10 +50,8 @@ const parseLine = (profile, l) => { const parseLocation = (profile, l) => { const res = _parseLocation(profile, l) - // todo: shorten has been made for stations, not any type of location - res.name = shorten(res.name) - if (res.type === 'station') { + res.name = shorten(res.name) res.id = to12Digit(res.id) // todo: https://github.com/derhuerst/vbb-hafas/blob/1c64bfe42422e2648b21016d233c808460250308/lib/parse.js#L67-L75 } diff --git a/parse/date-time.js b/parse/date-time.js index 4ff6900d..78635354 100644 --- a/parse/date-time.js +++ b/parse/date-time.js @@ -17,7 +17,6 @@ const parseDateTime = (profile, date, time) => { const offset = time.length > 6 ? parseInt(time.slice(0, -6)) : 0 - console.error(pDate, pTime, offset) const dt = DateTime.fromISO(pDate.join('-') + 'T' + pTime.join(':'), { locale: profile.locale, zone: profile.timezone diff --git a/parse/location.js b/parse/location.js index 1b7be2f3..1c8d37b7 100644 --- a/parse/location.js +++ b/parse/location.js @@ -17,13 +17,14 @@ const parseLocation = (profile, l) => { const station = { type: 'station', id: l.extId, + name: l.name, location: res } if ('pCls' in l) station.products = profile.parseProducts(l.pCls) return station } - if (type === POI) res.id = l.extId + if (l.type === POI) res.id = l.extId else if (l.type === ADDRESS) res.address = l.name else res.name = l.name diff --git a/parse/movement.js b/parse/movement.js index f5b032ef..2fe3d132 100644 --- a/parse/movement.js +++ b/parse/movement.js @@ -19,8 +19,8 @@ const createParseMovement = (profile, locations, lines, remarks) => { return { station: locations[s.locX], - departure: dep ? dep.format() : null, - arrival: arr ? arr.format() : null + departure: dep ? dep.toISO() : null, + arrival: arr ? arr.toISO() : null } } diff --git a/parse/stopover.js b/parse/stopover.js index 3a0e05d8..d7a3d24f 100644 --- a/parse/stopover.js +++ b/parse/stopover.js @@ -7,11 +7,11 @@ const createParseStopover = (profile, stations, lines, remarks, connection) => { } if (st.aTimeR || st.aTimeS) { const arr = profile.parseDateTime(profile, connection.date, st.aTimeR || st.aTimeS) - res.arrival = arr.format() + res.arrival = arr.toISO() } if (st.dTimeR || st.dTimeS) { const dep = profile.parseDateTime(profile, connection.date, st.dTimeR || st.dTimeS) - res.departure = dep.format() + res.departure = dep.toISO() } return res } diff --git a/test/db.js b/test/db.js index b9634e45..a40a6ad3 100644 --- a/test/db.js +++ b/test/db.js @@ -67,6 +67,8 @@ const assertIsJungfernheide = (t, s) => { t.ok(isRoughlyEqual(s.location.longitude, 13.299424, .0005)) } +// todo: this doesnt seem to work +// todo: DRY with assertValidStationProducts const assertValidProducts = (t, p) => { for (let k of Object.keys(modes)) { t.ok('boolean', typeof modes[k], 'mode ' + k + ' must be a boolean') @@ -169,7 +171,7 @@ test('Berlin Jungfernheide to Torfstraße 17', co.wrap(function* (t) { const d = part.destination assertValidAddress(t, d) - t.equal(d.name, 'Torfstraße 17') + t.equal(d.address, 'Torfstraße 17') t.ok(isRoughlyEqual(.0001, d.latitude, 52.5416823)) t.ok(isRoughlyEqual(.0001, d.longitude, 13.3491223)) diff --git a/test/util.js b/test/util.js index 1d9b2047..d7b73170 100644 --- a/test/util.js +++ b/test/util.js @@ -55,6 +55,7 @@ const assertValidLocation = (t, l, coordsOptional = false) => { } } +// todo: https://github.com/public-transport/friendly-public-transport-format/tree/babf2b82947ab0e655a4a0e1cbee6b5519af9172/spec#modes const isValidMode = (m) => { return m === 'walking' || m === 'train' || diff --git a/test/vbb.js b/test/vbb.js index 2bbf1ac7..86369d34 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -203,7 +203,7 @@ test('journeys – station to address', co.wrap(function* (t) { const dest = part.destination assertValidAddress(t, dest) - t.strictEqual(dest.name, 'Torfstr. 17') + t.strictEqual(dest.address, 'Torfstraße 17') t.ok(isRoughlyEqual(.0001, dest.latitude, 52.5416823)) t.ok(isRoughlyEqual(.0001, dest.longitude, 13.3491223)) assertValidWhen(t, part.arrival) From a2fa56edc1faccc1cf466eb413837737ef819df6 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 11 Dec 2017 19:53:26 +0100 Subject: [PATCH 43/73] more bugfixes :bug: --- parse/departure.js | 2 +- parse/location.js | 4 ++-- test/db.js | 10 +++++++++- test/util.js | 7 +++++-- test/vbb.js | 16 +++++++++++----- 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/parse/departure.js b/parse/departure.js index 7ded8c06..c9d6c298 100644 --- a/parse/departure.js +++ b/parse/departure.js @@ -15,7 +15,7 @@ const createParseDeparture = (profile, stations, lines, remarks) => { const res = { journeyId: d.jid, station: stations[parseInt(d.stbStop.locX)] || null, - when: when.format(), + when: when.toISO(), direction: profile.parseStationName(d.dirTxt), line: lines[parseInt(d.prodX)] || null, remarks: d.remL ? d.remL.map(findRemark) : [], diff --git a/parse/location.js b/parse/location.js index 1c8d37b7..b776b920 100644 --- a/parse/location.js +++ b/parse/location.js @@ -24,9 +24,9 @@ const parseLocation = (profile, l) => { return station } - if (l.type === POI) res.id = l.extId - else if (l.type === ADDRESS) res.address = l.name + if (l.type === ADDRESS) res.address = l.name else res.name = l.name + if (l.type === POI) res.id = l.extId return res } diff --git a/test/db.js b/test/db.js index a40a6ad3..340373f2 100644 --- a/test/db.js +++ b/test/db.js @@ -238,6 +238,11 @@ test('nearby Berlin Jungfernheide', co.wrap(function* (t) { t.ok(nearby[0].distance >= 0) t.ok(nearby[0].distance <= 100) + for (let n of nearby) { + if (n.type === 'station') assertValidStation(t, n) + else assertValidLocation(t, n) + } + t.end() })) @@ -250,7 +255,10 @@ test('locations named Jungfernheide', co.wrap(function* (t) { t.ok(locations.length > 0) t.ok(locations.length <= 10) - for (let location of locations) assertValidLocation(t, location) + for (let l of locations) { + if (l.type === 'station') assertValidStation(t, l) + else assertValidLocation(t, l) + } t.ok(locations.some(isJungfernheide)) t.end() diff --git a/test/util.js b/test/util.js index d7b73170..343a1f36 100644 --- a/test/util.js +++ b/test/util.js @@ -20,8 +20,11 @@ const assertValidStation = (t, s, coordsOptional = false) => { const assertValidPoi = (t, p) => { t.equal(typeof p.id, 'string') t.equal(typeof p.name, 'string') - t.equal(typeof a.address, 'string') // todo: do POIs always have an address? - assertValidLocation(t, a, true) // todo: do POIs always have coords? + if (p.address !== null && p.address !== undefined) { + t.equal(typeof p.address, 'string') + t.ok(p.address) + } + assertValidLocation(t, p, true) // todo: do POIs always have coords? } const assertValidAddress = (t, a) => { diff --git a/test/vbb.js b/test/vbb.js index 86369d34..04682321 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -277,7 +277,10 @@ test('nearby', co.wrap(function* (t) { const nearby = yield client.nearby(52.4873452, 13.3310411, {distance: 200}) t.ok(Array.isArray(nearby)) - for (let n of nearby) assertValidLocation(t, n, false) + for (let n of nearby) { + if (n.type === 'station') assertValidStation(t, n) + else assertValidLocation(t, n, false) + } t.equal(nearby[0].id, '900000044201') t.equal(nearby[0].name, 'U Berliner Str.') @@ -300,10 +303,13 @@ test('locations', co.wrap(function* (t) { t.ok(Array.isArray(locations)) t.ok(locations.length > 0) t.ok(locations.length <= 10) - for (let l of locations) assertValidLocation(t, l) - t.ok(locations.find((s) => s.type === 'station')) - t.ok(locations.find((s) => s.type === 'poi')) - t.ok(locations.find((s) => s.type === 'address')) + for (let l of locations) { + if (l.type === 'station') assertValidStation(t, l) + else assertValidLocation(t, l) + } + t.ok(locations.find(s => s.type === 'station')) + t.ok(locations.find(s => s.id && s.name)) // POIs + t.ok(locations.find(s => !s.name && s.address)) // addresses t.end() })) From eacbd8ef01eb875ceb7b76ed926b8b8e20d82b9c Mon Sep 17 00:00:00 2001 From: Jannis R Date: Tue, 12 Dec 2017 03:21:12 +0100 Subject: [PATCH 44/73] validate profile --- index.js | 5 ++--- lib/default-profile.js | 3 --- lib/validate-profile.js | 43 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 lib/validate-profile.js diff --git a/index.js b/index.js index 69ff8c21..d60098dc 100644 --- a/index.js +++ b/index.js @@ -3,14 +3,13 @@ const minBy = require('lodash/minBy') const maxBy = require('lodash/maxBy') +const validateProfile = require('./lib/validate-profile') const defaultProfile = require('./lib/default-profile') const request = require('./lib/request') const createClient = (profile) => { profile = Object.assign({}, defaultProfile, profile) - if ('string' !== typeof profile.timezone) { - throw new Error('profile.timezone must be a string.') - } + validateProfile(profile) const departures = (station, opt = {}) => { if ('string' !== typeof station) throw new Error('station must be a string.') diff --git a/lib/default-profile.js b/lib/default-profile.js index 8d673536..418be358 100644 --- a/lib/default-profile.js +++ b/lib/default-profile.js @@ -15,7 +15,6 @@ const parseStopover = require('../parse/stopover') const formatAddress = require('../format/address') const formatCoord = require('../format/coord') const formatDate = require('../format/date') -const filters = require('../format/filters') const formatLocationFilter = require('../format/location-filter') const formatPoi = require('../format/poi') const formatStation = require('../format/station') @@ -25,7 +24,6 @@ const formatRectangle = require('../format/rectangle') const id = x => x -// todo: find out which are actually necessary const defaultProfile = { transformReqBody: id, transformReq: id, @@ -48,7 +46,6 @@ const defaultProfile = { formatAddress, formatCoord, formatDate, - filters, formatLocationFilter, formatPoi, formatStation, diff --git a/lib/validate-profile.js b/lib/validate-profile.js new file mode 100644 index 00000000..7d8b595b --- /dev/null +++ b/lib/validate-profile.js @@ -0,0 +1,43 @@ +'use strict' + +const types = { + locale: 'string', + timezone: 'string', + transformReq: 'function', + transformReqBody: 'function', + transformJourneysQuery: 'function', + + parseDateTime: 'function', + parseDeparture: 'function', + parseJourneyPart: 'function', + parseJourney: 'function', + parseLine: 'function', + parseStationName: 'function', + parseLocation: 'function', + parseMovement: 'function', + parseNearby: 'function', + parseOperator: 'function', + parseRemark: 'function', + parseStopover: 'function', + + formatAddress: 'function', + formatCoord: 'function', + formatDate: 'function', + formatLocationFilter: 'function', + formatPoi: 'function', + formatStation: 'function', + formatTime: 'function', + formatLocation: 'function', + formatRectangle: 'function' +} + +const validateProfile = (profile) => { + for (let key of Object.keys(types)) { + const type = types[key] + if (type !== typeof profile[key]) { + throw new Error(`profile.${key} must be a ${type}.`) + } + } +} + +module.exports = validateProfile From 81c9411cfea122dd081b414988f8d25ff8a0352b Mon Sep 17 00:00:00 2001 From: Jannis R Date: Tue, 12 Dec 2017 03:28:54 +0100 Subject: [PATCH 45/73] deal with some todos, add more --- index.js | 17 ++++++++--------- lib/validate-profile.js | 5 +++++ p/db/index.js | 2 ++ p/db/modes.js | 13 +++++++++++++ p/vbb/index.js | 24 +++++++++++++++++++++++- p/vbb/modes.js | 10 ++++++++++ package.json | 1 + parse/journey-part.js | 26 +++++++------------------- parse/line.js | 5 ++--- test/util.js | 18 +++++++----------- 10 files changed, 78 insertions(+), 43 deletions(-) diff --git a/index.js b/index.js index d60098dc..4e4eaad5 100644 --- a/index.js +++ b/index.js @@ -73,14 +73,13 @@ const createClient = (profile) => { viaLocL: opt.via ? [opt.via] : null, arrLocL: [to], jnyFltrL: [products], + getTariff: !!opt.tickets, // todo: what is req.gisFltrL? - // todo: what are all these for? - getPT: true, - outFrwd: true, - getTariff: !!opt.tickets, - getIV: false, // walk & bike as alternatives? - getPolyline: false // shape for displaying on a map? + 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, { @@ -193,7 +192,8 @@ const createClient = (profile) => { opt = Object.assign({ results: 256, // maximum number of vehicles duration: 30, // compute frames for the next n seconds - frames: 3 // nr of frames to compute + frames: 3, // nr of frames to compute + products: null // optionally an object of booleans }, opt || {}) opt.when = opt.when || new Date() @@ -211,8 +211,7 @@ const createClient = (profile) => { perStep: Math.round(durationPerStep), ageOfReport: true, // todo: what is this? jnyFltrL: [ - // todo: use `profile.formatProducts(opt.products || {})` - {type: 'PROD', mode: 'INC', value: '127'} + profile.formatProducts(opt.products || {}) ], trainPosMode: 'CALC' // todo: what is this? what about realtime? } diff --git a/lib/validate-profile.js b/lib/validate-profile.js index 7d8b595b..7d875c74 100644 --- a/lib/validate-profile.js +++ b/lib/validate-profile.js @@ -7,6 +7,8 @@ const types = { transformReqBody: 'function', transformJourneysQuery: 'function', + products: 'object', + parseDateTime: 'function', parseDeparture: 'function', parseJourneyPart: 'function', @@ -37,6 +39,9 @@ const validateProfile = (profile) => { if (type !== typeof profile[key]) { throw new Error(`profile.${key} must be a ${type}.`) } + if (type === 'object' && profile[key] === null) { + throw new Error(`profile.${key} must not be null.`) + } } } diff --git a/p/db/index.js b/p/db/index.js index 9217e7a2..d42a7b97 100644 --- a/p/db/index.js +++ b/p/db/index.js @@ -144,6 +144,8 @@ const dbProfile = { transformReq, transformJourneysQuery, + products: modes.allProducts, + // todo: parseLocation parseLine, parseProducts: createParseBitmask(modes.bitmasks), diff --git a/p/db/modes.js b/p/db/modes.js index 9e91d505..7ee4b8f5 100644 --- a/p/db/modes.js +++ b/p/db/modes.js @@ -91,4 +91,17 @@ m.bitmasks[128] = m.subway m.bitmasks[256] = m.tram m.bitmasks[512] = m.taxi +m.allProducts = [ + m.nationalExp, + m.national, + m.regionalExp, + m.regional, + m.suburban, + m.bus, + m.ferry, + m.subway, + m.tram, + m.taxi +] + module.exports = m diff --git a/p/vbb/index.js b/p/vbb/index.js index 9fe551b6..700c9226 100644 --- a/p/vbb/index.js +++ b/p/vbb/index.js @@ -4,10 +4,12 @@ const shorten = require('vbb-short-station-name') const {to12Digit, to9Digit} = require('vbb-translate-ids') const parseLineName = require('vbb-parse-line') const parseTicket = require('vbb-parse-ticket') +const getStations = require('vbb-stations') const _parseLine = require('../../parse/line') const _parseLocation = require('../../parse/location') const _createParseJourney = require('../../parse/journey') +const _createParseStopover = require('../../parse/stopover') const _formatStation = require('../../format/station') const createParseBitmask = require('../../parse/products-bitmask') const createFormatBitmask = require('../../format/products-bitmask') @@ -53,7 +55,10 @@ const parseLocation = (profile, l) => { if (res.type === 'station') { res.name = shorten(res.name) res.id = to12Digit(res.id) - // todo: https://github.com/derhuerst/vbb-hafas/blob/1c64bfe42422e2648b21016d233c808460250308/lib/parse.js#L67-L75 + if (!res.location.latitude || !res.location.longitude) { + const [s] = getStations(res.id) + if (s) Object.assign(res.location, s.coordinates) + } } return res } @@ -88,6 +93,20 @@ const createParseJourney = (profile, stations, lines, remarks) => { return parseJourneyWithTickets } +const createParseStopover = (profile, stations, lines, remarks, connection) => { + const parseStopover = _createParseStopover(profile, stations, lines, remarks, connection) + + const parseStopoverWithShorten = (st) => { + const res = parseStopover(st) + if (res.station && res.station.name) { + res.station.name = shorten(res.station.name) + } + return res + } + + return parseStopoverWithShorten +} + const isIBNR = /^\d{9,}$/ const formatStation = (id) => { if (!isIBNR.test(id)) throw new Error('station ID must be an IBNR.') @@ -119,11 +138,14 @@ const vbbProfile = { endpoint: 'https://fahrinfo.vbb.de/bin/mgate.exe', transformReqBody, + products: modes.allProducts, + parseStationName: shorten, parseLocation, parseLine, parseProducts: createParseBitmask(modes.bitmasks), parseJourney: createParseJourney, + parseStopover: createParseStopover, formatStation, formatProducts, diff --git a/p/vbb/modes.js b/p/vbb/modes.js index 65adc5be..388462b3 100644 --- a/p/vbb/modes.js +++ b/p/vbb/modes.js @@ -95,6 +95,16 @@ m.categories = [ m.unknown ] +m.allProducts = [ + m.suburban, + m.subway, + m.tram, + m.bus, + m.ferry, + m.express, + m.regional +] + // m.parseCategory = (category) => { // return m.categories[parseInt(category)] || m.unknown // } diff --git a/package.json b/package.json index 9dd80ca3..fe28f309 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "slugg": "^1.2.0", "vbb-parse-ticket": "^0.2.1", "vbb-short-station-name": "^0.4.0", + "vbb-stations": "^5.8.0", "vbb-translate-ids": "^3.1.0" }, "devDependencies": { diff --git a/parse/journey-part.js b/parse/journey-part.js index a2468067..198c5fc6 100644 --- a/parse/journey-part.js +++ b/parse/journey-part.js @@ -5,22 +5,6 @@ const parseDateTime = require('./date-time') const clone = obj => Object.assign({}, obj) const createParseJourneyPart = (profile, stations, lines, remarks) => { - const parseStopover = (j, st) => { - const res = { - station: stations[parseInt(st.locX)] - } - if (st.aTimeR || st.aTimeS) { - const arr = parseDateTime(profile, j.date, st.aTimeR || st.aTimeS) - res.arrival = arr.toISO() - } - if (st.dTimeR || st.dTimeS) { - const dep = parseDateTime(profile, j.date, st.dTimeR || st.dTimeS) - res.departure = dep.toISO() - } - - return res - } - // todo: finish parse/remark.js first const applyRemark = (j, rm) => {} @@ -28,6 +12,7 @@ const createParseJourneyPart = (profile, stations, lines, remarks) => { // todo: pt.dep.dProgType, pt.arr.dProgType // todo: what is pt.jny.dirFlg? // todo: how does pt.freq work? + // todo: what is pt.himL? const parseJourneyPart = (j, pt) => { // j = journey, pt = part const dep = profile.parseDateTime(profile, j.date, pt.dep.dTimeR || pt.dep.dTimeS) const arr = profile.parseDateTime(profile, j.date, pt.arr.aTimeR || pt.arr.aTimeS) @@ -46,7 +31,9 @@ const createParseJourneyPart = (profile, stations, lines, remarks) => { if (pt.type === 'WALK') { res.mode = 'walking' + res.public = true } else if (pt.type === 'JNY') { + // todo: pull `public` value from `profile.products` res.id = pt.jny.jid res.line = lines[parseInt(pt.jny.prodX)] || null res.direction = profile.parseStationName(pt.jny.dirTxt) @@ -55,7 +42,8 @@ const createParseJourneyPart = (profile, stations, lines, remarks) => { if (pt.arr.aPlatfS) res.arrivalPlatform = pt.arr.aPlatfS if (pt.jny.stopL) { - res.passed = pt.jny.stopL.map(stopover => parseStopover(j, stopover)) + const parse = profile.parseStopover(profile, stations, lines, remarks, j) + res.passed = pt.jny.stopL.map(parse) } if (Array.isArray(pt.jny.remL)) { for (let remark of pt.jny.remL) applyRemark(j, remark) @@ -63,8 +51,8 @@ const createParseJourneyPart = (profile, stations, lines, remarks) => { if (pt.jny.freq && pt.jny.freq.jnyL) { const parseAlternative = (a) => { - // todo: realtime - const when = profile.parseDateTime(profile, j.date, a.stopL[0].dTimeS) + const t = a.stopL[0].dTimeS || a.stopL[0].dTimeR + const when = profile.parseDateTime(profile, j.date, t) return { line: lines[parseInt(a.prodX)] || null, when: when.toISO() diff --git a/parse/line.js b/parse/line.js index af999382..e8163875 100644 --- a/parse/line.js +++ b/parse/line.js @@ -1,8 +1,7 @@ 'use strict' -// todo: what is p.number vs p.line? -// todo: what is p.icoX? -// todo: what is p.oprX? +// todo: are p.number and p.line ever different? +// todo: operator from p.oprX? const parseLine = (profile, p) => { if (!p) return null // todo: handle this upstream const res = { diff --git a/test/util.js b/test/util.js index 343a1f36..ced875da 100644 --- a/test/util.js +++ b/test/util.js @@ -24,12 +24,12 @@ const assertValidPoi = (t, p) => { t.equal(typeof p.address, 'string') t.ok(p.address) } - assertValidLocation(t, p, true) // todo: do POIs always have coords? + assertValidLocation(t, p, true) } const assertValidAddress = (t, a) => { t.equal(typeof a.address, 'string') - assertValidLocation(t, a, true) // todo: do addresses always have coords? + assertValidLocation(t, a, true) } const assertValidLocation = (t, l, coordsOptional = false) => { @@ -58,18 +58,15 @@ const assertValidLocation = (t, l, coordsOptional = false) => { } } -// todo: https://github.com/public-transport/friendly-public-transport-format/tree/babf2b82947ab0e655a4a0e1cbee6b5519af9172/spec#modes -const isValidMode = (m) => { - return m === 'walking' || - m === 'train' || - m === 'bus' || - m === 'ferry' -} +const validLineModes = [ + 'train', 'bus', 'ferry', 'taxi', 'gondola', 'aircraft', + 'car', 'bicycle', 'walking' +] const assertValidLine = (t, l) => { t.equal(l.type, 'line') t.equal(typeof l.name, 'string') - t.ok(isValidMode(l.mode), 'invalid mode ' + l.mode) + t.ok(validLineModes.includes(l.mode), 'invalid mode ' + l.mode) t.equal(typeof l.product, 'string') t.equal(l.public, true) } @@ -140,7 +137,6 @@ module.exports = { assertValidPoi, assertValidAddress, assertValidLocation, - isValidMode, assertValidLine, isValidDateTime, assertValidStopover, From 040ed41de79eea924b9c17c8f000bb66a2df4538 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Tue, 12 Dec 2017 18:31:41 +0100 Subject: [PATCH 46/73] VBB: add (anti)clockwise symbols to Ringbahn --- p/vbb/index.js | 22 ++++++++++++++++++++++ parse/line.js | 4 +++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/p/vbb/index.js b/p/vbb/index.js index 700c9226..fa632345 100644 --- a/p/vbb/index.js +++ b/p/vbb/index.js @@ -10,6 +10,7 @@ const _parseLine = require('../../parse/line') const _parseLocation = require('../../parse/location') const _createParseJourney = require('../../parse/journey') const _createParseStopover = require('../../parse/stopover') +const _createParseDeparture = require('../../parse/departure') const _formatStation = require('../../format/station') const createParseBitmask = require('../../parse/products-bitmask') const createFormatBitmask = require('../../format/products-bitmask') @@ -107,6 +108,26 @@ const createParseStopover = (profile, stations, lines, remarks, connection) => { return parseStopoverWithShorten } +const createParseDeparture = (profile, stations, lines, remarks) => { + const parseDeparture = _createParseDeparture(profile, stations, lines, remarks) + + const ringbahnClockwise = /^ringbahn s\s?41$/i + const ringbahnAnticlockwise = /^ringbahn s\s?42$/i + const parseDepartureRenameRingbahn = (j) => { + const res = parseDeparture(j) + + if (res.line && res.line.product === 'suburban') { + const d = res.direction && res.direction.trim() + if (ringbahnClockwise.test(d)) res.direction = 'Ringbahn S41 ⟳' + else if (ringbahnAnticlockwise.test(d)) res.direction = 'Ringbahn S42 ⟲' + } + + return res + } + + return parseDepartureRenameRingbahn +} + const isIBNR = /^\d{9,}$/ const formatStation = (id) => { if (!isIBNR.test(id)) throw new Error('station ID must be an IBNR.') @@ -145,6 +166,7 @@ const vbbProfile = { parseLine, parseProducts: createParseBitmask(modes.bitmasks), parseJourney: createParseJourney, + parseDeparture: createParseDeparture, parseStopover: createParseStopover, formatStation, diff --git a/parse/line.js b/parse/line.js index e8163875..a2619043 100644 --- a/parse/line.js +++ b/parse/line.js @@ -11,7 +11,9 @@ const parseLine = (profile, p) => { } if (p.cls) res.class = p.cls - if (p.prodCtx) res.productCode = +p.prodCtx.catCode + if (p.prodCtx && p.prodCtx.catCode !== undefined) { + res.productCode = +p.prodCtx.catCode + } // todo: parse mode, remove from profiles From 024aa0e94825bf98d1be3f408ba2621c349a3e62 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Tue, 12 Dec 2017 23:15:06 +0100 Subject: [PATCH 47/73] deal with some todos, add more --- format/address.js | 5 +++-- format/poi.js | 5 +++-- p/db/index.js | 2 +- parse/journey.js | 1 - test/db.js | 2 +- test/util.js | 6 ++++-- test/vbb.js | 2 +- 7 files changed, 13 insertions(+), 10 deletions(-) diff --git a/format/address.js b/format/address.js index b629ecc6..01f90b11 100644 --- a/format/address.js +++ b/format/address.js @@ -3,8 +3,9 @@ const formatCoord = require('./coord') const formatAddress = (a) => { - // todo: type-checking, better error msgs - if (!a.latitude || !a.longitude || !a.name) throw new Error('invalid address.') + if (!a.type !== 'location' || !a.latitude || !a.longitude || !a.address) { + throw new Error('invalid address') + } return { type: 'A', diff --git a/format/poi.js b/format/poi.js index cef0c55a..bb2d137d 100644 --- a/format/poi.js +++ b/format/poi.js @@ -3,8 +3,9 @@ const formatCoord = require('./coord') const formatPoi = (p) => { - // todo: type-checking, better error msgs - if (!p.latitude || !p.longitude || !p.id || !p.name) throw new Error('invalid poi.') + if (!p.type !== 'location' || !p.latitude || !p.longitude || !p.id || !p.name) { + throw new Error('invalid POI') + } return { type: 'P', diff --git a/p/db/index.js b/p/db/index.js index d42a7b97..676dbdcc 100644 --- a/p/db/index.js +++ b/p/db/index.js @@ -42,7 +42,7 @@ const transformJourneysQuery = (query, opt) => { if (opt.bike) filters.push(bike) query.trfReq = { - jnyCl: 2, // todo + jnyCl: opt.firstClass === true ? 1 : 2, tvlrProf: [{ type: 'E', redtnCard: opt.loyaltyCard diff --git a/parse/journey.js b/parse/journey.js index 7e66d5d3..7dc9c737 100644 --- a/parse/journey.js +++ b/parse/journey.js @@ -11,7 +11,6 @@ const createParseJourney = (profile, stations, lines, remarks) => { // todo: c.dep.dProgType, c.arr.dProgType // todo: c.conSubscr // todo: c.trfRes x vbb-parse-ticket - // todo: use computed information from part const parseJourney = (j) => { const parts = j.secL.map(part => parsePart(j, part)) const res = { diff --git a/test/db.js b/test/db.js index 340373f2..9c76627a 100644 --- a/test/db.js +++ b/test/db.js @@ -16,7 +16,7 @@ const { assertValidLocation, assertValidLine, assertValidStopover, - when, isValidWhen // todo: timezone + when, isValidWhen } = require('./util.js') const assertValidStationProducts = (t, p) => { diff --git a/test/util.js b/test/util.js index ced875da..10c8dc52 100644 --- a/test/util.js +++ b/test/util.js @@ -89,8 +89,10 @@ const hour = 60 * 60 * 1000 const week = 7 * 24 * hour // next Monday 10 am -const dt = DateTime.local().startOf('week').plus({weeks: 1, hours: 10}) -const when = new Date(dt.toISO()) +const when = DateTime.fromMillis(Date.now(), { + zone: 'Europe/Berlin', + locale: 'de-DE' +}).startOf('week').plus({weeks: 1, hours: 10}).toJSDate() const isValidWhen = (w) => { const ts = +new Date(w) if (Number.isNaN(ts)) return false diff --git a/test/vbb.js b/test/vbb.js index 04682321..62d18222 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -19,7 +19,7 @@ const { assertValidLine: _assertValidLine, assertValidStopover, hour, when, - assertValidWhen, // todo: timezone + assertValidWhen, assertValidTicket } = require('./util') From 0b37e62e0902a364d0d4977957a4a67dcb534279 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Tue, 12 Dec 2017 23:24:55 +0100 Subject: [PATCH 48/73] fix for 3811b45 :bug: --- test/db.js | 4 ++-- test/vbb.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/db.js b/test/db.js index 9c76627a..35292b12 100644 --- a/test/db.js +++ b/test/db.js @@ -151,7 +151,7 @@ test('Berlin Jungfernheide to München Hbf', co.wrap(function* (t) { test('Berlin Jungfernheide to Torfstraße 17', co.wrap(function* (t) { const journeys = yield client.journeys('8011167', { - type: 'address', name: 'Torfstraße 17', + type: 'location', address: 'Torfstraße 17', latitude: 52.5416823, longitude: 13.3491223 }, {when}) @@ -180,7 +180,7 @@ test('Berlin Jungfernheide to Torfstraße 17', co.wrap(function* (t) { test('Berlin Jungfernheide to ATZE Musiktheater', co.wrap(function* (t) { const journeys = yield client.journeys('8011167', { - type: 'poi', name: 'ATZE Musiktheater', id: '991598902', + type: 'location', id: '991598902', name: 'ATZE Musiktheater', latitude: 52.542417, longitude: 13.350437 }, {when}) diff --git a/test/vbb.js b/test/vbb.js index 62d18222..1726fec8 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -188,7 +188,7 @@ test('journey part details', co.wrap(function* (t) { test('journeys – station to address', co.wrap(function* (t) { const journeys = yield client.journeys(spichernstr, { - type: 'address', name: 'Torfstraße 17', + type: 'location', address: 'Torfstraße 17', latitude: 52.5416823, longitude: 13.3491223 }, {results: 1, when}) @@ -215,7 +215,7 @@ test('journeys – station to address', co.wrap(function* (t) { test('journeys – station to POI', co.wrap(function* (t) { const journeys = yield client.journeys(spichernstr, { - type: 'poi', name: 'ATZE Musiktheater', id: 9980720, + type: 'location', id: '9980720', name: 'ATZE Musiktheater', latitude: 52.543333, longitude: 13.351686 }, {results: 1, when}) From e6f7a095050d0d7e913698432e8d9222f08b74f6 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Tue, 12 Dec 2017 23:40:12 +0100 Subject: [PATCH 49/73] more fixes for 3811b45 :bug: --- format/address.js | 4 ++-- format/location.js | 6 +++--- format/poi.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/format/address.js b/format/address.js index 01f90b11..2774c380 100644 --- a/format/address.js +++ b/format/address.js @@ -3,13 +3,13 @@ const formatCoord = require('./coord') const formatAddress = (a) => { - if (!a.type !== 'location' || !a.latitude || !a.longitude || !a.address) { + if (a.type !== 'location' || !a.latitude || !a.longitude || !a.address) { throw new Error('invalid address') } return { type: 'A', - name: a.name, + name: a.address, crd: { x: formatCoord(a.longitude), y: formatCoord(a.latitude) diff --git a/format/location.js b/format/location.js index 3401e32e..121cd13f 100644 --- a/format/location.js +++ b/format/location.js @@ -2,10 +2,10 @@ const formatLocation = (profile, l) => { if ('string' === typeof l) return profile.formatStation(l) - if ('object' === typeof l) { + if ('object' === typeof l && !Array.isArray(l)) { if (l.type === 'station') return profile.formatStation(l.id) - if (l.type === 'poi') return profile.formatPoi(l) - if (l.type === 'address') return profile.formatAddress(l) + if ('string' === typeof l.id) return profile.formatPoi(l) + if ('string' === typeof l.address) return profile.formatAddress(l) throw new Error('invalid location type: ' + l.type) } throw new Error('valid station, address or poi required.') diff --git a/format/poi.js b/format/poi.js index bb2d137d..3991e4c6 100644 --- a/format/poi.js +++ b/format/poi.js @@ -3,7 +3,7 @@ const formatCoord = require('./coord') const formatPoi = (p) => { - if (!p.type !== 'location' || !p.latitude || !p.longitude || !p.id || !p.name) { + if (p.type !== 'location' || !p.latitude || !p.longitude || !p.id || !p.name) { throw new Error('invalid POI') } From 09fc50f5b37eefacef258ab6752c085fe94955da Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sat, 16 Dec 2017 01:23:11 +0100 Subject: [PATCH 50/73] comply with FPTF 1.0.1, using validate-fptf --- p/db/modes.js | 2 +- p/vbb/modes.js | 2 +- package.json | 1 + parse/line.js | 10 ++++++++++ test/util.js | 29 ++++++++++++++++------------- test/vbb.js | 1 - 6 files changed, 29 insertions(+), 16 deletions(-) diff --git a/p/db/modes.js b/p/db/modes.js index 7ee4b8f5..be2252c3 100644 --- a/p/db/modes.js +++ b/p/db/modes.js @@ -47,7 +47,7 @@ const m = { bitmask: 64, name: 'Ferry', short: 'F', - mode: 'ferry', + mode: 'watercraft', product: 'ferry' }, subway: { diff --git a/p/vbb/modes.js b/p/vbb/modes.js index 388462b3..3e771793 100644 --- a/p/vbb/modes.js +++ b/p/vbb/modes.js @@ -42,7 +42,7 @@ const m = { category: 4, bitmask: 16, name: 'Fähre', - mode: 'ferry', + mode: 'watercraft', short: 'F', product: 'ferry' }, diff --git a/package.json b/package.json index fe28f309..39244a73 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "tap-spec": "^4.1.1", "tape": "^4.8.0", "tape-promise": "^2.0.1", + "validate-fptf": "^1.0.1", "vbb-parse-line": "^0.2.5", "vbb-stations-autocomplete": "^2.9.0" }, diff --git a/parse/line.js b/parse/line.js index a2619043..19f79680 100644 --- a/parse/line.js +++ b/parse/line.js @@ -1,15 +1,25 @@ 'use strict' +const slugg = require('slugg') + // todo: are p.number and p.line ever different? // todo: operator from p.oprX? const parseLine = (profile, p) => { if (!p) return null // todo: handle this upstream const res = { type: 'line', + id: null, name: p.line || p.name, public: true } + // We don't get a proper line id from the API, so we use the trip nr here. + // todo: find a better way + if (p.prodCtx && p.prodCtx.num) res.id = p.prodCtx.num + // This is terrible, but FPTF demands an ID. Let's pray for VBB to expose an ID. + else if (p.line) res.id = slugg(p.line.trim()) + else if (p.name) res.id = slugg(p.name.trim()) + if (p.cls) res.class = p.cls if (p.prodCtx && p.prodCtx.catCode !== undefined) { res.productCode = +p.prodCtx.catCode diff --git a/test/util.js b/test/util.js index 10c8dc52..90c3f69d 100644 --- a/test/util.js +++ b/test/util.js @@ -1,15 +1,20 @@ 'use strict' +const validateFptf = require('validate-fptf') const isRoughlyEqual = require('is-roughly-equal') const {DateTime} = require('luxon') const isValidWGS84 = require('is-coordinates') +const validateFptfWith = (t, item, allowedTypes, name) => { + try { + validateFptf.recurse(allowedTypes, item, name) + } catch (err) { + t.ifError(err) + } +} + const assertValidStation = (t, s, coordsOptional = false) => { - t.equal(s.type, 'station') - t.equal(typeof s.id, 'string') - t.ok(s.id) - t.equal(typeof s.name, 'string') - t.ok(s.name) + validateFptfWith(t, s, ['station'], 'station') if (!coordsOptional || (s.location !== null && s.location !== undefined)) { t.ok(s.location) @@ -18,18 +23,20 @@ const assertValidStation = (t, s, coordsOptional = false) => { } const assertValidPoi = (t, p) => { + assertValidLocation(t, p, true) + t.equal(typeof p.id, 'string') t.equal(typeof p.name, 'string') if (p.address !== null && p.address !== undefined) { t.equal(typeof p.address, 'string') t.ok(p.address) } - assertValidLocation(t, p, true) } const assertValidAddress = (t, a) => { - t.equal(typeof a.address, 'string') assertValidLocation(t, a, true) + + t.equal(typeof a.address, 'string') } const assertValidLocation = (t, l, coordsOptional = false) => { @@ -59,16 +66,12 @@ const assertValidLocation = (t, l, coordsOptional = false) => { } const validLineModes = [ - 'train', 'bus', 'ferry', 'taxi', 'gondola', 'aircraft', + 'train', 'bus', 'watercraft', 'taxi', 'gondola', 'aircraft', 'car', 'bicycle', 'walking' ] const assertValidLine = (t, l) => { - t.equal(l.type, 'line') - t.equal(typeof l.name, 'string') - t.ok(validLineModes.includes(l.mode), 'invalid mode ' + l.mode) - t.equal(typeof l.product, 'string') - t.equal(l.public, true) + validateFptfWith(t, l, ['line'], 'line') } const isValidDateTime = (w) => { diff --git a/test/vbb.js b/test/vbb.js index 1726fec8..a96faedc 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -10,7 +10,6 @@ const shorten = require('vbb-short-station-name') const createClient = require('..') const vbbProfile = require('../p/vbb') -const modes = require('../p/vbb/modes') const { assertValidStation: _assertValidStation, assertValidPoi, From b0157c75d524802adea881f86b605eb4caa8ac09 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sat, 16 Dec 2017 02:28:09 +0100 Subject: [PATCH 51/73] update validate-fptf :bug:, expose movement trip, add more todos --- package.json | 2 +- parse/departure.js | 1 + parse/movement.js | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 39244a73..c2df49f1 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "tap-spec": "^4.1.1", "tape": "^4.8.0", "tape-promise": "^2.0.1", - "validate-fptf": "^1.0.1", + "validate-fptf": "^1.0.2", "vbb-parse-line": "^0.2.5", "vbb-stations-autocomplete": "^2.9.0" }, diff --git a/parse/departure.js b/parse/departure.js index c9d6c298..7946b5b4 100644 --- a/parse/departure.js +++ b/parse/departure.js @@ -21,6 +21,7 @@ const createParseDeparture = (profile, stations, lines, remarks) => { remarks: d.remL ? d.remL.map(findRemark) : [], trip: +d.jid.split('|')[1] // todo: this seems brittle } + // todo: res.trip from rawLine.prodCtx.num if (d.stbStop.dTimeR && d.stbStop.dTimeS) { const realtime = profile.parseDateTime(profile, d.date, d.stbStop.dTimeR) diff --git a/parse/movement.js b/parse/movement.js index 2fe3d132..bf2378ef 100644 --- a/parse/movement.js +++ b/parse/movement.js @@ -26,6 +26,7 @@ const createParseMovement = (profile, locations, lines, remarks) => { const res = { direction: profile.parseStationName(m.dirTxt), + trip: m.jid && +m.jid.split('|')[1] || null, // todo: this seems brittle line: lines[m.prodX] || null, location: m.pos ? { type: 'location', From 595c7458305e3c8494c5c7a5340b1511fc250558 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sat, 16 Dec 2017 03:20:11 +0100 Subject: [PATCH 52/73] VBB: support 7-digit stations --- p/vbb/index.js | 12 +++++++++--- test/vbb.js | 5 ++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/p/vbb/index.js b/p/vbb/index.js index fa632345..a1018930 100644 --- a/p/vbb/index.js +++ b/p/vbb/index.js @@ -128,10 +128,16 @@ const createParseDeparture = (profile, stations, lines, remarks) => { return parseDepartureRenameRingbahn } -const isIBNR = /^\d{9,}$/ +const validIBNR = /^\d+$/ const formatStation = (id) => { - if (!isIBNR.test(id)) throw new Error('station ID must be an IBNR.') - id = to9Digit(id) + if ('string' !== typeof id) throw new Error('station ID must be a string.') + const l = id.length + if ((l !== 7 && l !== 9 && l !== 12) || !validIBNR.test(id)) { + throw new Error('station ID must be a valid IBNR.') + } + // The VBB has some 7-digit stations. We don't convert them to 12 digits, + // because it only recognizes in the 7-digit format. see derhuerst/vbb-hafas#22 + if (l !== 7) id = to9Digit(id) return _formatStation(id) } diff --git a/test/vbb.js b/test/vbb.js index a96faedc..ccf03bb2 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -260,10 +260,9 @@ test('departures', co.wrap(function* (t) { t.end() })) -// todo -test.skip('departures at 7-digit station', co.wrap(function* (t) { +test('departures at 7-digit station', co.wrap(function* (t) { const eisenach = '8010097' // see derhuerst/vbb-hafas#22 - yield client.departures(eisenach, {when}) + await client.departures(eisenach, {when}) t.pass('did not fail') t.end() From e03ad5bf3d5cb3c0b99cc4370721d86ab33a3f71 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sat, 16 Dec 2017 03:22:36 +0100 Subject: [PATCH 53/73] =?UTF-8?q?fix=20build=20=F0=9F=92=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/vbb.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/vbb.js b/test/vbb.js index ccf03bb2..fb0676b5 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -262,7 +262,7 @@ test('departures', co.wrap(function* (t) { test('departures at 7-digit station', co.wrap(function* (t) { const eisenach = '8010097' // see derhuerst/vbb-hafas#22 - await client.departures(eisenach, {when}) + yield client.departures(eisenach, {when}) t.pass('did not fail') t.end() From 1415036e46d3d5ce877978f3f4a577ef976e7ed4 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sat, 16 Dec 2017 07:54:39 +0100 Subject: [PATCH 54/73] add missing dep, expose profile --- index.js | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 4e4eaad5..f57ec057 100644 --- a/index.js +++ b/index.js @@ -227,6 +227,7 @@ const createClient = (profile) => { const client = {departures, journeys, locations, nearby} if (profile.journeyPart) client.journeyPart = journeyPart if (profile.radar) client.radar = radar + Object.defineProperty(client, 'profile', {value: profile}) return client } diff --git a/package.json b/package.json index c2df49f1..e7c2db7b 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "pinkie-promise": "^2.0.1", "query-string": "^5.0.0", "slugg": "^1.2.0", + "vbb-parse-line": "^0.2.5", "vbb-parse-ticket": "^0.2.1", "vbb-short-station-name": "^0.4.0", "vbb-stations": "^5.8.0", @@ -47,7 +48,6 @@ "tape": "^4.8.0", "tape-promise": "^2.0.1", "validate-fptf": "^1.0.2", - "vbb-parse-line": "^0.2.5", "vbb-stations-autocomplete": "^2.9.0" }, "scripts": { From e573ee2842b681423a5cd443c28ff39374faa0c1 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 17 Dec 2017 17:28:08 +0100 Subject: [PATCH 55/73] docs: update FPTF link, minor improvements --- readme.md | 195 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 103 insertions(+), 92 deletions(-) diff --git a/readme.md b/readme.md index 9807b0d3..f6d51d5e 100644 --- a/readme.md +++ b/readme.md @@ -1,9 +1,9 @@ # hafas-client -**A client for HAFAS public transport APIs**. Sort of like [public-transport-enable](https://github.com/schildbach/public-transport-enabler), but with a smaller scope. It also contains customisations for the following transport networks: +**A client for HAFAS public transport APIs**. Sort of like [public-transport-enabler](https://github.com/schildbach/public-transport-enabler), but with a smaller scope. It also contains customisations for the following transport networks: -- [Deutsche Bahn](https://en.wikipedia.org/wiki/Deutsche_Bahn) – [src](p/db/index.js) -- [Berlin public transport](https://en.wikipedia.org/wiki/Verkehrsverbund_Berlin-Brandenburg) – [src](p/vbb/index.js) +- [Deutsche Bahn](https://en.wikipedia.org/wiki/Deutsche_Bahn) – [src at `p/db`](p/db/index.js) +- [Berlin public transport](https://en.wikipedia.org/wiki/Verkehrsverbund_Berlin-Brandenburg) – [src at `p/vbb`](p/vbb/index.js) [![npm version](https://img.shields.io/npm/v/hafas-client.svg)](https://www.npmjs.com/package/hafas-client) [![build status](https://img.shields.io/travis/derhuerst/hafas-client.svg)](https://travis-ci.org/derhuerst/hafas-client) @@ -15,7 +15,7 @@ There's [a company called HaCon](http://hacon.de) that sells [a public transport management system called HAFAS](https://de.wikipedia.org/wiki/HAFAS). It is [used by companies all over Europe](https://gist.github.com/derhuerst/2b7ed83bfa5f115125a5) to serve routing and departure information for apps. All those endpoints are similar, with the same terms and API routes, but have slightly different options, filters and enable features. -`hafas-client` contains all logic for communicating with these, as well as serialising from and parsing to [FPTF](https://github.com/public-transport/friendly-public-transport-format). Endpoint-specific customisations increase the quality of the returned data. +`hafas-client` contains all logic for communicating with these, as well as serialising from and parsing to [*Friendly Public Transport Format (FPTF)* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/blob/1.0.1/spec/readme.md). Endpoint-specific customisations (called "profiles" here) increase the quality of the returned data. ## Installing @@ -31,6 +31,7 @@ npm install hafas-client const createClient = require('hafas-client') const dbProfile = require('hafas-client/p/db') +// create a client with Deutsche Bahn profile const client = createClient(dbProfile) // Berlin Jungfernheide to München Hbf @@ -39,116 +40,126 @@ client.journeys('8011167', '8000261', {results: 1}) .catch(console.error) ``` +The returned [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/promise) will resolved with an array of one `journey` in the [*FPTF* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/blob/1.0.1/spec/readme.md). + ```js [ { - origin: { - type: 'station', - id: '8089100', - name: 'Berlin Jungfernheide (S)', - coordinates: { - latitude: 52.530291, - longitude: 13.299451 - }, - products: { /* … */ } - }, - departure: '2017-11-13T01:00:00Z', - - destination: { - type: 'station', - id: '8000261', - name: 'München Hbf', - coordinates: { - latitude: 48.140364, - longitude: 11.558735 - }, - products: { /* … */ } - }, - arrival: '2017-11-13T09:39:00Z', - parts: [ { - id: '1|1436339|0|80|12112017', - + id: '1|100067|48|81|17122017', origin: { type: 'station', id: '8089100', name: 'Berlin Jungfernheide (S)', - coordinates: { + location: { + type: 'location', latitude: 52.530291, longitude: 13.299451 }, - products: { - nationalExp: false, - national: false, - regionalExp: false, - regional: true, - suburban: true, - bus: true, - ferry: false, - subway: true, - tram: false, - taxi: false - } + products: { /* … */ } }, - departure: '2017-11-13T00:50:00Z', - departurePlatform: '6', - + departure: '2017-12-17T17:05:00.000+01:00', + departurePlatform: '5', destination: { type: 'station', - id: '8089047', - name: 'Berlin Westkreuz', - coordinates: { - latitude: 52.500752, - longitude: 13.283854 - }, - products: { - nationalExp: false, - national: false, - regionalExp: false, - regional: true, - suburban: true, - bus: true, - ferry: false, - subway: false, - tram: false, - taxi: false - } + id: '8089118', + name: 'Berlin Beusselstraße', + location: { /* … */ }, + products: { /* … */ } }, - arrival: '2017-11-13T00:57:00Z', - delay: 0, - + arrival: '2017-12-17T17:08:00.000+01:00', + arrivalPlatform: '1', line: { type: 'line', - name: 'S 42', + id: '41172', + name: 'S 41', + public: true, mode: 'train', product: 'suburban', class: 16, - productCode: 4, - productName: 's' + productCode: 4 }, - direction: 'Ringbahn <-', - arrivalPlatform: '12' + direction: 'Ringbahn ->' + }, /* … */ { + origin: { + type: 'station', + id: '730749', + name: 'Berlin Hauptbahnhof (S+U), Berlin', + location: { + type: 'location', + latitude: 52.526461, + longitude: 13.369378 + }, + products: { /* … */ } + }, + departure: '2017-12-17T17:25:00.000+01:00', + destination: { + type: 'station', + id: '8098160', + name: 'Berlin Hbf (tief)', + location: { /* … */ }, + products: { /* … */ } + }, + arrival: '2017-12-17T17:33:00.000+01:00', + mode: 'walking', + public: true }, { - id: '1|332491|0|80|12112017', - - origin: { /* … */ }, - departure: '2017-11-13T01:05:00Z', - departurePlatform: '3', - - destination: { /* … */ }, - arrival: '2017-11-13T01:18:00Z', - delay: 0, - + id: '1|70906|0|81|17122017', + origin: { + type: 'station', + id: '8098160', + name: 'Berlin Hbf (tief)', + location: { /* … */ }, + products: { /* … */ } + }, + departure: '2017-12-17T17:37:00.000+01:00', + departurePlatform: '1', + destination: { + type: 'station', + id: '8000261', + name: 'München Hbf', + location: { /* … */ }, + products: { /* … */ } + }, + arrival: '2017-12-17T22:45:00.000+01:00', + arrivalPlatform: '13', line: { /* … */ }, - direction: 'Berlin Ostbahnhof' - }, { - origin: { /* … */ }, - departure: '2017-11-13T01:18:00Z', - destination: { /* … */ }, - arrival: '2017-11-13T01:26:00Z', - mode: 'walking' - }, { - /* … */ - } ] + direction: 'München Hbf' + } ], + origin: { + type: 'station', + id: '8089100', + name: 'Berlin Jungfernheide (S)', + location: { + type: 'location', + latitude: 52.530291, + longitude: 13.299451 + }, + products: { + nationalExp: false, + national: false, + regionalExp: false, + regional: true, + suburban: true, + bus: true, + ferry: false, + subway: true, + tram: false, + taxi: false + } + }, + departure: '2017-12-17T17:05:00.000+01:00', + destination: { + type: 'station', + id: '8000261', + name: 'München Hbf', + location: { /* … */ }, + products: { /* … */ } + }, + arrival: '2017-12-17T22:45:00.000+01:00', + price: { + amount: null, + hint: 'No pricing information available.' + } } ] ``` From 2ae8fd2ba0e87ea84e3b51563b00fb14f01d9914 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 17 Dec 2017 17:42:50 +0100 Subject: [PATCH 56/73] docs for VBB profile --- p/vbb/readme.md | 22 ++++++++++++++++++++++ readme.md | 4 ++-- 2 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 p/vbb/readme.md diff --git a/p/vbb/readme.md b/p/vbb/readme.md new file mode 100644 index 00000000..a3ff3d1c --- /dev/null +++ b/p/vbb/readme.md @@ -0,0 +1,22 @@ +# VBB profile for `hafas-client` + +[*Verkehrsverbund Berlin-Brandenburg (VBB)*](https://en.wikipedia.org/wiki/Verkehrsverbund_Berlin-Brandenburg) is a group of public transport companies, running the public transport network in [Berlin](https://en.wikipedia.org/wiki/Berlin). This profile adds *VBB*-specific customizations to `hafas-client`. + +## Usage + +```js +const createClient = require('hafas-client') +const vbbProfile = require('hafas-client/p/vbb') + +// create a client with VBB profile +const client = createClient(vbbProfile) +``` + + +## Customisations + +- parses *VBB*-specific products (such as *X-Bus*) +- strips parts from station names that are unnecessary in the Berlin context +- parses line names to give more information (e.g. "Is it an express bus?") +- parses *VBB*-specific tickets +- renames *Ringbahn* line names to contain `⟳` and `⟲` diff --git a/readme.md b/readme.md index f6d51d5e..0529081f 100644 --- a/readme.md +++ b/readme.md @@ -1,9 +1,9 @@ # hafas-client -**A client for HAFAS public transport APIs**. Sort of like [public-transport-enabler](https://github.com/schildbach/public-transport-enabler), but with a smaller scope. It also contains customisations for the following transport networks: +**A client for HAFAS public transport APIs**. Sort of like [public-transport-enabler](https://github.com/schildbach/public-transport-enabler), but with a smaller scope. It also [contains customisations](p) for the following transport networks: - [Deutsche Bahn](https://en.wikipedia.org/wiki/Deutsche_Bahn) – [src at `p/db`](p/db/index.js) -- [Berlin public transport](https://en.wikipedia.org/wiki/Verkehrsverbund_Berlin-Brandenburg) – [src at `p/vbb`](p/vbb/index.js) +- [Berlin public transport](https://en.wikipedia.org/wiki/Verkehrsverbund_Berlin-Brandenburg) - [docs](p/vbb/readme.md) – [src](p/vbb/index.js) [![npm version](https://img.shields.io/npm/v/hafas-client.svg)](https://www.npmjs.com/package/hafas-client) [![build status](https://img.shields.io/travis/derhuerst/hafas-client.svg)](https://travis-ci.org/derhuerst/hafas-client) From bcddc49049715d754e8e746443dcfd7c04145921 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 17 Dec 2017 17:48:21 +0100 Subject: [PATCH 57/73] docs for DB profile --- p/db/readme.md | 21 +++++++++++++++++++++ readme.md | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 p/db/readme.md diff --git a/p/db/readme.md b/p/db/readme.md new file mode 100644 index 00000000..a94e57c0 --- /dev/null +++ b/p/db/readme.md @@ -0,0 +1,21 @@ +# DB profile for `hafas-client` + +[*Deutsche Bahn (DB)*](https://en.wikipedia.org/wiki/Deutsche_Bahn) is the largest German long-distance public transport company. This profile adds *DB*-specific customizations to `hafas-client`. + +## Usage + +```js +const createClient = require('hafas-client') +const dbProfile = require('hafas-client/p/db') + +// create a client with DB profile +const client = createClient(dbProfile) +``` + + +## Customisations + +- supports 1st and 2nd class with `journey()` +- supports [their loyalty cards](https://en.wikipedia.org/wiki/Deutsche_Bahn#Tickets) with `journey()` +- parses *DB*-specific products (such as *InterCity-Express*) +- exposes the cheapest ticket price for a `journey` diff --git a/readme.md b/readme.md index 0529081f..eaa16015 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ **A client for HAFAS public transport APIs**. Sort of like [public-transport-enabler](https://github.com/schildbach/public-transport-enabler), but with a smaller scope. It also [contains customisations](p) for the following transport networks: -- [Deutsche Bahn](https://en.wikipedia.org/wiki/Deutsche_Bahn) – [src at `p/db`](p/db/index.js) +- [Deutsche Bahn](https://en.wikipedia.org/wiki/Deutsche_Bahn) - [docs](p/db/readme.md) – [src](p/db/index.js) - [Berlin public transport](https://en.wikipedia.org/wiki/Verkehrsverbund_Berlin-Brandenburg) - [docs](p/vbb/readme.md) – [src](p/vbb/index.js) [![npm version](https://img.shields.io/npm/v/hafas-client.svg)](https://www.npmjs.com/package/hafas-client) From 374fd6120a0bcd0e99eccc7496b0ec1b850c8c4a Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 17 Dec 2017 18:16:04 +0100 Subject: [PATCH 58/73] docs: methods, usage examples, more related projects --- docs/departures.md | 155 ++++++++++++++++++++++ docs/journey-part.md | 112 ++++++++++++++++ docs/journeys.md | 236 ++++++++++++++++++++++++++++++++++ docs/locations.md | 66 ++++++++++ docs/nearby.md | 77 +++++++++++ docs/radar.md | 142 ++++++++++++++++++++ example.js => p/db/example.js | 7 +- p/db/readme.md | 2 +- p/vbb/example.js | 8 +- p/vbb/readme.md | 2 +- package.json | 3 +- readme.md | 25 +++- 12 files changed, 824 insertions(+), 11 deletions(-) create mode 100644 docs/departures.md create mode 100644 docs/journey-part.md create mode 100644 docs/journeys.md create mode 100644 docs/locations.md create mode 100644 docs/nearby.md create mode 100644 docs/radar.md rename example.js => p/db/example.js (75%) diff --git a/docs/departures.md b/docs/departures.md new file mode 100644 index 00000000..6396ccee --- /dev/null +++ b/docs/departures.md @@ -0,0 +1,155 @@ +# `departures(station, [opt])` + +`station` must be in one of these formats: + +```js +// a station ID, in a format compatible to the profile you use +'900000013102' + +// an FPTF `station` object +{ + type: 'station', + id: '900000013102', + name: 'foo station', + location: { + type: 'location', + latitude: 1.23, + longitude: 3.21 + } +} +``` + +With `opt`, you can override the default options, which look like this: + +```js +{ + when: new Date(), + direction: null, // only show departures heading to this station + duration: 10 // show departures for the next n minutes +} +``` + +## Response + +*Note:* As stated in the [*Friendly Public Transport Format* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/tree/1.0.1), the `when` include the current delay. The `delay` field, if present, expresses how much the former differs from the schedule. + +You may pass the `journeyId` field into [`journeyPart(ref, lineName, [opt])`](journey-part.md) to get details on the vehicle's journey. + +As an example, we're going to use the VBB profile: + +```js +const createClient = require('hafas-client') +const vbbProfile = require('hafas-client/p/vbb') + +const client = createClient(vbbProfile) + +// S Charlottenburg +client.journeys('900000024101', {duration: 3}) +.then(console.log) +.catch(console.error) +``` + +The response may look like this: + +```js +[ { + journeyId: '1|31431|28|86|17122017', + trip: 31431, + station: { + type: 'station', + id: '900000024101', + name: 'S Charlottenburg', + location: { + type: 'location', + latitude: 52.504806, + longitude: 13.303846 + }, + products: { + suburban: true, + subway: false, + tram: false, + bus: true, + ferry: false, + express: false, + regional: true + } + }, + when: '2017-12-17T19:32:00.000+01:00', + delay: null + line: { + type: 'line', + id: '18299', + name: 'S9', + public: true, + mode: 'train', + product: 'suburban', + symbol: 'S', + nr: 9, + metro: false, + express: false, + night: false, + productCode: 0 + }, + direction: 'S Spandau' +}, { + journeyId: '1|30977|8|86|17122017', + trip: 30977, + station: { /* … */ }, + when: '2017-12-17T19:35:00.000+01:00', + delay: 0, + line: { + type: 'line', + id: '16441', + name: 'S5', + public: true, + mode: 'train', + product: 'suburban', + symbol: 'S', + nr: 5, + metro: false, + express: false, + night: false, + productCode: 0 + }, + direction: 'S Westkreuz' +}, { + journeyId: '1|28671|4|86|17122017', + trip: 28671, + station: { + type: 'station', + id: '900000024202', + name: 'U Wilmersdorfer Str.', + location: { + type: 'location', + latitude: 52.506415, + longitude: 13.306777 + }, + products: { + suburban: false, + subway: true, + tram: false, + bus: false, + ferry: false, + express: false, + regional: false + } + }, + when: '2017-12-17T19:35:00.000+01:00', + delay: 0, + line: { + type: 'line', + id: '19494', + name: 'U7', + public: true, + mode: 'train', + product: 'subway', + symbol: 'U', + nr: 7, + metro: false, + express: false, + night: false, + productCode: 1 + }, + direction: 'U Rudow' +} ] +``` diff --git a/docs/journey-part.md b/docs/journey-part.md new file mode 100644 index 00000000..ad9edf90 --- /dev/null +++ b/docs/journey-part.md @@ -0,0 +1,112 @@ +# `journeyPart(ref, lineName, [opt])` + +This method can be used to refetch information about a leg of a journey. Note that it is not supported by every profile/endpoint. + +Let's say you used [`journeys`](journeys.md) and now want to get more up-to-date data about the arrival/departure of a leg. You'd pass in a journey leg `id` like `'1|24983|22|86|18062017'`. `lineName` must be the name of the journey leg's `line.name`. You can get them like this: + +```js +const createClient = require('hafas-client') +const vbbProfile = require('hafas-client/p/vbb') + +const client = createClient(vbbProfile) + +// Hauptbahnhof to Heinrich-Heine-Str. +client.journeys('900000003201', '900000100008', {results: 1}) +.then(([journey]) => { + const part = journey.parts[0] + return client.journeyPart(part.id, part.line.name) +}) +.then(console.log) +.catch(console.error) +``` + +With `opt`, you can override the default options, which look like this: + +```js +{ + when: new Date() +} +``` + +## Response + +*Note:* As stated in the [*Friendly Public Transport Format* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/tree/1.0.1), the returned `departure` and `arrival` times include the current delay. The `departureDelay`/`arrivalDelay` fields express how much they differ from the schedule. + +As an example, we're going to use the VBB profile: + +```js +const createClient = require('hafas-client') +const vbbProfile = require('hafas-client/p/vbb') + +const client = createClient(vbbProfile) + +client.journeyPart('1|31431|28|86|17122017', 'S9', {when: 1513534689273}) +.then(console.log) +.catch(console.error) +``` + +The response looked like this: + +```js +{ + id: '1|31431|28|86|17122017', + origin: { + type: 'station', + id: '900000260005', + name: 'S Flughafen Berlin-Schönefeld', + location: { + type: 'location', + latitude: 52.390796, + longitude: 13.51352 + }, + products: { + suburban: true, + subway: false, + tram: false, + bus: true, + ferry: false, + express: false, + regional: true + } + }, + departure: '2017-12-17T18:37:00.000+01:00', + departurePlatform: '13', + destination: { + type: 'station', + id: '900000029101', + name: 'S Spandau', + location: { + type: 'location', + latitude: 52.534794, + longitude: 13.197477 + }, + products: { + suburban: true, + subway: false, + tram: false, + bus: true, + ferry: false, + express: true, + regional: true + } + }, + arrival: '2017-12-17T19:49:00.000+01:00', + line: { + type: 'line', + id: '18299', + name: 'S9', + public: true, + mode: 'train', + product: 'suburban', + symbol: 'S', + nr: 9, + metro: false, + express: false, + night: false, + productCode: 0 + }, + arrivalPlatform: '2', + direction: 'S Spandau', + passed: [ /* … */ ] +} +``` diff --git a/docs/journeys.md b/docs/journeys.md new file mode 100644 index 00000000..29f9bb1c --- /dev/null +++ b/docs/journeys.md @@ -0,0 +1,236 @@ +# `journeys(from, to, [opt])` + +`from` and `to` each must be in one of these formats: + +```js +// a station ID, in a format compatible to the profile you use +'900000013102' + +// an FPTF `station` object +{ + type: 'station', + id: '900000013102', + name: 'foo station', + location: { + type: 'location', + latitude: 1.23, + longitude: 3.21 + } +} + +// a point of interest, which is an FPTF `location` object +{ + type: 'location', + id: '123', + name: 'foo restaurant', + latitude: 1.23, + longitude: 3.21 +} + +// an address, which is an FTPF `location` object +{ + type: 'location', + address: 'foo street 1', + latitude: 1.23, + longitude: 3.21 +} +``` + +With `opt`, you can override the default options, which look like this: + +```js +{ + when: new Date(), + results: 5, // how many journeys? + via: null, // let journeys pass this station + passedStations: false, // return stations on the way? + transfers: 5, // maximum of 5 transfers + transferTime: 0, // minimum time for a single transfer in minutes + accessibility: 'none', // 'none', 'partial' or 'complete' + bike: false, // only bike-friendly journeys + products: { + suburban: true, + subway: true, + tram: true, + bus: true, + ferry: true, + express: true, + regional: true + }, + tickets: false // return tickets? only available with some profiles +} +``` + +## Response + +*Note:* As stated in the [*Friendly Public Transport Format* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/tree/1.0.1), the returned `departure` and `arrival` times include the current delay. The `departureDelay`/`arrivalDelay` fields express how much they differ from the schedule. + +As an example, we're going to use the VBB profile: + +```js +const createClient = require('hafas-client') +const vbbProfile = require('hafas-client/p/vbb') + +const client = createClient(vbbProfile) + +// Hauptbahnhof to Heinrich-Heine-Str. +client.journeys('900000003201', '900000100008', { + results: 1, + passedStations: true +}) +.then(console.log) +.catch(console.error) +``` + +The response may look like this: + +```js +[ { + parts: [ { + 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 + }, + direction: 'S Potsdam Hauptbahnhof', + passed: [ { + station: { + type: 'station', + id: '900000003201', + name: 'S+U Berlin Hauptbahnhof', + location: { /* … */ }, + products: { /* … */ } + }, + arrival: '2017-12-17T19:06:00.000+01:00', + departure: '2017-12-17T19:07:00.000+01:00' + }, { + 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: { /* … */ } + }, + 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 +} ] +``` + +If you pass `tickets: true`, each `journey` will have a tickets array that looks like this: + +```js +[ { + name: 'Berlin Tarifgebiet A-B: Einzelfahrausweis – Regeltarif', + price: 2.8, + tariff: 'Berlin', + coverage: 'AB', + variant: 'adult', + amount: 1 +}, { + name: 'Berlin Tarifgebiet A-B: Einzelfahrausweis – Ermäßigungstarif', + price: 1.7, + tariff: 'Berlin', + coverage: 'AB', + variant: 'reduced', + amount: 1, + reduced: true +}, /* … */ { + name: 'Berlin Tarifgebiet A-B: Tageskarte – Ermäßigungstarif', + price: 4.7, + tariff: 'Berlin', + coverage: 'AB', + variant: '1 day, reduced', + amount: 1, + reduced: true, + fullDay: true +}, /* … */ { + name: 'Berlin Tarifgebiet A-B: 4-Fahrten-Karte – Regeltarif', + price: 9, + tariff: 'Berlin', + coverage: 'AB', + variant: '4x adult', + amount: 4 +} ] +``` diff --git a/docs/locations.md b/docs/locations.md new file mode 100644 index 00000000..6f8b439d --- /dev/null +++ b/docs/locations.md @@ -0,0 +1,66 @@ +# `locations(query, [opt])` + +`query` must be an string (e.g. `'Alexanderplatz'`). + +With `opt`, you can override the default options, which look like this: + +```js +{ + fuzzy: true // find only exact matches? + , results: 10 // how many search results? + , stations: true + , addresses: true + , poi: true // points of interest +} +``` + +## Response + +As an example, we're going to use the VBB profile: + +```js +const createClient = require('hafas-client') +const vbbProfile = require('hafas-client/p/vbb') + +const client = createClient(vbbProfile) + +client.locations('Alexanderplatz', {results: 3}) +.then(console.log) +.catch(console.error) +``` + +The response may look like this: + +```js +[ { + type: 'station', + id: '900000100003', + name: 'S+U Alexanderplatz', + location: { + type: 'location', + latitude: 52.521508, + longitude: 13.411267 + }, + products: { + suburban: true, + subway: true, + tram: true, + bus: true, + ferry: false, + express: false, + regional: true + } +}, { // point of interest + type: 'location', + name: 'Berlin, Holiday Inn Centre Alexanderplatz****', + id: '900980709', + latitude: 52.523549, + longitude: 13.418441 +}, { // point of interest + type: 'location', + name: 'Berlin, Hotel Agon am Alexanderplatz', + id: '900980176', + latitude: 52.524556, + longitude: 13.420266 +} ] +``` diff --git a/docs/nearby.md b/docs/nearby.md new file mode 100644 index 00000000..1a646367 --- /dev/null +++ b/docs/nearby.md @@ -0,0 +1,77 @@ +# `nearby(latitude, longitude, [opt])` + +This method can be used to find stations close to a location. Note that it is not supported by every profile/endpoint. + +`latitude` and `longitude` must be GPS coordinates like `52.5137344` and `13.4744798`. + +With `opt`, you can override the default options, which look like this: + +```js +{ + distance: null, // maximum walking distance in meters + poi: false, // return points of interest? + stations: true, // return stations? +} +``` + +## Response + +As an example, we're going to use the VBB profile: + +```js +const createClient = require('hafas-client') +const vbbProfile = require('hafas-client/p/vbb') + +const client = createClient(vbbProfile) + +client.nearby(52.5137344, 13.4744798, {distance: 400}) +.then(console.log) +.catch(console.error) +``` + +The response may look like this: + +```js +[ { + type: 'station', + id: '900000120001', + name: 'S+U Frankfurter Allee', + location: { + type: 'location', + latitude: 52.513616, + longitude: 13.475298 + }, + products: { + suburban: true, + subway: true, + tram: true, + bus: true, + ferry: false, + express: false, + regional: false + }, + distance: 56 +}, { + type: 'station', + id: '900000120540', + name: 'Scharnweberstr./Weichselstr.', + location: { + type: 'location', + latitude: 52.512339, + longitude: 13.470174 + }, + products: { /* … */ }, + distance: 330 +}, { + type: 'station', + id: '900000160544', + name: 'Rathaus Lichtenberg', + location: { + type: 'location', + latitude: 52.515908, + longitude: 13.479073 + }, + products: { /* … */ }, + distance: 394 +} ] +``` diff --git a/docs/radar.md b/docs/radar.md new file mode 100644 index 00000000..c719bec1 --- /dev/null +++ b/docs/radar.md @@ -0,0 +1,142 @@ +# `radar(north, west, south, east, [opt])` + +Use this method to find all vehicles currently in an area. Note that it is not supported by every profile/endpoint. + +`north`, `west`, `south` and `eath` must be numbers (e.g. `52.52411`). Together, they form a [bounding box](https://en.wikipedia.org/wiki/Minimum_bounding_box). + +With `opt`, you can override the default options, which look like this: + +```js +{ + results: 256, // maximum number of vehicles + duration: 30, // compute frames for the next n seconds + frames: 3, // nr of frames to compute +} +``` + +## Response + +*Note:* As stated in the [*Friendly Public Transport Format* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/tree/1.0.1), the returned `departure` and `arrival` times include the current delay. The `departureDelay`/`arrivalDelay` fields express how much they differ from the schedule. + +As an example, we're going to use the VBB profile: + +```js +const createClient = require('hafas-client') +const vbbProfile = require('hafas-client/p/vbb') + +const client = createClient(vbbProfile) + +client.radar(52.52411, 13.41002, 52.51942, 13.41709, {results: 5}) +.then(console.log) +.catch(console.error) +``` + +The response may look like this: + +```js +[ { + location: { + type: 'location', + latitude: 52.521508, + longitude: 13.411267 + }, + line: { + type: 'line', + id: 's9', + name: 'S9', + public: true, + mode: 'train', + product: 'suburban', + symbol: 'S', + nr: 9, + metro: false, + express: false, + night: false + }, + direction: 'S Flughafen Berlin-Schönefeld', + trip: 31463, + nextStops: [ { + station: { + type: 'station', + id: '900000029101', + name: 'S Spandau', + location: { + type: 'location', + latitude: 52.534794, + longitude: 13.197477 + }, + products: { + suburban: true, + subway: false, + tram: false, + bus: true, + ferry: false, + express: true, + regional: true + } + }, + arrival: null, + departure: '2017-12-17T19:16:00.000+01:00' + } /* … */ ], + frames: [ { + origin: { + type: 'station', + id: '900000100003', + name: 'S+U Alexanderplatz', + location: { /* … */ }, + products: { /* … */ } + }, + destination: { + type: 'station', + id: '900000100004', + name: 'S+U Jannowitzbrücke', + location: { /* … */ }, + products: { /* … */ } + }, + t: 0 + }, /* … */ { + origin: { /* Alexanderplatz */ }, + destination: { /* Jannowitzbrücke */ }, + t: 30000 + } ] +}, { + location: { + type: 'location', + latitude: 52.523297, + longitude: 13.411151 + }, + line: { + type: 'line', + id: 'm2', + name: 'M2', + public: true, + mode: 'train', + product: 'tram', + symbol: 'M', + nr: 2, + metro: true, + express: false, + night: false + }, + direction: 'Heinersdorf', + trip: 26321, + nextStops: [ { + station: { /* S+U Alexanderplatz/Dircksenstr. */ }, + arrival: null, + departure: '2017-12-17T19:52:00.000+01:00' + }, { + station: { /* Memhardstr. */ }, + arrival: '2017-12-17T19:54:00.000+01:00', + departure: '2017-12-17T19:54:00.000+01:00' + }, /* … */ ], + frames: [ { + origin: { /* S+U Alexanderplatz/Dircksenstr. */ }, + destination: { /* Memhardstr. */ }, + t: 0 + }, /* … */ { + origin: { /* Memhardstr. */ }, + destination: { /* Mollstr./Prenzlauer Allee */ }, + t: 30000 + } ] +}, /* … */ ] +``` diff --git a/example.js b/p/db/example.js similarity index 75% rename from example.js rename to p/db/example.js index ed52a3fa..72742f8c 100644 --- a/example.js +++ b/p/db/example.js @@ -1,7 +1,7 @@ 'use strict' -const createClient = require('.') -const dbProfile = require('./p/db') +const createClient = require('../../') +const dbProfile = require('.') const client = createClient(dbProfile) @@ -9,8 +9,9 @@ const client = createClient(dbProfile) client.journeys('8011167', '8000261', {results: 1, tickets: true}) // client.departures('8011167', {duration: 1}) // client.locations('Berlin Jungfernheide') -// client.locations('ATZE Musiktheater', {poi: true, addressses: false, fuzzy: false}) +// client.locations('Atze Musiktheater', {poi: true, addressses: false, fuzzy: false}) // client.nearby(52.4751309, 13.3656537, {results: 1}) + .then((data) => { console.log(require('util').inspect(data, {depth: null})) }, console.error) diff --git a/p/db/readme.md b/p/db/readme.md index a94e57c0..0d566c74 100644 --- a/p/db/readme.md +++ b/p/db/readme.md @@ -1,6 +1,6 @@ # DB profile for `hafas-client` -[*Deutsche Bahn (DB)*](https://en.wikipedia.org/wiki/Deutsche_Bahn) is the largest German long-distance public transport company. This profile adds *DB*-specific customizations to `hafas-client`. +[*Deutsche Bahn (DB)*](https://en.wikipedia.org/wiki/Deutsche_Bahn) is the largest German long-distance public transport company. This profile adds *DB*-specific customizations to `hafas-client`. Consider using [`db-hafas`](https://github.com/derhuerst/db-hafas#db-hafas), to always get the customized client right away. ## Usage diff --git a/p/vbb/example.js b/p/vbb/example.js index a347a4fc..089e5af2 100644 --- a/p/vbb/example.js +++ b/p/vbb/example.js @@ -5,12 +5,14 @@ const vbbProfile = require('.') const client = createClient(vbbProfile) +// Hauptbahnhof to Charlottenburg client.journeys('900000003201', '900000024101', {results: 1}) // client.departures('900000013102', {duration: 1}) // client.locations('Alexanderplatz', {results: 2}) // client.nearby(52.5137344, 13.4744798, {distance: 60}) // client.radar(52.52411, 13.41002, 52.51942, 13.41709, {results: 10}) + .then((data) => { - console.log(data) - // console.log(require('util').inspect(data, {depth: null})) -}, console.error) + console.log(require('util').inspect(data, {depth: null})) +}) +.catch(console.error) diff --git a/p/vbb/readme.md b/p/vbb/readme.md index a3ff3d1c..35b4b89a 100644 --- a/p/vbb/readme.md +++ b/p/vbb/readme.md @@ -1,6 +1,6 @@ # VBB profile for `hafas-client` -[*Verkehrsverbund Berlin-Brandenburg (VBB)*](https://en.wikipedia.org/wiki/Verkehrsverbund_Berlin-Brandenburg) is a group of public transport companies, running the public transport network in [Berlin](https://en.wikipedia.org/wiki/Berlin). This profile adds *VBB*-specific customizations to `hafas-client`. +[*Verkehrsverbund Berlin-Brandenburg (VBB)*](https://en.wikipedia.org/wiki/Verkehrsverbund_Berlin-Brandenburg) is a group of public transport companies, running the public transport network in [Berlin](https://en.wikipedia.org/wiki/Berlin). This profile adds *VBB*-specific customizations to `hafas-client`. Consider using [`vbb-hafas`](https://github.com/derhuerst/vbb-hafas#vbb-hafas), to always get the customized client right away. ## Usage diff --git a/package.json b/package.json index e7c2db7b..3437284b 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "lib", "parse", "format", - "p" + "p", + "docs" ], "author": "Jannis R ", "homepage": "https://github.com/derhuerst/hafas-client", diff --git a/readme.md b/readme.md index eaa16015..d29f30c4 100644 --- a/readme.md +++ b/readme.md @@ -2,8 +2,8 @@ **A client for HAFAS public transport APIs**. Sort of like [public-transport-enabler](https://github.com/schildbach/public-transport-enabler), but with a smaller scope. It also [contains customisations](p) for the following transport networks: -- [Deutsche Bahn](https://en.wikipedia.org/wiki/Deutsche_Bahn) - [docs](p/db/readme.md) – [src](p/db/index.js) -- [Berlin public transport](https://en.wikipedia.org/wiki/Verkehrsverbund_Berlin-Brandenburg) - [docs](p/vbb/readme.md) – [src](p/vbb/index.js) +- [Deutsche Bahn](https://en.wikipedia.org/wiki/Deutsche_Bahn) - [docs](p/db/readme.md) – [usage example](p/db/example.js) – [src](p/db/index.js) +- [Berlin public transport](https://en.wikipedia.org/wiki/Verkehrsverbund_Berlin-Brandenburg) - [docs](p/vbb/readme.md) – [usage example](p/vbb/example.js) – [src](p/vbb/index.js) [![npm version](https://img.shields.io/npm/v/hafas-client.svg)](https://www.npmjs.com/package/hafas-client) [![build status](https://img.shields.io/travis/derhuerst/hafas-client.svg)](https://travis-ci.org/derhuerst/hafas-client) @@ -163,6 +163,27 @@ The returned [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript } ] ``` +## API + +- [`journeys(from, to, [opt])`](docs/journeys.md) – get journeys between locations +- [`journeyPart(ref, name, [opt])`](docs/journey-part.md) – get details for a part of a journey +- [`departures(station, [opt])`](docs/departures.md) – query the next departures at a station +- [`locations(query, [opt])`](docs/locations.md) – find stations, POIs and addresses +- [`nearby(latitude, longitude, [opt])`](docs/nearby.md) – show stations & POIs around +- [`radar(query, [opt])`](docs/radar.md) – find all vehicles currently in a certain area + + +## Related + +- [*Friendly Public Transport Format*](https://github.com/public-transport/friendly-public-transport-format#friendly-public-transport-format-fptf) – A format for APIs, libraries and datasets containing and working with public transport data. +- [`db-hafas`](https://github.com/derhuerst/db-hafas#db-hafas) – JavaScript client for the DB HAFAS API. +- [`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-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) + ## Contributing From aeac22b569ccdfe734d1dccfdddfcbba9b609693 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 17 Dec 2017 20:33:04 +0100 Subject: [PATCH 59/73] journeyPart: passedStations option --- docs/journey-part.md | 3 ++- index.js | 5 ++++- parse/journey-part.js | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/journey-part.md b/docs/journey-part.md index ad9edf90..b08d300a 100644 --- a/docs/journey-part.md +++ b/docs/journey-part.md @@ -24,7 +24,8 @@ With `opt`, you can override the default options, which look like this: ```js { - when: new Date() + when: new Date(), + passedStations: true // return stations on the way? } ``` diff --git a/index.js b/index.js index f57ec057..ab19dfd4 100644 --- a/index.js +++ b/index.js @@ -159,6 +159,9 @@ const createClient = (profile) => { } const journeyPart = (ref, lineName, opt = {}) => { + opt = Object.assign({ + passedStations: true // return stations on the way? + }, opt) opt.when = opt.when || new Date() return request(profile, { @@ -179,7 +182,7 @@ const createClient = (profile) => { arr: maxBy(d.journey.stopL, 'idx'), jny: d.journey } - return parse(d.journey, part) + return parse(d.journey, part, !!opt.passedStations) }) } diff --git a/parse/journey-part.js b/parse/journey-part.js index 198c5fc6..c1d8648b 100644 --- a/parse/journey-part.js +++ b/parse/journey-part.js @@ -13,7 +13,7 @@ const createParseJourneyPart = (profile, stations, lines, remarks) => { // todo: what is pt.jny.dirFlg? // todo: how does pt.freq work? // todo: what is pt.himL? - const parseJourneyPart = (j, pt) => { // j = journey, pt = part + const parseJourneyPart = (j, pt, passed = true) => { // j = journey, pt = part const dep = profile.parseDateTime(profile, j.date, pt.dep.dTimeR || pt.dep.dTimeS) const arr = profile.parseDateTime(profile, j.date, pt.arr.aTimeR || pt.arr.aTimeS) const res = { @@ -41,7 +41,7 @@ const createParseJourneyPart = (profile, stations, lines, remarks) => { if (pt.dep.dPlatfS) res.departurePlatform = pt.dep.dPlatfS if (pt.arr.aPlatfS) res.arrivalPlatform = pt.arr.aPlatfS - if (pt.jny.stopL) { + if (passed && pt.jny.stopL) { const parse = profile.parseStopover(profile, stations, lines, remarks, j) res.passed = pt.jny.stopL.map(parse) } From 9e5e8b9d5622b209fe3804981e8a6df9fea9b8b0 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 18 Dec 2017 11:57:48 +0100 Subject: [PATCH 60/73] radar: support delays --- docs/radar.md | 12 +++++++++--- parse/movement.js | 14 +++++++++++++- test/util.js | 6 ++++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/docs/radar.md b/docs/radar.md index c719bec1..4db46136 100644 --- a/docs/radar.md +++ b/docs/radar.md @@ -76,7 +76,9 @@ The response may look like this: } }, arrival: null, - departure: '2017-12-17T19:16:00.000+01:00' + arrivalDelay: null, + departure: '2017-12-17T19:16:00.000+01:00', + departureDelay: null } /* … */ ], frames: [ { origin: { @@ -123,11 +125,15 @@ The response may look like this: nextStops: [ { station: { /* S+U Alexanderplatz/Dircksenstr. */ }, arrival: null, - departure: '2017-12-17T19:52:00.000+01:00' + arrivalDelay: null, + departure: '2017-12-17T19:52:00.000+01:00', + departureDelay: null }, { station: { /* Memhardstr. */ }, arrival: '2017-12-17T19:54:00.000+01:00', - departure: '2017-12-17T19:54:00.000+01:00' + arrivalDelay: null, + departure: '2017-12-17T19:54:00.000+01:00', + departureDelay: null }, /* … */ ], frames: [ { origin: { /* S+U Alexanderplatz/Dircksenstr. */ }, diff --git a/parse/movement.js b/parse/movement.js index bf2378ef..e03a3821 100644 --- a/parse/movement.js +++ b/parse/movement.js @@ -17,11 +17,23 @@ const createParseMovement = (profile, locations, lines, remarks) => { ? profile.parseDateTime(profile, m.date, s.aTimeR || s.aTimeS) : null - return { + const res = { station: locations[s.locX], departure: dep ? dep.toISO() : null, arrival: arr ? arr.toISO() : null } + + if (m.dTimeR && m.dTimeS) { + const plannedDep = profile.parseDateTime(profile, m.date, s.dTimeS) + res.departureDelay = Math.round((dep - plannedDep) / 1000) + } else res.departureDelay = null + + if (m.aTimeR && m.aTimeS) { + const plannedArr = profile.parseDateTime(profile, m.date, s.aTimeS) + res.arrivalDelay = Math.round((arr - plannedArr) / 1000) + } else res.arrivalDelay = null + + return res } const res = { diff --git a/test/util.js b/test/util.js index 90c3f69d..6a374bac 100644 --- a/test/util.js +++ b/test/util.js @@ -81,6 +81,12 @@ const isValidDateTime = (w) => { const assertValidStopover = (t, s, coordsOptional = false) => { if ('arrival' in s) t.ok(isValidDateTime(s.arrival)) if ('departure' in s) t.ok(isValidDateTime(s.departure)) + if (s.arrivalDelay !== null && s.arrivalDelay !== undefined) { + t.equal(typeof s.arrivalDelay, 'number') + } + if (s.departureDelay !== null && s.departureDelay !== undefined) { + t.equal(typeof s.departureDelay, 'number') + } if (!('arrival' in s) && !('departure' in s)) { t.fail('stopover doesn\'t contain arrival or departure') } From 9f0db98ceab2ad1c33a7722cb5c2d8070220b482 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 18 Dec 2017 12:00:06 +0100 Subject: [PATCH 61/73] simplify docs :memo: --- readme.md | 38 ++++++++++++-------------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/readme.md b/readme.md index d29f30c4..86249212 100644 --- a/readme.md +++ b/readme.md @@ -25,6 +25,16 @@ npm install hafas-client ``` +## API + +- [`journeys(from, to, [opt])`](docs/journeys.md) – get journeys between locations +- [`journeyPart(ref, name, [opt])`](docs/journey-part.md) – get details for a part of a journey +- [`departures(station, [opt])`](docs/departures.md) – query the next departures at a station +- [`locations(query, [opt])`](docs/locations.md) – find stations, POIs and addresses +- [`nearby(latitude, longitude, [opt])`](docs/nearby.md) – show stations & POIs around +- [`radar(query, [opt])`](docs/radar.md) – find all vehicles currently in a certain area + + ## Usage ```js @@ -129,23 +139,8 @@ The returned [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript type: 'station', id: '8089100', name: 'Berlin Jungfernheide (S)', - location: { - type: 'location', - latitude: 52.530291, - longitude: 13.299451 - }, - products: { - nationalExp: false, - national: false, - regionalExp: false, - regional: true, - suburban: true, - bus: true, - ferry: false, - subway: true, - tram: false, - taxi: false - } + location: { /* … */ }, + products: { /* … */ } }, departure: '2017-12-17T17:05:00.000+01:00', destination: { @@ -163,15 +158,6 @@ The returned [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript } ] ``` -## API - -- [`journeys(from, to, [opt])`](docs/journeys.md) – get journeys between locations -- [`journeyPart(ref, name, [opt])`](docs/journey-part.md) – get details for a part of a journey -- [`departures(station, [opt])`](docs/departures.md) – query the next departures at a station -- [`locations(query, [opt])`](docs/locations.md) – find stations, POIs and addresses -- [`nearby(latitude, longitude, [opt])`](docs/nearby.md) – show stations & POIs around -- [`radar(query, [opt])`](docs/radar.md) – find all vehicles currently in a certain area - ## Related From 965d5393ef5c895e3346c07d960ba07beb0be458 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 18 Dec 2017 20:01:12 +0100 Subject: [PATCH 62/73] accessibility filters for every profile --- index.js | 15 +++++++++++++-- lib/default-profile.js | 2 ++ p/db/index.js | 5 +---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index ab19dfd4..93409c1c 100644 --- a/index.js +++ b/index.js @@ -60,7 +60,18 @@ const createClient = (profile) => { }, opt) if (opt.via) opt.via = profile.formatLocation(profile, opt.via) opt.when = opt.when || new Date() - const products = profile.formatProducts(opt.products || {}) + + const filters = [ + profile.formatProducts(opt.products || {}) + ] + if ( + opt.accessibility && + profile.filters && + profile.filters.accessibility && + profile.filters.accessibility[opt.accessibility] + ) { + filters.push(profile.filters.accessibility[opt.accessibility]) + } const query = profile.transformJourneysQuery({ outDate: profile.formatDate(profile, opt.when), @@ -72,7 +83,7 @@ const createClient = (profile) => { depLocL: [from], viaLocL: opt.via ? [opt.via] : null, arrLocL: [to], - jnyFltrL: [products], + jnyFltrL: filters, getTariff: !!opt.tickets, // todo: what is req.gisFltrL? diff --git a/lib/default-profile.js b/lib/default-profile.js index 418be358..a26fbb67 100644 --- a/lib/default-profile.js +++ b/lib/default-profile.js @@ -21,6 +21,7 @@ const formatStation = require('../format/station') const formatTime = require('../format/time') const formatLocation = require('../format/location') const formatRectangle = require('../format/rectangle') +const filters = require('../format/filters') const id = x => x @@ -52,6 +53,7 @@ const defaultProfile = { formatTime, formatLocation, formatRectangle, + filters, journeyPart: false, radar: false diff --git a/p/db/index.js b/p/db/index.js index 676dbdcc..89770c35 100644 --- a/p/db/index.js +++ b/p/db/index.js @@ -7,7 +7,7 @@ const _createParseJourney = require('../../parse/journey') const _formatStation = require('../../format/station') const createParseBitmask = require('../../parse/products-bitmask') const createFormatBitmask = require('../../format/products-bitmask') -const {accessibility, bike} = require('../../format/filters') +const {bike} = require('../../format/filters') const modes = require('./modes') const formatLoyaltyCard = require('./loyalty-cards').format @@ -36,9 +36,6 @@ const transformReq = (req) => { const transformJourneysQuery = (query, opt) => { const filters = query.jnyFltrL - if (opt.accessibility && accessibility[opt.accessibility]) { - filters.push(accessibility[opt.accessibility]) - } if (opt.bike) filters.push(bike) query.trfReq = { From f6077d5b6db297ba2a401a87950fe3664fe1a4dd Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 18 Dec 2017 21:11:35 +0100 Subject: [PATCH 63/73] cancelled flag for departures & passed stations ported abf323d over from master --- docs/departures.md | 5 +++-- docs/journey-part.md | 2 +- docs/journeys.md | 7 +++++-- parse/departure.js | 8 +++++++- parse/journey-part.js | 13 ++++++++----- parse/stopover.js | 14 ++++++++++++++ 6 files changed, 38 insertions(+), 11 deletions(-) diff --git a/docs/departures.md b/docs/departures.md index 6396ccee..6c37a82e 100644 --- a/docs/departures.md +++ b/docs/departures.md @@ -95,8 +95,9 @@ The response may look like this: journeyId: '1|30977|8|86|17122017', trip: 30977, station: { /* … */ }, - when: '2017-12-17T19:35:00.000+01:00', - delay: 0, + when: null, + delay: null, + cancelled: true, line: { type: 'line', id: '16441', diff --git a/docs/journey-part.md b/docs/journey-part.md index b08d300a..faba512e 100644 --- a/docs/journey-part.md +++ b/docs/journey-part.md @@ -92,6 +92,7 @@ The response looked like this: } }, arrival: '2017-12-17T19:49:00.000+01:00', + arrivalPlatform: '2', line: { type: 'line', id: '18299', @@ -106,7 +107,6 @@ The response looked like this: night: false, productCode: 0 }, - arrivalPlatform: '2', direction: 'S Spandau', passed: [ /* … */ ] } diff --git a/docs/journeys.md b/docs/journeys.md index 29f9bb1c..4617936d 100644 --- a/docs/journeys.md +++ b/docs/journeys.md @@ -154,8 +154,9 @@ The response may look like this: location: { /* … */ }, products: { /* … */ } }, - arrival: '2017-12-17T19:06:00.000+01:00', - departure: '2017-12-17T19:07:00.000+01:00' + arrival: null, + departure: null, + cancelled: true }, { station: { type: 'station', @@ -234,3 +235,5 @@ If you pass `tickets: true`, each `journey` will have a tickets array that looks amount: 4 } ] ``` + +If a journey leg has been cancelled, a `cancelled: true` will be added. Also, `departure`/`departureDelay`/`departurePlatform` and `arrival`/`arrivalDelay`/`arrivalPlatform` will be `null`. diff --git a/parse/departure.js b/parse/departure.js index 7946b5b4..1db0f914 100644 --- a/parse/departure.js +++ b/parse/departure.js @@ -1,7 +1,6 @@ 'use strict' // todos from derhuerst/hafas-client#2 -// - stdStop.dCncl // - stdStop.dPlatfS, stdStop.dPlatfR // todo: what is d.jny.dirFlg? // todo: d.stbStop.dProgType @@ -29,6 +28,13 @@ const createParseDeparture = (profile, stations, lines, remarks) => { res.delay = Math.round((realtime - planned) / 1000) } else res.delay = null + // todo: follow public-transport/friendly-public-transport-format#27 here + // see also derhuerst/vbb-rest#19 + if (pt.arr.aCncl || pt.dep.dCncl) { + res.cancelled = true + res.when = res.delay = null + } + return res } diff --git a/parse/journey-part.js b/parse/journey-part.js index c1d8648b..9d66d792 100644 --- a/parse/journey-part.js +++ b/parse/journey-part.js @@ -66,11 +66,14 @@ const createParseJourneyPart = (profile, stations, lines, remarks) => { // todo: follow public-transport/friendly-public-transport-format#27 here // see also derhuerst/vbb-rest#19 - if (pt.arr.dCncl && pt.dep.dCncl) { - result.cancelled = true - result.departure = result.departurePlatform = null - result.arrival = result.arrivalPlatform = null - result.delay = null + if (pt.arr.aCncl) { + res.cancelled = true + res.arrival = res.arrivalPlatform = null + } + if (pt.dep.dCncl) { + res.cancelled = true + res.departure = res.departurePlatform = null + res.delay = null } return res diff --git a/parse/stopover.js b/parse/stopover.js index d7a3d24f..216d9c1b 100644 --- a/parse/stopover.js +++ b/parse/stopover.js @@ -1,5 +1,7 @@ 'use strict' +// todo: arrivalDelay, departureDelay or only delay ? +// todo: arrivalPlatform, departurePlatform const createParseStopover = (profile, stations, lines, remarks, connection) => { const parseStopover = (st) => { const res = { @@ -13,6 +15,18 @@ const createParseStopover = (profile, stations, lines, remarks, connection) => { const dep = profile.parseDateTime(profile, connection.date, st.dTimeR || st.dTimeS) res.departure = dep.toISO() } + + // todo: follow public-transport/friendly-public-transport-format#27 here + // see also derhuerst/vbb-rest#19 + if (st.aCncl) { + res.cancelled = true + res.arrival = null + } + if (st.dCncl) { + res.cancelled = true + res.departure = null + } + return res } From 28e601553d908db9453d56d1490c79e7530ab842 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 18 Dec 2017 23:25:41 +0100 Subject: [PATCH 64/73] =?UTF-8?q?fix=20DB=20modes=20=F0=9F=90=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- p/db/modes.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/p/db/modes.js b/p/db/modes.js index be2252c3..cdd63eae 100644 --- a/p/db/modes.js +++ b/p/db/modes.js @@ -17,15 +17,15 @@ const m = { }, regionalExp: { bitmask: 4, - name: 'InterRegio', - short: 'IR', + name: 'RegionalExpress & InterRegio', + short: 'RE/IR', mode: 'train', product: 'regionalExp' }, regional: { bitmask: 8, - name: 'RegionalExpress & Regio', - short: 'RE/RB', + name: 'Regio', + short: 'RB', mode: 'train', product: 'regional' }, From c304119de539591b022c74c95921dad6bebf05d3 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 18 Dec 2017 23:33:29 +0100 Subject: [PATCH 65/73] =?UTF-8?q?bugfixes=20=F0=9F=90=9B=F0=9F=92=9A,=20up?= =?UTF-8?q?date=20deps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- p/db/modes.js | 1 + package.json | 4 ++-- parse/departure.js | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/p/db/modes.js b/p/db/modes.js index cdd63eae..6996b768 100644 --- a/p/db/modes.js +++ b/p/db/modes.js @@ -1,5 +1,6 @@ 'use strict' +// todo: https://gist.github.com/anonymous/d3323a5d2d6e159ed42b12afd0380434#file-haf_products-properties-L1-L95 const m = { nationalExp: { bitmask: 1, diff --git a/package.json b/package.json index 3437284b..83856381 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "vbb-parse-line": "^0.2.5", "vbb-parse-ticket": "^0.2.1", "vbb-short-station-name": "^0.4.0", - "vbb-stations": "^5.8.0", + "vbb-stations": "^5.9.0", "vbb-translate-ids": "^3.1.0" }, "devDependencies": { @@ -49,7 +49,7 @@ "tape": "^4.8.0", "tape-promise": "^2.0.1", "validate-fptf": "^1.0.2", - "vbb-stations-autocomplete": "^2.9.0" + "vbb-stations-autocomplete": "^2.11.0" }, "scripts": { "test": "node test/index.js", diff --git a/parse/departure.js b/parse/departure.js index 1db0f914..a4fce0dd 100644 --- a/parse/departure.js +++ b/parse/departure.js @@ -30,7 +30,7 @@ const createParseDeparture = (profile, stations, lines, remarks) => { // todo: follow public-transport/friendly-public-transport-format#27 here // see also derhuerst/vbb-rest#19 - if (pt.arr.aCncl || pt.dep.dCncl) { + if (d.stbStop.aCncl || d.stbStop.dCncl) { res.cancelled = true res.when = res.delay = null } From 8985f8ccd2d8ffa32b09189154174b9c230cc91c Mon Sep 17 00:00:00 2001 From: Jannis R Date: Thu, 28 Dec 2017 16:56:27 +0100 Subject: [PATCH 66/73] rename journey.parts -> journey.legs --- index.js | 10 +++++----- lib/default-profile.js | 6 +++--- lib/validate-profile.js | 2 +- p/vbb/index.js | 2 +- parse/index.js | 2 +- parse/{journey-part.js => journey-leg.js} | 8 ++++---- parse/journey.js | 18 +++++++++--------- 7 files changed, 24 insertions(+), 24 deletions(-) rename parse/{journey-part.js => journey-leg.js} (91%) diff --git a/index.js b/index.js index 93409c1c..616e6a0c 100644 --- a/index.js +++ b/index.js @@ -169,7 +169,7 @@ const createClient = (profile) => { }) } - const journeyPart = (ref, lineName, opt = {}) => { + const journeyLeg = (ref, lineName, opt = {}) => { opt = Object.assign({ passedStations: true // return stations on the way? }, opt) @@ -185,15 +185,15 @@ const createClient = (profile) => { } }) .then((d) => { - const parse = profile.parseJourneyPart(profile, d.locations, d.lines, d.remarks) + const parse = profile.parseJourneyLeg(profile, d.locations, d.lines, d.remarks) - const part = { // pretend the part is contained in a journey + const leg = { // pretend the leg is contained in a journey type: 'JNY', dep: minBy(d.journey.stopL, 'idx'), arr: maxBy(d.journey.stopL, 'idx'), jny: d.journey } - return parse(d.journey, part, !!opt.passedStations) + return parse(d.journey, leg, !!opt.passedStations) }) } @@ -239,7 +239,7 @@ const createClient = (profile) => { } const client = {departures, journeys, locations, nearby} - if (profile.journeyPart) client.journeyPart = journeyPart + if (profile.journeyLeg) client.journeyLeg = journeyLeg if (profile.radar) client.radar = radar Object.defineProperty(client, 'profile', {value: profile}) return client diff --git a/lib/default-profile.js b/lib/default-profile.js index a26fbb67..ec2a09fe 100644 --- a/lib/default-profile.js +++ b/lib/default-profile.js @@ -2,7 +2,7 @@ const parseDateTime = require('../parse/date-time') const parseDeparture = require('../parse/departure') -const parseJourneyPart = require('../parse/journey-part') +const parseJourneyLeg = require('../parse/journey-leg') const parseJourney = require('../parse/journey') const parseLine = require('../parse/line') const parseLocation = require('../parse/location') @@ -33,7 +33,7 @@ const defaultProfile = { parseDateTime, parseDeparture, - parseJourneyPart, + parseJourneyLeg, parseJourney, parseLine, parseStationName: id, @@ -55,7 +55,7 @@ const defaultProfile = { formatRectangle, filters, - journeyPart: false, + journeyLeg: false, radar: false } diff --git a/lib/validate-profile.js b/lib/validate-profile.js index 7d875c74..cfa8a840 100644 --- a/lib/validate-profile.js +++ b/lib/validate-profile.js @@ -11,7 +11,7 @@ const types = { parseDateTime: 'function', parseDeparture: 'function', - parseJourneyPart: 'function', + parseJourneyLeg: 'function', parseJourney: 'function', parseLine: 'function', parseStationName: 'function', diff --git a/p/vbb/index.js b/p/vbb/index.js index a1018930..dce6261a 100644 --- a/p/vbb/index.js +++ b/p/vbb/index.js @@ -178,7 +178,7 @@ const vbbProfile = { formatStation, formatProducts, - journeyPart: true, + journeyLeg: true, radar: true } diff --git a/parse/index.js b/parse/index.js index af61cf55..11a5bd0f 100644 --- a/parse/index.js +++ b/parse/index.js @@ -7,7 +7,7 @@ module.exports = { remark: require('./remark'), operator: require('./operator'), stopover: require('./stopover'), - journeyPart: require('./journey-part'), + journeyLeg: require('./journey-leg'), journey: require('./journey'), nearby: require('./nearby'), movement: require('./movement') diff --git a/parse/journey-part.js b/parse/journey-leg.js similarity index 91% rename from parse/journey-part.js rename to parse/journey-leg.js index 9d66d792..cf8a4f7a 100644 --- a/parse/journey-part.js +++ b/parse/journey-leg.js @@ -4,7 +4,7 @@ const parseDateTime = require('./date-time') const clone = obj => Object.assign({}, obj) -const createParseJourneyPart = (profile, stations, lines, remarks) => { +const createParseJourneyLeg = (profile, stations, lines, remarks) => { // todo: finish parse/remark.js first const applyRemark = (j, rm) => {} @@ -13,7 +13,7 @@ const createParseJourneyPart = (profile, stations, lines, remarks) => { // todo: what is pt.jny.dirFlg? // todo: how does pt.freq work? // todo: what is pt.himL? - const parseJourneyPart = (j, pt, passed = true) => { // j = journey, pt = part + const parseJourneyLeg = (j, pt, passed = true) => { // j = journey, pt = part const dep = profile.parseDateTime(profile, j.date, pt.dep.dTimeR || pt.dep.dTimeS) const arr = profile.parseDateTime(profile, j.date, pt.arr.aTimeR || pt.arr.aTimeS) const res = { @@ -79,7 +79,7 @@ const createParseJourneyPart = (profile, stations, lines, remarks) => { return res } - return parseJourneyPart + return parseJourneyLeg } -module.exports = createParseJourneyPart +module.exports = createParseJourneyLeg diff --git a/parse/journey.js b/parse/journey.js index 7dc9c737..2463c114 100644 --- a/parse/journey.js +++ b/parse/journey.js @@ -1,26 +1,26 @@ 'use strict' -const createParseJourneyPart = require('./journey-part') +const createParseJourneyLeg = require('./journey-leg') const clone = obj => Object.assign({}, obj) const createParseJourney = (profile, stations, lines, remarks) => { - const parsePart = createParseJourneyPart(profile, stations, lines, remarks) + const parseLeg = createParseJourneyLeg(profile, stations, lines, remarks) // todo: c.sDays // todo: c.dep.dProgType, c.arr.dProgType // todo: c.conSubscr // todo: c.trfRes x vbb-parse-ticket const parseJourney = (j) => { - const parts = j.secL.map(part => parsePart(j, part)) + const legs = j.secL.map(leg => parseLeg(j, leg)) const res = { - parts, - origin: parts[0].origin, - destination: parts[parts.length - 1].destination, - departure: parts[0].departure, - arrival: parts[parts.length - 1].arrival + legs, + origin: legs[0].origin, + destination: legs[legs.length - 1].destination, + departure: legs[0].departure, + arrival: legs[legs.length - 1].arrival } - if (parts.some(p => p.cancelled)) { + if (legs.some(p => p.cancelled)) { res.cancelled = true res.departure = res.arrival = null } From c02983492b72d74d061e1d29971d746c1eec3f64 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Thu, 28 Dec 2017 17:14:53 +0100 Subject: [PATCH 67/73] =?UTF-8?q?docs,=20tests:=20rename=20journey.parts?= =?UTF-8?q?=20->=20journey.legs=20=F0=9F=93=9D=E2=9C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/departures.md | 2 +- docs/{journey-part.md => journey-leg.md} | 8 +-- docs/journeys.md | 2 +- readme.md | 4 +- test/db.js | 72 +++++++++---------- test/vbb.js | 92 ++++++++++++------------ 6 files changed, 90 insertions(+), 90 deletions(-) rename docs/{journey-part.md => journey-leg.md} (92%) diff --git a/docs/departures.md b/docs/departures.md index 6c37a82e..9620331d 100644 --- a/docs/departures.md +++ b/docs/departures.md @@ -33,7 +33,7 @@ With `opt`, you can override the default options, which look like this: *Note:* As stated in the [*Friendly Public Transport Format* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/tree/1.0.1), the `when` include the current delay. The `delay` field, if present, expresses how much the former differs from the schedule. -You may pass the `journeyId` field into [`journeyPart(ref, lineName, [opt])`](journey-part.md) to get details on the vehicle's journey. +You may pass the `journeyId` field into [`journeyLeg(ref, lineName, [opt])`](journey-leg.md) to get details on the vehicle's journey. As an example, we're going to use the VBB profile: diff --git a/docs/journey-part.md b/docs/journey-leg.md similarity index 92% rename from docs/journey-part.md rename to docs/journey-leg.md index faba512e..9538199f 100644 --- a/docs/journey-part.md +++ b/docs/journey-leg.md @@ -1,4 +1,4 @@ -# `journeyPart(ref, lineName, [opt])` +# `journeyLeg(ref, lineName, [opt])` This method can be used to refetch information about a leg of a journey. Note that it is not supported by every profile/endpoint. @@ -13,8 +13,8 @@ const client = createClient(vbbProfile) // Hauptbahnhof to Heinrich-Heine-Str. client.journeys('900000003201', '900000100008', {results: 1}) .then(([journey]) => { - const part = journey.parts[0] - return client.journeyPart(part.id, part.line.name) + const leg = journey.legs[0] + return client.journeyLeg(leg.id, leg.line.name) }) .then(console.log) .catch(console.error) @@ -41,7 +41,7 @@ const vbbProfile = require('hafas-client/p/vbb') const client = createClient(vbbProfile) -client.journeyPart('1|31431|28|86|17122017', 'S9', {when: 1513534689273}) +client.journeyLeg('1|31431|28|86|17122017', 'S9', {when: 1513534689273}) .then(console.log) .catch(console.error) ``` diff --git a/docs/journeys.md b/docs/journeys.md index 4617936d..98cca95e 100644 --- a/docs/journeys.md +++ b/docs/journeys.md @@ -86,7 +86,7 @@ The response may look like this: ```js [ { - parts: [ { + legs: [ { id: '1|31041|35|86|17122017', origin: { type: 'station', diff --git a/readme.md b/readme.md index 86249212..23840a56 100644 --- a/readme.md +++ b/readme.md @@ -28,7 +28,7 @@ npm install hafas-client ## API - [`journeys(from, to, [opt])`](docs/journeys.md) – get journeys between locations -- [`journeyPart(ref, name, [opt])`](docs/journey-part.md) – get details for a part of a journey +- [`journeyLeg(ref, name, [opt])`](docs/journey-leg.md) – get details for a leg of a journey - [`departures(station, [opt])`](docs/departures.md) – query the next departures at a station - [`locations(query, [opt])`](docs/locations.md) – find stations, POIs and addresses - [`nearby(latitude, longitude, [opt])`](docs/nearby.md) – show stations & POIs around @@ -54,7 +54,7 @@ The returned [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript ```js [ { - parts: [ { + legs: [ { id: '1|100067|48|81|17122017', origin: { type: 'station', diff --git a/test/db.js b/test/db.js index 35292b12..317b3c65 100644 --- a/test/db.js +++ b/test/db.js @@ -118,30 +118,30 @@ test('Berlin Jungfernheide to München Hbf', co.wrap(function* (t) { } t.ok(isValidWhen(journey.arrival)) - t.ok(Array.isArray(journey.parts)) - t.ok(journey.parts.length > 0, 'no parts') - const part = journey.parts[0] + t.ok(Array.isArray(journey.legs)) + t.ok(journey.legs.length > 0, 'no legs') + const leg = journey.legs[0] - assertValidStation(t, part.origin) - assertValidStationProducts(t, part.origin.products) - if (!(yield findStation(part.origin.id))) { - console.error('unknown station', part.origin.id, part.origin.name) + assertValidStation(t, leg.origin) + assertValidStationProducts(t, leg.origin.products) + if (!(yield findStation(leg.origin.id))) { + console.error('unknown station', leg.origin.id, leg.origin.name) } - t.ok(isValidWhen(part.departure)) - t.equal(typeof part.departurePlatform, 'string') + t.ok(isValidWhen(leg.departure)) + t.equal(typeof leg.departurePlatform, 'string') - assertValidStation(t, part.destination) - assertValidStationProducts(t, part.origin.products) - if (!(yield findStation(part.destination.id))) { - console.error('unknown station', part.destination.id, part.destination.name) + assertValidStation(t, leg.destination) + assertValidStationProducts(t, leg.origin.products) + if (!(yield findStation(leg.destination.id))) { + console.error('unknown station', leg.destination.id, leg.destination.name) } - t.ok(isValidWhen(part.arrival)) - t.equal(typeof part.arrivalPlatform, 'string') + t.ok(isValidWhen(leg.arrival)) + t.equal(typeof leg.arrivalPlatform, 'string') - assertValidLine(t, part.line) + assertValidLine(t, leg.line) - t.ok(Array.isArray(part.passed)) - for (let stopover of part.passed) assertValidStopover(t, stopover) + t.ok(Array.isArray(leg.passed)) + for (let stopover of leg.passed) assertValidStopover(t, stopover) if (journey.price) assertValidPrice(t, journey.price) } @@ -158,18 +158,18 @@ test('Berlin Jungfernheide to Torfstraße 17', co.wrap(function* (t) { t.ok(Array.isArray(journeys)) t.ok(journeys.length >= 1, 'no journeys') const journey = journeys[0] - const part = journey.parts[journey.parts.length - 1] + const leg = journey.legs[journey.legs.length - 1] - assertValidStation(t, part.origin) - assertValidStationProducts(t, part.origin.products) - if (!(yield findStation(part.origin.id))) { - console.error('unknown station', part.origin.id, part.origin.name) + assertValidStation(t, leg.origin) + assertValidStationProducts(t, leg.origin.products) + if (!(yield findStation(leg.origin.id))) { + console.error('unknown station', leg.origin.id, leg.origin.name) } - if (part.origin.products) assertValidProducts(t, part.origin.products) - t.ok(isValidWhen(part.departure)) - t.ok(isValidWhen(part.arrival)) + if (leg.origin.products) assertValidProducts(t, leg.origin.products) + t.ok(isValidWhen(leg.departure)) + t.ok(isValidWhen(leg.arrival)) - const d = part.destination + const d = leg.destination assertValidAddress(t, d) t.equal(d.address, 'Torfstraße 17') t.ok(isRoughlyEqual(.0001, d.latitude, 52.5416823)) @@ -187,18 +187,18 @@ test('Berlin Jungfernheide to ATZE Musiktheater', co.wrap(function* (t) { t.ok(Array.isArray(journeys)) t.ok(journeys.length >= 1, 'no journeys') const journey = journeys[0] - const part = journey.parts[journey.parts.length - 1] + const leg = journey.legs[journey.legs.length - 1] - assertValidStation(t, part.origin) - assertValidStationProducts(t, part.origin.products) - if (!(yield findStation(part.origin.id))) { - console.error('unknown station', part.origin.id, part.origin.name) + assertValidStation(t, leg.origin) + assertValidStationProducts(t, leg.origin.products) + if (!(yield findStation(leg.origin.id))) { + console.error('unknown station', leg.origin.id, leg.origin.name) } - if (part.origin.products) assertValidProducts(t, part.origin.products) - t.ok(isValidWhen(part.departure)) - t.ok(isValidWhen(part.arrival)) + if (leg.origin.products) assertValidProducts(t, leg.origin.products) + t.ok(isValidWhen(leg.departure)) + t.ok(isValidWhen(leg.arrival)) - const d = part.destination + const d = leg.destination assertValidPoi(t, d) t.equal(d.name, 'ATZE Musiktheater') t.ok(isRoughlyEqual(.0001, d.latitude, 52.542399)) diff --git a/test/vbb.js b/test/vbb.js index fb0676b5..cc9eec9d 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -77,29 +77,29 @@ test('journeys – station to station', co.wrap(function* (t) { t.strictEqual(journey.destination.id, amrumerStr) assertValidWhen(t, journey.arrival) - t.ok(Array.isArray(journey.parts)) - t.strictEqual(journey.parts.length, 1) - const part = journey.parts[0] + t.ok(Array.isArray(journey.legs)) + t.strictEqual(journey.legs.length, 1) + const leg = journey.legs[0] - t.equal(typeof part.id, 'string') - t.ok(part.id) - assertValidStation(t, part.origin) - assertValidStationProducts(t, part.origin.products) - t.ok(part.origin.name.indexOf('(Berlin)') === -1) - t.strictEqual(part.origin.id, spichernstr) - assertValidWhen(t, part.departure) + t.equal(typeof leg.id, 'string') + t.ok(leg.id) + assertValidStation(t, leg.origin) + assertValidStationProducts(t, leg.origin.products) + t.ok(leg.origin.name.indexOf('(Berlin)') === -1) + t.strictEqual(leg.origin.id, spichernstr) + assertValidWhen(t, leg.departure) - assertValidStation(t, part.destination) - assertValidStationProducts(t, part.destination.products) - t.strictEqual(part.destination.id, amrumerStr) - assertValidWhen(t, part.arrival) + assertValidStation(t, leg.destination) + assertValidStationProducts(t, leg.destination.products) + t.strictEqual(leg.destination.id, amrumerStr) + assertValidWhen(t, leg.arrival) - assertValidLine(t, part.line) - t.ok(findStation(part.direction)) - t.ok(part.direction.indexOf('(Berlin)') === -1) + assertValidLine(t, leg.line) + t.ok(findStation(leg.direction)) + t.ok(leg.direction.indexOf('(Berlin)') === -1) - t.ok(Array.isArray(part.passed)) - for (let passed of part.passed) assertValidStopover(t, passed) + t.ok(Array.isArray(leg.passed)) + for (let passed of leg.passed) assertValidStopover(t, passed) // todo: find a journey where there ticket info is always available if (journey.tickets) { @@ -128,11 +128,11 @@ test('journeys – only subway', co.wrap(function* (t) { t.ok(journeys.length > 1) for (let journey of journeys) { - for (let part of journey.parts) { - if (part.line) { - assertValidLine(t, part.line) - t.equal(part.line.mode, 'train') - t.equal(part.line.product, 'subway') + for (let leg of journey.legs) { + if (leg.line) { + assertValidLine(t, leg.line) + t.equal(leg.line.mode, 'train') + t.equal(leg.line.product, 'subway') } } } @@ -159,26 +159,26 @@ test('journeys – fails with no product', co.wrap(function* (t) { } })) -test('journey part details', co.wrap(function* (t) { +test('journey leg details', co.wrap(function* (t) { const journeys = yield client.journeys(spichernstr, amrumerStr, { results: 1, when }) - const p = journeys[0].parts[0] + const p = journeys[0].legs[0] t.ok(p.id, 'precondition failed') t.ok(p.line.name, 'precondition failed') - const part = yield client.journeyPart(p.id, p.line.name, {when}) + const leg = yield client.journeyLeg(p.id, p.line.name, {when}) - t.equal(typeof part.id, 'string') - t.ok(part.id) + t.equal(typeof leg.id, 'string') + t.ok(leg.id) - assertValidLine(t, part.line) + assertValidLine(t, leg.line) - t.equal(typeof part.direction, 'string') - t.ok(part.direction) + t.equal(typeof leg.direction, 'string') + t.ok(leg.direction) - t.ok(Array.isArray(part.passed)) - for (let passed of part.passed) assertValidStopover(t, passed) + t.ok(Array.isArray(leg.passed)) + for (let passed of leg.passed) assertValidStopover(t, passed) t.end() })) @@ -194,18 +194,18 @@ test('journeys – station to address', co.wrap(function* (t) { t.ok(Array.isArray(journeys)) t.strictEqual(journeys.length, 1) const journey = journeys[0] - const part = journey.parts[journey.parts.length - 1] + const leg = journey.legs[journey.legs.length - 1] - assertValidStation(t, part.origin) - assertValidStationProducts(t, part.origin.products) - assertValidWhen(t, part.departure) + assertValidStation(t, leg.origin) + assertValidStationProducts(t, leg.origin.products) + assertValidWhen(t, leg.departure) - const dest = part.destination + 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)) - assertValidWhen(t, part.arrival) + assertValidWhen(t, leg.arrival) t.end() })) @@ -221,18 +221,18 @@ test('journeys – station to POI', co.wrap(function* (t) { t.ok(Array.isArray(journeys)) t.strictEqual(journeys.length, 1) const journey = journeys[0] - const part = journey.parts[journey.parts.length - 1] + const leg = journey.legs[journey.legs.length - 1] - assertValidStation(t, part.origin) - assertValidStationProducts(t, part.origin.products) - assertValidWhen(t, part.departure) + assertValidStation(t, leg.origin) + assertValidStationProducts(t, leg.origin.products) + assertValidWhen(t, leg.departure) - const dest = part.destination + const dest = leg.destination assertValidPoi(t, dest) t.strictEqual(dest.name, 'ATZE Musiktheater') t.ok(isRoughlyEqual(.0001, dest.latitude, 52.543333)) t.ok(isRoughlyEqual(.0001, dest.longitude, 13.351686)) - assertValidWhen(t, part.arrival) + assertValidWhen(t, leg.arrival) t.end() })) From 75513aa0f725ec56507c5ddc1b732f0c4b277998 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Thu, 4 Jan 2018 16:19:42 +0100 Subject: [PATCH 68/73] departures: accept station objects :bug: As mentioned in the docs. --- index.js | 6 ++++-- test/db.js | 16 ++++++++++++++++ test/vbb.js | 16 ++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 616e6a0c..a02fa68e 100644 --- a/index.js +++ b/index.js @@ -12,7 +12,9 @@ const createClient = (profile) => { validateProfile(profile) const departures = (station, opt = {}) => { - if ('string' !== typeof station) throw new Error('station must be a string.') + if ('object' === typeof station) station = profile.formatStation(station.id) + else if ('string' === typeof station) station = profile.formatStation(station) + else throw new Error('station must be an object or a string.') opt = Object.assign({ direction: null, // only show departures heading to this station @@ -28,7 +30,7 @@ const createClient = (profile) => { type: 'DEP', date: profile.formatDate(profile, opt.when), time: profile.formatTime(profile, opt.when), - stbLoc: profile.formatStation(station), + stbLoc: station, dirLoc: dir, jnyFltrL: [products], dur: opt.duration, diff --git a/test/db.js b/test/db.js index 317b3c65..735a9c0f 100644 --- a/test/db.js +++ b/test/db.js @@ -226,6 +226,22 @@ test('departures at Berlin Jungfernheide', co.wrap(function* (t) { t.end() })) +test('departures with station object', co.wrap(function* (t) { + yield client.departures({ + type: 'station', + id: '8011167', + name: 'Berlin Jungfernheide', + location: { + type: 'location', + latitude: 1.23, + longitude: 2.34 + } + }, {when}) + + t.ok('did not fail') + t.end() +})) + test('nearby Berlin Jungfernheide', co.wrap(function* (t) { const nearby = yield client.nearby(52.530273, 13.299433, { results: 2, distance: 400 diff --git a/test/vbb.js b/test/vbb.js index cc9eec9d..156a7580 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -260,6 +260,22 @@ test('departures', co.wrap(function* (t) { t.end() })) +test('departures with station object', co.wrap(function* (t) { + yield client.departures({ + type: 'station', + id: spichernstr, + name: 'U Spichernstr', + location: { + type: 'location', + latitude: 1.23, + longitude: 2.34 + } + }, {when}) + + t.ok('did not fail') + t.end() +})) + test('departures at 7-digit station', co.wrap(function* (t) { const eisenach = '8010097' // see derhuerst/vbb-hafas#22 yield client.departures(eisenach, {when}) From c4f67e15d364461dda3ba74a2dee602a3a6b85b0 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Thu, 4 Jan 2018 22:37:47 +0100 Subject: [PATCH 69/73] minimal profiles docs :memo: --- docs/departures.md | 2 +- docs/journeys.md | 2 +- p/readme.md | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 p/readme.md diff --git a/docs/departures.md b/docs/departures.md index 9620331d..b0217f14 100644 --- a/docs/departures.md +++ b/docs/departures.md @@ -31,7 +31,7 @@ With `opt`, you can override the default options, which look like this: ## Response -*Note:* As stated in the [*Friendly Public Transport Format* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/tree/1.0.1), the `when` include the current delay. The `delay` field, if present, expresses how much the former differs from the schedule. +*Note:* As stated in the [*Friendly Public Transport Format* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/tree/1.0.1), the `when` field includes the current delay. The `delay` field, if present, expresses how much the former differs from the schedule. You may pass the `journeyId` field into [`journeyLeg(ref, lineName, [opt])`](journey-leg.md) to get details on the vehicle's journey. diff --git a/docs/journeys.md b/docs/journeys.md index 98cca95e..47ec0c9e 100644 --- a/docs/journeys.md +++ b/docs/journeys.md @@ -199,7 +199,7 @@ The response may look like this: } ] ``` -If you pass `tickets: true`, each `journey` will have a tickets array that looks like this: +Some [profiles](../p) are able to parse the ticket information, if returned by the API. For example, if you pass `tickets: true` with the [VBB profile](../p/vbb), each `journey` will have a tickets array that looks like this: ```js [ { diff --git a/p/readme.md b/p/readme.md new file mode 100644 index 00000000..dea7cc16 --- /dev/null +++ b/p/readme.md @@ -0,0 +1,15 @@ +# Profiles + +This directory contains specific customisations for each endpoint, called *profiles*. They **parse data from the API differently, add additional information, or enable non-default methods** (such as [`journeyLeg`](../docs/journey-leg.md)) if they are supported. + +Each profile has it's own directory. It will be passed into `hafas-client` and is expected to be in a certain structure: + +```js +const createClient = require('hafas-client') +const someProfile = require('hafas-client/p/some-profile') + +// create a client with the profile +const client = createClient(dbProfile) + +// use it to query data… +``` From 508ff9fc728fcc12aea50b8a38101b24b181527e Mon Sep 17 00:00:00 2001 From: Jannis R Date: Fri, 5 Jan 2018 14:53:03 +0100 Subject: [PATCH 70/73] nearby: lat & lon -> location object --- docs/nearby.md | 10 +++++++--- index.js | 18 +++++++++++++----- readme.md | 2 +- test/db.js | 6 +++++- test/vbb.js | 6 +++++- 5 files changed, 31 insertions(+), 11 deletions(-) diff --git a/docs/nearby.md b/docs/nearby.md index 1a646367..3424e281 100644 --- a/docs/nearby.md +++ b/docs/nearby.md @@ -1,8 +1,8 @@ -# `nearby(latitude, longitude, [opt])` +# `nearby(location, [opt])` This method can be used to find stations close to a location. Note that it is not supported by every profile/endpoint. -`latitude` and `longitude` must be GPS coordinates like `52.5137344` and `13.4744798`. +`location` must be an [*FPTF* `location` object](https://github.com/public-transport/friendly-public-transport-format/blob/1.0.1/spec/readme.md#location-objects). With `opt`, you can override the default options, which look like this: @@ -24,7 +24,11 @@ const vbbProfile = require('hafas-client/p/vbb') const client = createClient(vbbProfile) -client.nearby(52.5137344, 13.4744798, {distance: 400}) +client.nearby({ + type: 'location', + latitude: 52.5137344, + longitude: 13.4744798 +}, {distance: 400}) .then(console.log) .catch(console.error) ``` diff --git a/index.js b/index.js index a02fa68e..5c5f3a6a 100644 --- a/index.js +++ b/index.js @@ -137,9 +137,17 @@ const createClient = (profile) => { }) } - const nearby = (latitude, longitude, opt = {}) => { - if ('number' !== typeof latitude) throw new Error('latitude must be a number.') - if ('number' !== typeof longitude) throw new Error('longitude must be a number.') + const nearby = (location, opt = {}) => { + if ('object' !== typeof location || Array.isArray(location)) { + throw new Error('location must be an object.') + } else if (location.type !== 'location') { + throw new Error('invalid location object.') + } else if ('number' !== typeof location.latitude) { + throw new Error('location.latitude must be a number.') + } else if ('number' !== typeof location.longitude) { + throw new Error('location.longitude must be a number.') + } + opt = Object.assign({ results: 8, // maximum number of results distance: null, // maximum walking distance in meters @@ -153,8 +161,8 @@ const createClient = (profile) => { req: { ring: { cCrd: { - x: profile.formatCoord(longitude), - y: profile.formatCoord(latitude) + x: profile.formatCoord(location.longitude), + y: profile.formatCoord(location.latitude) }, maxDist: opt.distance || -1, minDist: 0 diff --git a/readme.md b/readme.md index 23840a56..e957243a 100644 --- a/readme.md +++ b/readme.md @@ -31,7 +31,7 @@ npm install hafas-client - [`journeyLeg(ref, name, [opt])`](docs/journey-leg.md) – get details for a leg of a journey - [`departures(station, [opt])`](docs/departures.md) – query the next departures at a station - [`locations(query, [opt])`](docs/locations.md) – find stations, POIs and addresses -- [`nearby(latitude, longitude, [opt])`](docs/nearby.md) – show stations & POIs around +- [`nearby(location, [opt])`](docs/nearby.md) – show stations & POIs around - [`radar(query, [opt])`](docs/radar.md) – find all vehicles currently in a certain area diff --git a/test/db.js b/test/db.js index 735a9c0f..9f9801d6 100644 --- a/test/db.js +++ b/test/db.js @@ -243,7 +243,11 @@ test('departures with station object', co.wrap(function* (t) { })) test('nearby Berlin Jungfernheide', co.wrap(function* (t) { - const nearby = yield client.nearby(52.530273, 13.299433, { + const nearby = yield client.nearby({ + type: 'location', + latitude: 52.530273, + longitude: 13.299433 + }, { results: 2, distance: 400 }) diff --git a/test/vbb.js b/test/vbb.js index 156a7580..71371a57 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -288,7 +288,11 @@ test('departures at 7-digit station', co.wrap(function* (t) { test('nearby', co.wrap(function* (t) { // Berliner Str./Bundesallee - const nearby = yield client.nearby(52.4873452, 13.3310411, {distance: 200}) + const nearby = yield client.nearby({ + type: 'location', + latitude: 52.4873452, + longitude: 13.3310411 + }, {distance: 200}) t.ok(Array.isArray(nearby)) for (let n of nearby) { From 951c26c45acc87d900a5bf518a0d483fade7470b Mon Sep 17 00:00:00 2001 From: Jannis R Date: Fri, 5 Jan 2018 15:01:32 +0100 Subject: [PATCH 71/73] minor docs improvements --- docs/departures.md | 2 +- docs/journey-leg.md | 2 +- docs/journeys.md | 2 +- docs/locations.md | 2 +- docs/nearby.md | 2 +- docs/radar.md | 2 +- readme.md | 8 ++++---- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/departures.md b/docs/departures.md index b0217f14..60756dde 100644 --- a/docs/departures.md +++ b/docs/departures.md @@ -35,7 +35,7 @@ With `opt`, you can override the default options, which look like this: You may pass the `journeyId` field into [`journeyLeg(ref, lineName, [opt])`](journey-leg.md) to get details on the vehicle's journey. -As an example, we're going to use the VBB profile: +As an example, we're going to use the [VBB profile](../p/vbb): ```js const createClient = require('hafas-client') diff --git a/docs/journey-leg.md b/docs/journey-leg.md index 9538199f..b5f24afe 100644 --- a/docs/journey-leg.md +++ b/docs/journey-leg.md @@ -33,7 +33,7 @@ With `opt`, you can override the default options, which look like this: *Note:* As stated in the [*Friendly Public Transport Format* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/tree/1.0.1), the returned `departure` and `arrival` times include the current delay. The `departureDelay`/`arrivalDelay` fields express how much they differ from the schedule. -As an example, we're going to use the VBB profile: +As an example, we're going to use the [VBB profile](../p/vbb): ```js const createClient = require('hafas-client') diff --git a/docs/journeys.md b/docs/journeys.md index 47ec0c9e..3aa30ae2 100644 --- a/docs/journeys.md +++ b/docs/journeys.md @@ -65,7 +65,7 @@ With `opt`, you can override the default options, which look like this: *Note:* As stated in the [*Friendly Public Transport Format* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/tree/1.0.1), the returned `departure` and `arrival` times include the current delay. The `departureDelay`/`arrivalDelay` fields express how much they differ from the schedule. -As an example, we're going to use the VBB profile: +As an example, we're going to use the [VBB profile](../p/vbb): ```js const createClient = require('hafas-client') diff --git a/docs/locations.md b/docs/locations.md index 6f8b439d..8b9d17ab 100644 --- a/docs/locations.md +++ b/docs/locations.md @@ -16,7 +16,7 @@ With `opt`, you can override the default options, which look like this: ## Response -As an example, we're going to use the VBB profile: +As an example, we're going to use the [VBB profile](../p/vbb): ```js const createClient = require('hafas-client') diff --git a/docs/nearby.md b/docs/nearby.md index 3424e281..397162f0 100644 --- a/docs/nearby.md +++ b/docs/nearby.md @@ -16,7 +16,7 @@ With `opt`, you can override the default options, which look like this: ## Response -As an example, we're going to use the VBB profile: +As an example, we're going to use the [VBB profile](../p/vbb): ```js const createClient = require('hafas-client') diff --git a/docs/radar.md b/docs/radar.md index 4db46136..b1be3bfa 100644 --- a/docs/radar.md +++ b/docs/radar.md @@ -18,7 +18,7 @@ With `opt`, you can override the default options, which look like this: *Note:* As stated in the [*Friendly Public Transport Format* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/tree/1.0.1), the returned `departure` and `arrival` times include the current delay. The `departureDelay`/`arrivalDelay` fields express how much they differ from the schedule. -As an example, we're going to use the VBB profile: +As an example, we're going to use the [VBB profile](../p/vbb): ```js const createClient = require('hafas-client') diff --git a/readme.md b/readme.md index e957243a..14f941be 100644 --- a/readme.md +++ b/readme.md @@ -2,8 +2,8 @@ **A client for HAFAS public transport APIs**. Sort of like [public-transport-enabler](https://github.com/schildbach/public-transport-enabler), but with a smaller scope. It also [contains customisations](p) for the following transport networks: -- [Deutsche Bahn](https://en.wikipedia.org/wiki/Deutsche_Bahn) - [docs](p/db/readme.md) – [usage example](p/db/example.js) – [src](p/db/index.js) -- [Berlin public transport](https://en.wikipedia.org/wiki/Verkehrsverbund_Berlin-Brandenburg) - [docs](p/vbb/readme.md) – [usage example](p/vbb/example.js) – [src](p/vbb/index.js) +- [Deutsche Bahn](https://en.wikipedia.org/wiki/Deutsche_Bahn) – [docs](p/db/readme.md) – [usage example](p/db/example.js) – [src](p/db/index.js) +- [Berlin public transport](https://en.wikipedia.org/wiki/Verkehrsverbund_Berlin-Brandenburg) – [docs](p/vbb/readme.md) – [usage example](p/vbb/example.js) – [src](p/vbb/index.js) [![npm version](https://img.shields.io/npm/v/hafas-client.svg)](https://www.npmjs.com/package/hafas-client) [![build status](https://img.shields.io/travis/derhuerst/hafas-client.svg)](https://travis-ci.org/derhuerst/hafas-client) @@ -13,9 +13,9 @@ ## Background -There's [a company called HaCon](http://hacon.de) that sells [a public transport management system called HAFAS](https://de.wikipedia.org/wiki/HAFAS). It is [used by companies all over Europe](https://gist.github.com/derhuerst/2b7ed83bfa5f115125a5) to serve routing and departure information for apps. All those endpoints are similar, with the same terms and API routes, but have slightly different options, filters and enable features. +There's [a company called HaCon](http://hacon.de) that sells [a public transport management system called HAFAS](https://de.wikipedia.org/wiki/HAFAS). It is [used by companies all over Europe](https://gist.github.com/derhuerst/2b7ed83bfa5f115125a5) to serve routing and departure information for apps. All those endpoints are similar, with the same terms and API routes, but have slightly different options, filters and sets of enabled features. -`hafas-client` contains all logic for communicating with these, as well as serialising from and parsing to [*Friendly Public Transport Format (FPTF)* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/blob/1.0.1/spec/readme.md). Endpoint-specific customisations (called "profiles" here) increase the quality of the returned data. +`hafas-client` contains all logic for communicating with these, as well as serialising from and parsing to [*Friendly Public Transport Format (FPTF)* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/blob/1.0.1/spec/readme.md). Endpoint-specific customisations (called *profiles* here) increase the quality of the returned data. ## Installing From d7eca5b4d16db05f790522a56b8374700b50381f Mon Sep 17 00:00:00 2001 From: Jannis R Date: Fri, 5 Jan 2018 18:46:19 +0100 Subject: [PATCH 72/73] expose line operators closes #6 --- docs/departures.md | 13 ++++++++--- docs/journey-leg.md | 7 +++++- docs/journeys.md | 7 +++++- docs/radar.md | 14 ++++++++++-- lib/request.js | 7 +++--- p/db/index.js | 29 +++++++++++++---------- p/vbb/index.js | 43 +++++++++++++++++++--------------- parse/line.js | 56 +++++++++++++++++++++++++-------------------- 8 files changed, 110 insertions(+), 66 deletions(-) diff --git a/docs/departures.md b/docs/departures.md index 60756dde..5cc60754 100644 --- a/docs/departures.md +++ b/docs/departures.md @@ -88,7 +88,12 @@ The response may look like this: metro: false, express: false, night: false, - productCode: 0 + productCode: 0, + operator: { + type: 'operator', + id: 's-bahn-berlin-gmbh', + name: 'S-Bahn Berlin GmbH' + } }, direction: 'S Spandau' }, { @@ -110,7 +115,8 @@ The response may look like this: metro: false, express: false, night: false, - productCode: 0 + productCode: 0, + operator: { /* … */ } }, direction: 'S Westkreuz' }, { @@ -149,7 +155,8 @@ The response may look like this: metro: false, express: false, night: false, - productCode: 1 + productCode: 1, + operator: { /* … */ } }, direction: 'U Rudow' } ] diff --git a/docs/journey-leg.md b/docs/journey-leg.md index b5f24afe..95de8c14 100644 --- a/docs/journey-leg.md +++ b/docs/journey-leg.md @@ -105,7 +105,12 @@ The response looked like this: metro: false, express: false, night: false, - productCode: 0 + productCode: 0, + operator: { + type: 'operator', + id: 's-bahn-berlin-gmbh', + name: 'S-Bahn Berlin GmbH' + } }, direction: 'S Spandau', passed: [ /* … */ ] diff --git a/docs/journeys.md b/docs/journeys.md index 3aa30ae2..86446da8 100644 --- a/docs/journeys.md +++ b/docs/journeys.md @@ -143,7 +143,12 @@ The response may look like this: metro: false, express: false, night: false, - productCode: 0 + productCode: 0, + operator: { + type: 'operator', + id: 's-bahn-berlin-gmbh', + name: 'S-Bahn Berlin GmbH' + } }, direction: 'S Potsdam Hauptbahnhof', passed: [ { diff --git a/docs/radar.md b/docs/radar.md index b1be3bfa..281b2f77 100644 --- a/docs/radar.md +++ b/docs/radar.md @@ -51,7 +51,12 @@ The response may look like this: nr: 9, metro: false, express: false, - night: false + night: false, + operator: { + type: 'operator', + id: 's-bahn-berlin-gmbh', + name: 'S-Bahn Berlin GmbH' + } }, direction: 'S Flughafen Berlin-Schönefeld', trip: 31463, @@ -118,7 +123,12 @@ The response may look like this: nr: 2, metro: true, express: false, - night: false + night: false, + operator: { + type: 'operator', + id: 'berliner-verkehrsbetriebe', + name: 'Berliner Verkehrsbetriebe' + } }, direction: 'Heinersdorf', trip: 26321, diff --git a/lib/request.js b/lib/request.js index c693c092..0eccbb52 100644 --- a/lib/request.js +++ b/lib/request.js @@ -45,15 +45,16 @@ const request = (profile, data) => { if (Array.isArray(c.locL)) { d.locations = c.locL.map(loc => profile.parseLocation(profile, loc)) } - if (Array.isArray(c.prodL)) { - d.lines = c.prodL.map(line => profile.parseLine(profile, line)) - } if (Array.isArray(c.remL)) { d.remarks = c.remL.map(rem => profile.parseRemark(profile, rem)) } if (Array.isArray(c.opL)) { d.operators = c.opL.map(op => profile.parseOperator(profile, op)) } + if (Array.isArray(c.prodL)) { + const parse = profile.parseLine(profile, d.operators) + d.lines = c.prodL.map(parse) + } return d }) } diff --git a/p/db/index.js b/p/db/index.js index 89770c35..4b6dd41c 100644 --- a/p/db/index.js +++ b/p/db/index.js @@ -2,7 +2,7 @@ const crypto = require('crypto') -const _parseLine = require('../../parse/line') +const _createParseLine = require('../../parse/line') const _createParseJourney = require('../../parse/journey') const _formatStation = require('../../format/station') const createParseBitmask = require('../../parse/products-bitmask') @@ -52,19 +52,24 @@ const transformJourneysQuery = (query, opt) => { return query } -const parseLine = (profile, l) => { - const res = _parseLine(profile, l) +const createParseLine = (profile, operators) => { + const parseLine = _createParseLine(profile, operators) - res.mode = res.product = null - if ('class' in res) { - const data = modes.bitmasks[parseInt(res.class)] - if (data) { - res.mode = data.mode - res.product = data.product + const parseLineWithMode = (l) => { + const res = parseLine(l) + + res.mode = res.product = null + if ('class' in res) { + const data = modes.bitmasks[parseInt(res.class)] + if (data) { + res.mode = data.mode + res.product = data.product + } } - } - return res + return res + } + return parseLineWithMode } const createParseJourney = (profile, stations, lines, remarks) => { @@ -144,7 +149,7 @@ const dbProfile = { products: modes.allProducts, // todo: parseLocation - parseLine, + parseLine: createParseLine, parseProducts: createParseBitmask(modes.bitmasks), parseJourney: createParseJourney, diff --git a/p/vbb/index.js b/p/vbb/index.js index dce6261a..2c9325c6 100644 --- a/p/vbb/index.js +++ b/p/vbb/index.js @@ -6,7 +6,7 @@ const parseLineName = require('vbb-parse-line') const parseTicket = require('vbb-parse-ticket') const getStations = require('vbb-stations') -const _parseLine = require('../../parse/line') +const _createParseLine = require('../../parse/line') const _parseLocation = require('../../parse/location') const _createParseJourney = require('../../parse/journey') const _createParseStopover = require('../../parse/stopover') @@ -28,26 +28,31 @@ const transformReqBody = (body) => { return body } -const parseLine = (profile, l) => { - const res = _parseLine(profile, l) +const createParseLine = (profile, operators) => { + const parseLine = _createParseLine(profile, operators) - res.mode = res.product = null - if ('class' in res) { - const data = modes.bitmasks[parseInt(res.class)] - if (data) { - res.mode = data.mode - res.product = data.product + const parseLineWithMode = (l) => { + const res = parseLine(l) + + res.mode = res.product = null + if ('class' in res) { + const data = modes.bitmasks[parseInt(res.class)] + if (data) { + res.mode = data.mode + res.product = data.product + } } + + const details = parseLineName(l.name) + res.symbol = details.symbol + res.nr = details.nr + res.metro = details.metro + res.express = details.express + res.night = details.night + + return res } - - const details = parseLineName(l.name) - res.symbol = details.symbol - res.nr = details.nr - res.metro = details.metro - res.express = details.express - res.night = details.night - - return res + return parseLineWithMode } const parseLocation = (profile, l) => { @@ -169,7 +174,7 @@ const vbbProfile = { parseStationName: shorten, parseLocation, - parseLine, + parseLine: createParseLine, parseProducts: createParseBitmask(modes.bitmasks), parseJourney: createParseJourney, parseDeparture: createParseDeparture, diff --git a/parse/line.js b/parse/line.js index 19f79680..47333a14 100644 --- a/parse/line.js +++ b/parse/line.js @@ -3,31 +3,37 @@ const slugg = require('slugg') // todo: are p.number and p.line ever different? -// todo: operator from p.oprX? -const parseLine = (profile, p) => { - if (!p) return null // todo: handle this upstream - const res = { - type: 'line', - id: null, - name: p.line || p.name, - public: true +const createParseLine = (profile, operators) => { + const parseLine = (p) => { + if (!p) return null // todo: handle this upstream + const res = { + type: 'line', + id: null, + name: p.line || p.name, + public: true + } + + // We don't get a proper line id from the API, so we use the trip nr here. + // todo: find a better way + if (p.prodCtx && p.prodCtx.num) res.id = p.prodCtx.num + // This is terrible, but FPTF demands an ID. Let's pray for VBB to expose an ID. + else if (p.line) res.id = slugg(p.line.trim()) + else if (p.name) res.id = slugg(p.name.trim()) + + if (p.cls) res.class = p.cls + if (p.prodCtx && p.prodCtx.catCode !== undefined) { + res.productCode = +p.prodCtx.catCode + } + + // todo: parse mode, remove from profiles + + if ('number' === typeof p.oprX) { + res.operator = operators[p.oprX] || null + } + + return res } - - // We don't get a proper line id from the API, so we use the trip nr here. - // todo: find a better way - if (p.prodCtx && p.prodCtx.num) res.id = p.prodCtx.num - // This is terrible, but FPTF demands an ID. Let's pray for VBB to expose an ID. - else if (p.line) res.id = slugg(p.line.trim()) - else if (p.name) res.id = slugg(p.name.trim()) - - if (p.cls) res.class = p.cls - if (p.prodCtx && p.prodCtx.catCode !== undefined) { - res.productCode = +p.prodCtx.catCode - } - - // todo: parse mode, remove from profiles - - return res + return parseLine } -module.exports = parseLine +module.exports = createParseLine From 95151ccd0ef1ef7d9ce6d9a80f66a0300c67e54a Mon Sep 17 00:00:00 2001 From: Jannis R Date: Fri, 5 Jan 2018 21:33:03 +0100 Subject: [PATCH 73/73] more docs improvements :memo: --- docs/departures.md | 14 +++++++------- readme.md | 15 +++++++++++---- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/docs/departures.md b/docs/departures.md index 5cc60754..6438b986 100644 --- a/docs/departures.md +++ b/docs/departures.md @@ -65,13 +65,13 @@ The response may look like this: longitude: 13.303846 }, products: { - suburban: true, - subway: false, - tram: false, - bus: true, - ferry: false, - express: false, - regional: true + suburban: true, + subway: false, + tram: false, + bus: true, + ferry: false, + express: false, + regional: true } }, when: '2017-12-17T19:32:00.000+01:00', diff --git a/readme.md b/readme.md index 14f941be..16fc85f5 100644 --- a/readme.md +++ b/readme.md @@ -2,8 +2,10 @@ **A client for HAFAS public transport APIs**. Sort of like [public-transport-enabler](https://github.com/schildbach/public-transport-enabler), but with a smaller scope. It also [contains customisations](p) for the following transport networks: -- [Deutsche Bahn](https://en.wikipedia.org/wiki/Deutsche_Bahn) – [docs](p/db/readme.md) – [usage example](p/db/example.js) – [src](p/db/index.js) -- [Berlin public transport](https://en.wikipedia.org/wiki/Verkehrsverbund_Berlin-Brandenburg) – [docs](p/vbb/readme.md) – [usage example](p/vbb/example.js) – [src](p/vbb/index.js) +HAFAS endpoint | wrapper library? | docs | example code | source code +---------------|------------------|------|---------|------------ +[Deutsche Bahn](https://en.wikipedia.org/wiki/Deutsche_Bahn) | [`vbb-hafas`](https://github.com/derhuerst/vbb-hafas), which has additional features | [docs](p/db/readme.md) | [example code](p/db/example.js) | [src](p/db/index.js) +[Berlin & Brandenburg public transport](https://en.wikipedia.org/wiki/Verkehrsverbund_Berlin-Brandenburg) | [`db-hafas`](https://github.com/derhuerst/db-hafas), which has additional features | [docs](p/vbb/readme.md) | [example code](p/vbb/example.js) | [src](p/vbb/index.js) [![npm version](https://img.shields.io/npm/v/hafas-client.svg)](https://www.npmjs.com/package/hafas-client) [![build status](https://img.shields.io/travis/derhuerst/hafas-client.svg)](https://travis-ci.org/derhuerst/hafas-client) @@ -50,7 +52,7 @@ client.journeys('8011167', '8000261', {results: 1}) .catch(console.error) ``` -The returned [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/promise) will resolved with an array of one `journey` in the [*FPTF* `1.0.1`](https://github.com/public-transport/friendly-public-transport-format/blob/1.0.1/spec/readme.md). +The returned [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/promise) will resolve with an array of one [*FPTF* `journey`](https://github.com/public-transport/friendly-public-transport-format/blob/1.0.1/spec/readme.md#journey). ```js [ { @@ -86,7 +88,12 @@ The returned [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript mode: 'train', product: 'suburban', class: 16, - productCode: 4 + productCode: 4, + operator: { + type: 'operator', + id: 's-bahn-berlin-gmbh', + name: 'S-Bahn Berlin GmbH' + } }, direction: 'Ringbahn ->' }, /* … */ {