From 8ce89f0fe6fd7f7ad53b74d5c22571872237059f Mon Sep 17 00:00:00 2001 From: Jannis R Date: Tue, 17 Apr 2018 18:43:42 +0200 Subject: [PATCH 01/24] formatLocation: more helpful error messages --- format/location.js | 7 ++++--- index.js | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/format/location.js b/format/location.js index 121cd13f..aa7d19d1 100644 --- a/format/location.js +++ b/format/location.js @@ -1,14 +1,15 @@ 'use strict' -const formatLocation = (profile, l) => { +const formatLocation = (profile, l, name = 'location') => { if ('string' === typeof l) return profile.formatStation(l) if ('object' === typeof l && !Array.isArray(l)) { if (l.type === 'station') return profile.formatStation(l.id) if ('string' === typeof l.id) return profile.formatPoi(l) if ('string' === typeof l.address) return profile.formatAddress(l) - throw new Error('invalid location type: ' + l.type) + if (!l.type) throw new Error(`missing ${name}.type`) + throw new Error(`invalid ${name}type: ${l.type}`) } - throw new Error('valid station, address or poi required.') + throw new Error(name + ': valid station, address or poi required.') } module.exports = formatLocation diff --git a/index.js b/index.js index 7d0482b7..7ea60620 100644 --- a/index.js +++ b/index.js @@ -57,8 +57,8 @@ const createClient = (profile, request = _request) => { } const journeys = (from, to, opt = {}) => { - from = profile.formatLocation(profile, from) - to = profile.formatLocation(profile, to) + from = profile.formatLocation(profile, from, 'from') + to = profile.formatLocation(profile, to, 'to') if (('earlierThan' in opt) && ('laterThan' in opt)) { throw new Error('opt.laterThan and opt.laterThan are mutually exclusive.') @@ -94,7 +94,7 @@ const createClient = (profile, request = _request) => { bike: false, // only bike-friendly journeys tickets: false, // return tickets? }, opt) - if (opt.via) opt.via = profile.formatLocation(profile, opt.via) + if (opt.via) opt.via = profile.formatLocation(profile, opt.via, 'opt.via') opt.when = opt.when || new Date() const filters = [ From 47aea604df3d2949840b7f8fcb7750dd2bfb20a4 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Thu, 3 May 2018 10:29:43 +0200 Subject: [PATCH 02/24] fix error message :bug: --- format/location.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/format/location.js b/format/location.js index aa7d19d1..39f0a990 100644 --- a/format/location.js +++ b/format/location.js @@ -7,7 +7,7 @@ const formatLocation = (profile, l, name = 'location') => { if ('string' === typeof l.id) return profile.formatPoi(l) if ('string' === typeof l.address) return profile.formatAddress(l) if (!l.type) throw new Error(`missing ${name}.type`) - throw new Error(`invalid ${name}type: ${l.type}`) + throw new Error(`invalid ${name}.type: ${l.type}`) } throw new Error(name + ': valid station, address or poi required.') } From 0783d9e68a4b2fe63315e3abd92a44d6df2a0bfc Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 13 May 2018 22:42:02 +0200 Subject: [PATCH 03/24] fix Gitter badge --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index d5b042ef..70e885b1 100644 --- a/readme.md +++ b/readme.md @@ -13,7 +13,7 @@ HAFAS endpoint | wrapper library | docs | example code | source code [![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/public-transport/hafas-client.svg)](https://travis-ci.org/public-transport/hafas-client) ![ISC-licensed](https://img.shields.io/github/license/public-transport/hafas-client.svg) -[![chat on gitter](https://badges.gitter.im/derhuerst.svg)](https://gitter.im/derhuerst) +[![chat on gitter](https://badges.gitter.im/public-transport/Lobby.svg)](https://gitter.im/public-transport/Lobby) [![support me on Patreon](https://img.shields.io/badge/support%20me-on%20patreon-fa7664.svg)](https://patreon.com/derhuerst) From c435e253907af5fc244b5847ade5e192b5606423 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Sun, 13 May 2018 22:52:33 +0200 Subject: [PATCH 04/24] parseJourneyLeg: null as fallback direction :bug: --- parse/journey-leg.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parse/journey-leg.js b/parse/journey-leg.js index 65032de2..fd634a9f 100644 --- a/parse/journey-leg.js +++ b/parse/journey-leg.js @@ -48,7 +48,7 @@ const createParseJourneyLeg = (profile, stations, lines, remarks, polylines) => // todo: pull `public` value from `profile.products` res.id = pt.jny.jid res.line = lines[parseInt(pt.jny.prodX)] || null - res.direction = profile.parseStationName(pt.jny.dirTxt) + res.direction = profile.parseStationName(pt.jny.dirTxt) || null if (pt.dep.dPlatfS) res.departurePlatform = pt.dep.dPlatfS if (pt.arr.aPlatfS) res.arrivalPlatform = pt.arr.aPlatfS From 9a0bfc39a45fa69c01dfa0365e34c746c5e43309 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Tue, 15 May 2018 19:39:28 +0200 Subject: [PATCH 05/24] radar: polylines option, return polylines --- docs/radar.md | 3 +++ index.js | 9 +++++++-- parse/movement.js | 27 ++++++++++++++++++--------- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/docs/radar.md b/docs/radar.md index 281b2f77..8c4fa6a8 100644 --- a/docs/radar.md +++ b/docs/radar.md @@ -11,6 +11,7 @@ With `opt`, you can override the default options, which look like this: results: 256, // maximum number of vehicles duration: 30, // compute frames for the next n seconds frames: 3, // nr of frames to compute + polylines: false // return a track shape for each vehicle? } ``` @@ -156,3 +157,5 @@ The response may look like this: } ] }, /* … */ ] ``` + +If you pass `polylines: true`, each result will have a `polyline` field, containing an encoded shape. You can use e.g. [`@mapbox/polyline`](https://www.npmjs.com/package/@mapbox/polyline) to decode it. diff --git a/index.js b/index.js index ac8f5a8e..b08a408b 100644 --- a/index.js +++ b/index.js @@ -313,7 +313,8 @@ const createClient = (profile, request = _request) => { results: 256, // maximum number of vehicles duration: 30, // compute frames for the next n seconds frames: 3, // nr of frames to compute - products: null // optionally an object of booleans + products: null, // optionally an object of booleans + polylines: false // return a track shape for each vehicle? }, opt || {}) opt.when = opt.when || new Date() @@ -339,7 +340,11 @@ const createClient = (profile, request = _request) => { .then((d) => { if (!Array.isArray(d.jnyL)) return [] - const parse = profile.parseMovement(profile, d.locations, d.lines, d.remarks) + let polylines = [] + if (opt.polylines && Array.isArray(d.common.polyL)) { + polylines = d.common.polyL + } + const parse = profile.parseMovement(profile, d.locations, d.lines, d.remarks, polylines) return d.jnyL.map(parse) }) } diff --git a/parse/movement.js b/parse/movement.js index 4a006026..5de96cd9 100644 --- a/parse/movement.js +++ b/parse/movement.js @@ -1,13 +1,12 @@ 'use strict' -const createParseMovement = (profile, locations, lines, remarks) => { +const createParseMovement = (profile, locations, lines, remarks, polylines) => { // 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 pStopover = profile.parseStopover(profile, locations, lines, remarks, m.date) @@ -24,13 +23,23 @@ const createParseMovement = (profile, locations, lines, remarks) => { frames: [] } - 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]] || null, - destination: locations[m.ani.tLocX[i]] || null, - t: m.ani.mSec[i] - }) + if (m.ani) { + if (Array.isArray(m.ani.mSec)) { + for (let i = 0; i < m.ani.mSec.length; i++) { + res.frames.push({ + origin: locations[m.ani.fLocX[i]] || null, + destination: locations[m.ani.tLocX[i]] || null, + t: m.ani.mSec[i] + }) + } + } + + if (m.ani.poly && m.ani.poly.crdEncYX) { + res.polyline = m.ani.poly.crdEncYX + } else if (m.ani.polyG && Array.isArray(m.ani.polyG.polyXL)) { + let p = m.ani.polyG.polyXL[0] + // todo: there can be >1 polyline + res.polyline = polylines[p] && polylines[p].crdEncXY || null } } From 39aac10a178a9b6328a7af1a75722760867a1f9a Mon Sep 17 00:00:00 2001 From: Jannis R Date: Tue, 15 May 2018 23:04:13 +0200 Subject: [PATCH 06/24] parseMovement: empty default polylines arr :bug: --- parse/movement.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parse/movement.js b/parse/movement.js index 5de96cd9..5657f053 100644 --- a/parse/movement.js +++ b/parse/movement.js @@ -1,6 +1,6 @@ 'use strict' -const createParseMovement = (profile, locations, lines, remarks, polylines) => { +const createParseMovement = (profile, locations, lines, remarks, polylines = []) => { // todo: what is m.dirGeo? maybe the speed? // todo: what is m.stopL? // todo: what is m.proc? wut? From 7b5f13524d7bc524428183d0c92da3110b2c1adb Mon Sep 17 00:00:00 2001 From: Jannis R Date: Tue, 15 May 2018 23:49:56 +0200 Subject: [PATCH 07/24] 2.7.0 --- docs/changelog.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 135c5d70..3bc5021a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # Changelog +## `2.7.0` + +- `journeys()`: `polylines` option +- `journeyLeg()`: `polyline` option +- `radar()`: `polylines` option + ## `2.6.0` - 5d10d76 journey legs: parse cycle diff --git a/package.json b/package.json index f1a314b0..bb03ae66 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hafas-client", "description": "JavaScript client for HAFAS public transport APIs.", - "version": "2.6.0", + "version": "2.7.0", "main": "index.js", "files": [ "index.js", From aa480e01a2842df7b0686d48a98acf4c2cafba2b Mon Sep 17 00:00:00 2001 From: Jannis R Date: Wed, 16 May 2018 00:30:27 +0200 Subject: [PATCH 08/24] radar: fix polylines parsing :bug: --- index.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index b08a408b..7ed498ee 100644 --- a/index.js +++ b/index.js @@ -341,7 +341,7 @@ const createClient = (profile, request = _request) => { if (!Array.isArray(d.jnyL)) return [] let polylines = [] - if (opt.polylines && Array.isArray(d.common.polyL)) { + if (opt.polylines && d.common && Array.isArray(d.common.polyL)) { polylines = d.common.polyL } const parse = profile.parseMovement(profile, d.locations, d.lines, d.remarks, polylines) diff --git a/package.json b/package.json index bb03ae66..18ce6e92 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hafas-client", "description": "JavaScript client for HAFAS public transport APIs.", - "version": "2.7.0", + "version": "2.7.1", "main": "index.js", "files": [ "index.js", From a97e0d31e7c5009b1f6c4d2695227c07a51a1a54 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Wed, 16 May 2018 00:42:45 +0200 Subject: [PATCH 09/24] radar: fix polylines parsing :bug:, 2.7.2 --- package.json | 2 +- parse/movement.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 18ce6e92..9df16958 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hafas-client", "description": "JavaScript client for HAFAS public transport APIs.", - "version": "2.7.1", + "version": "2.7.2", "main": "index.js", "files": [ "index.js", diff --git a/parse/movement.js b/parse/movement.js index 5657f053..e2efdf5c 100644 --- a/parse/movement.js +++ b/parse/movement.js @@ -39,7 +39,7 @@ const createParseMovement = (profile, locations, lines, remarks, polylines = []) } else if (m.ani.polyG && Array.isArray(m.ani.polyG.polyXL)) { let p = m.ani.polyG.polyXL[0] // todo: there can be >1 polyline - res.polyline = polylines[p] && polylines[p].crdEncXY || null + res.polyline = polylines[p] && polylines[p].crdEncYX || null } } From 48f2cefb5b2b65a5f84f5c499835c3315ab67322 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Wed, 16 May 2018 19:30:09 +0200 Subject: [PATCH 10/24] parseMovement: expose journey (leg) id --- parse/movement.js | 1 + 1 file changed, 1 insertion(+) diff --git a/parse/movement.js b/parse/movement.js index e2efdf5c..7999eb52 100644 --- a/parse/movement.js +++ b/parse/movement.js @@ -12,6 +12,7 @@ const createParseMovement = (profile, locations, lines, remarks, polylines = []) const res = { direction: profile.parseStationName(m.dirTxt), + journeyId: m.jid || null, trip: m.jid && +m.jid.split('|')[1] || null, // todo: this seems brittle line: lines[m.prodX] || null, location: m.pos ? { From 0840d69ee9cc0fb9b4bf35237fa93f677991bd7d Mon Sep 17 00:00:00 2001 From: Jannis R Date: Wed, 16 May 2018 21:53:33 +0200 Subject: [PATCH 11/24] radar: assert north > south & east > west --- index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.js b/index.js index 6d5c082e..ab7f03e6 100644 --- a/index.js +++ b/index.js @@ -316,6 +316,8 @@ const createClient = (profile, request = _request) => { if ('number' !== typeof west) throw new Error('west must be a number.') if ('number' !== typeof south) throw new Error('south must be a number.') if ('number' !== typeof east) throw new Error('east must be a number.') + if (north <= south) throw new Error('north must be larger than south.') + if (east <= west) throw new Error('east must be larger than west.') opt = Object.assign({ results: 256, // maximum number of vehicles From 0ef03015e06daabf9d1b843ef75d1bd52109713c Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 21 May 2018 17:10:42 +0200 Subject: [PATCH 12/24] validate opt.when :boom: --- index.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index ab7f03e6..c533168f 100644 --- a/index.js +++ b/index.js @@ -31,7 +31,8 @@ const createClient = (profile, request = _request) => { 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() + opt.when = new Date(opt.when || Date.now()) + if (Number.isNaN(+opt.when)) throw new Error('opt.when is invalid') const products = profile.formatProductsFilter(opt.products || {}) const dir = opt.direction ? profile.formatStation(opt.direction) : null @@ -96,7 +97,8 @@ const createClient = (profile, request = _request) => { polylines: false // return leg shapes? }, opt) if (opt.via) opt.via = profile.formatLocation(profile, opt.via) - opt.when = opt.when || new Date() + opt.when = new Date(opt.when || Date.now()) + if (Number.isNaN(+opt.when)) throw new Error('opt.when is invalid') const filters = [ profile.formatProductsFilter(opt.products || {}) @@ -281,7 +283,8 @@ const createClient = (profile, request = _request) => { passedStations: true, // return stations on the way? polyline: false }, opt) - opt.when = opt.when || new Date() + opt.when = new Date(opt.when || Date.now()) + if (Number.isNaN(+opt.when)) throw new Error('opt.when is invalid') return request(profile, { cfg: {polyEnc: 'GPA'}, @@ -326,7 +329,8 @@ const createClient = (profile, request = _request) => { products: null, // optionally an object of booleans polylines: false // return a track shape for each vehicle? }, opt || {}) - opt.when = opt.when || new Date() + opt.when = new Date(opt.when || Date.now()) + if (Number.isNaN(+opt.when)) throw new Error('opt.when is invalid') const durationPerStep = opt.duration / Math.max(opt.frames, 1) * 1000 return request(profile, { From deefeb1f647524595a7747a48558564bc72c2ce3 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Wed, 16 May 2018 21:07:05 +0200 Subject: [PATCH 13/24] add parsePolyline to default profile --- lib/default-profile.js | 2 ++ package.json | 2 ++ parse/polyline.js | 53 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 parse/polyline.js diff --git a/lib/default-profile.js b/lib/default-profile.js index 340de25f..57d7eb36 100644 --- a/lib/default-profile.js +++ b/lib/default-profile.js @@ -6,6 +6,7 @@ const parseJourneyLeg = require('../parse/journey-leg') const parseJourney = require('../parse/journey') const parseLine = require('../parse/line') const parseLocation = require('../parse/location') +const parsePolyline = require('../parse/polyline') const parseMovement = require('../parse/movement') const parseNearby = require('../parse/nearby') const parseOperator = require('../parse/operator') @@ -42,6 +43,7 @@ const defaultProfile = { parseLine, parseStationName: id, parseLocation, + parsePolyline, parseMovement, parseNearby, parseOperator, diff --git a/package.json b/package.json index de06f070..d654d719 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,10 @@ "node": ">=6" }, "dependencies": { + "@mapbox/polyline": "^1.0.0", "capture-stack-trace": "^1.0.0", "fetch-ponyfill": "^6.0.0", + "gps-distance": "0.0.4", "lodash": "^4.17.5", "luxon": "^0.5.8", "p-throttle": "^1.1.0", diff --git a/parse/polyline.js b/parse/polyline.js new file mode 100644 index 00000000..5cdcff0c --- /dev/null +++ b/parse/polyline.js @@ -0,0 +1,53 @@ +'use strict' + +const {toGeoJSON} = require('@mapbox/polyline') +const distance = require('gps-distance') + +const createParsePolyline = (locations) => { + // todo: what is p.delta? + // todo: what is p.type? + // todo: what is p.crdEncS? + // todo: what is p.crdEncF? + const parsePolyline = (p) => { + const shape = toGeoJSON(p.crdEncYX) + if (shape.coordinates.length === 0) return null + + const res = shape.coordinates.map(crd => ({ + type: 'Feature', + properties: {}, + geometry: { + type: 'Point', + coordinates: crd + } + })) + + if (Array.isArray(p.ppLocRefL)) { + for (let ref of p.ppLocRefL) { + const p = res[ref.ppIdx] + const loc = locations[ref.locX] + if (p && loc) p.properties = loc + } + + // Often there is one more point right next to each point at a station. + // We filter them here if they are < 5m from each other. + for (let i = 1; i < res.length; i++) { + const p1 = res[i - 1].geometry.coordinates + const p2 = res[i].geometry.coordinates + const d = distance(p1[1], p1[0], p2[1], p2[0]) + if (d >= .005) continue + const l1 = Object.keys(res[i - 1].properties).length + const l2 = Object.keys(res[i].properties).length + if (l1 === 0 && l2 > 0) res.splice(i - 1, 1) + else if (l2 === 0 && l1 > 0) res.splice(i, 1) + } + } + + return { + type: 'FeatureCollection', + features: res + } + } + return parsePolyline +} + +module.exports = createParsePolyline From 431574b7569754985e17dcaf66ccda67cb9408e3 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Wed, 16 May 2018 21:07:43 +0200 Subject: [PATCH 14/24] parseJourneyLeg, parseMovement: use parsePolyline :boom:, refactor --- index.js | 15 +++------------ parse/journey-leg.js | 5 +++-- parse/movement.js | 13 ++++++++----- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/index.js b/index.js index c533168f..d2410c70 100644 --- a/index.js +++ b/index.js @@ -149,10 +149,7 @@ const createClient = (profile, request = _request) => { .then((d) => { if (!Array.isArray(d.outConL)) return [] - let polylines = [] - if (opt.polylines && Array.isArray(d.common.polyL)) { - polylines = d.common.polyL - } + const polylines = opt.polyline && d.common.polyL || [] const parse = profile.parseJourney(profile, d.locations, d.lines, d.remarks, polylines) if (!journeys.earlierRef) journeys.earlierRef = d.outCtxScrB @@ -298,10 +295,7 @@ const createClient = (profile, request = _request) => { } }) .then((d) => { - let polylines = [] - if (opt.polyline && Array.isArray(d.common.polyL)) { - polylines = d.common.polyL - } + const polylines = opt.polyline && d.common.polyL || [] const parse = profile.parseJourneyLeg(profile, d.locations, d.lines, d.remarks, polylines) const leg = { // pretend the leg is contained in a journey @@ -354,10 +348,7 @@ const createClient = (profile, request = _request) => { .then((d) => { if (!Array.isArray(d.jnyL)) return [] - let polylines = [] - if (opt.polylines && d.common && Array.isArray(d.common.polyL)) { - polylines = d.common.polyL - } + const polylines = opt.polyline && d.common.polyL || [] const parse = profile.parseMovement(profile, d.locations, d.lines, d.remarks, polylines) return d.jnyL.map(parse) }) diff --git a/parse/journey-leg.js b/parse/journey-leg.js index fd634a9f..bbc3f9da 100644 --- a/parse/journey-leg.js +++ b/parse/journey-leg.js @@ -36,9 +36,10 @@ const createParseJourneyLeg = (profile, stations, lines, remarks, polylines) => if (pt.jny && pt.jny.polyG) { let p = pt.jny.polyG.polyXL - p = p && polylines[p[0]] + p = Array.isArray(p) && polylines[p[0]] // todo: there can be >1 polyline - res.polyline = p && p.crdEncYX || null + const parse = profile.parsePolyline(stations) + res.polyline = p && parse(p) || null } if (pt.type === 'WALK') { diff --git a/parse/movement.js b/parse/movement.js index 7999eb52..8a3ab401 100644 --- a/parse/movement.js +++ b/parse/movement.js @@ -35,12 +35,15 @@ const createParseMovement = (profile, locations, lines, remarks, polylines = []) } } - if (m.ani.poly && m.ani.poly.crdEncYX) { - res.polyline = m.ani.poly.crdEncYX - } else if (m.ani.polyG && Array.isArray(m.ani.polyG.polyXL)) { - let p = m.ani.polyG.polyXL[0] + if (m.ani.poly) { + const parse = profile.parsePolyline(locations) + res.polyline = parse(m.ani.poly) + } else if (m.ani.polyG) { + let p = m.ani.polyG.polyXL + p = Array.isArray(p) && polylines[p[0]] // todo: there can be >1 polyline - res.polyline = polylines[p] && polylines[p].crdEncYX || null + const parse = profile.parsePolyline(locations) + res.polyline = p && parse(p) || null } } From 8c896fe8c1b88905e565852afed4c2c9d5cfc1f9 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Wed, 16 May 2018 21:27:12 +0200 Subject: [PATCH 15/24] docs for new polyline format :memo: --- docs/journey-leg.md | 64 ++++++++++++++++++++++++++++++++++++++++++++- docs/journeys.md | 2 +- docs/radar.md | 2 +- 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/docs/journey-leg.md b/docs/journey-leg.md index f54b0e6e..e21293c7 100644 --- a/docs/journey-leg.md +++ b/docs/journey-leg.md @@ -118,4 +118,66 @@ The response looked like this: } ``` -If you pass `polyline: true`, the leg will have a `polyline` field, containing an encoded shape. You can use e.g. [`@mapbox/polyline`](https://www.npmjs.com/package/@mapbox/polyline) to decode it. +### `polyline` option + +If you pass `polyline: true`, the leg will have a `polyline` field, containing a [GeoJSON](http://geojson.org) [`FeatureCollection`](https://tools.ietf.org/html/rfc7946#section-3.3) of [`Point`s](https://tools.ietf.org/html/rfc7946#appendix-A.1). Every `Point` next to a station will have `properties` containing the station's metadata. + +We'll look at an example for *U6* from *Alt-Mariendorf* to *Alt-Tegel*, taken from the [VBB profile](../p/vbb): + +```js +{ + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: { + type: 'station', + id: '900000070301', + name: 'U Alt-Mariendorf', + /* … */ + }, + geometry: { + type: 'Point', + coordinates: [13.3875, 52.43993] // longitude, latitude + } + }, + /* … */ + { + type: 'Feature', + properties: { + type: 'station', + id: '900000017101', + name: 'U Mehringdamm', + /* … */ + }, + geometry: { + type: 'Point', + coordinates: [13.38892, 52.49448] // longitude, latitude + } + }, + /* … */ + { + // intermediate point, without associated station + type: 'Feature', + properties: {}, + geometry: { + type: 'Point', + coordinates: [13.28599, 52.58742] // longitude, latitude + } + }, + { + type: 'Feature', + properties: { + type: 'station', + id: '900000089301', + name: 'U Alt-Tegel', + /* … */ + }, + geometry: { + type: 'Point', + coordinates: [13.28406, 52.58915] // longitude, latitude + } + } + ] +} +``` diff --git a/docs/journeys.md b/docs/journeys.md index 1b6d0051..016f052c 100644 --- a/docs/journeys.md +++ b/docs/journeys.md @@ -262,4 +262,4 @@ departure of last journey 2017-12-17T19:07:00.000+01:00 departure of first (later) journey 2017-12-17T19:19:00.000+01:00 ``` -If you pass `polylines: true`, each journey leg will have a `polyline` field, containing an encoded shape. You can use e.g. [`@mapbox/polyline`](https://www.npmjs.com/package/@mapbox/polyline) to decode it. +If you pass `polylines: true`, each journey leg will have a `polyline` field. Refer to [the section in the `journeyLeg()` docs](journey-leg.md#polyline-option) for details. diff --git a/docs/radar.md b/docs/radar.md index ab00b767..e42155c0 100644 --- a/docs/radar.md +++ b/docs/radar.md @@ -163,4 +163,4 @@ The response may look like this: }, /* … */ ] ``` -If you pass `polylines: true`, each result will have a `polyline` field, containing an encoded shape. You can use e.g. [`@mapbox/polyline`](https://www.npmjs.com/package/@mapbox/polyline) to decode it. +If you pass `polylines: true`, each journey leg will have a `polyline` field, as documented in [the corresponding section in the `journeyLeg()` docs](journey-leg.md#polyline-option), with the exception that station info is missing. From 03b9ab9a839fa15332b8dd2bd2b4ba47eebddf9c Mon Sep 17 00:00:00 2001 From: Jannis R Date: Wed, 16 May 2018 22:28:59 +0200 Subject: [PATCH 16/24] expect parsePolyline in lib/validate-profile --- lib/validate-profile.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/validate-profile.js b/lib/validate-profile.js index 21c86829..88e3d0c1 100644 --- a/lib/validate-profile.js +++ b/lib/validate-profile.js @@ -16,6 +16,7 @@ const types = { parseLine: 'function', parseStationName: 'function', parseLocation: 'function', + parsePolyline: 'function', parseMovement: 'function', parseNearby: 'function', parseOperator: 'function', From 00b1eb387c7c6b8e65053f04597e750a0d578f17 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 21 May 2018 12:15:40 +0200 Subject: [PATCH 17/24] crypto.createHash -> create-hash, 2.7.3 closes #52 --- lib/request.js | 4 ++-- package.json | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/request.js b/lib/request.js index 084fe26b..3cde8fbb 100644 --- a/lib/request.js +++ b/lib/request.js @@ -1,6 +1,6 @@ 'use strict' -const crypto = require('crypto') +const createHash = require('create-hash') let captureStackTrace = () => {} if (process.env.NODE_DEBUG === 'hafas-client') { captureStackTrace = require('capture-stack-trace') @@ -9,7 +9,7 @@ const {stringify} = require('query-string') const Promise = require('pinkie-promise') const {fetch} = require('fetch-ponyfill')({Promise}) -const md5 = input => crypto.createHash('md5').update(input).digest() +const md5 = input => createHash('md5').update(input).digest() const request = (profile, data) => { const body = profile.transformReqBody({lang: 'en', svcReqL: [data]}) diff --git a/package.json b/package.json index d654d719..7ab50742 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hafas-client", "description": "JavaScript client for HAFAS public transport APIs.", - "version": "2.7.2", + "version": "2.7.3", "main": "index.js", "files": [ "index.js", @@ -34,6 +34,7 @@ "dependencies": { "@mapbox/polyline": "^1.0.0", "capture-stack-trace": "^1.0.0", + "create-hash": "^1.2.0", "fetch-ponyfill": "^6.0.0", "gps-distance": "0.0.4", "lodash": "^4.17.5", From e47961938707d0f8f7b523da49070a56a50fadcf Mon Sep 17 00:00:00 2001 From: Jannis R Date: Thu, 24 May 2018 14:06:35 +0200 Subject: [PATCH 18/24] cherry-pick 16c3f01: DB: 1.16 protocol, journeyLeg, fix polylines :bug: --- p/db/index.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/p/db/index.js b/p/db/index.js index 812cb94d..773f8ab3 100644 --- a/p/db/index.js +++ b/p/db/index.js @@ -10,7 +10,7 @@ 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.ver = '1.16' body.auth = {type: 'AID', aid: 'n91dB8Z77MLdoR0K'} return body @@ -34,8 +34,8 @@ const transformJourneysQuery = (query, opt) => { return query } -const createParseJourney = (profile, stations, lines, remarks) => { - const parseJourney = _createParseJourney(profile, stations, lines, remarks) +const createParseJourney = (profile, stations, lines, remarks, polylines) => { + const parseJourney = _createParseJourney(profile, stations, lines, remarks, polylines) // todo: j.sotRating, j.conSubscr, j.isSotCon, j.showARSLink, k.sotCtxt // todo: j.conSubscr, j.showARSLink, j.useableTime @@ -96,7 +96,9 @@ const dbProfile = { // todo: parseLocation parseJourney: createParseJourney, - formatStation + formatStation, + + journeyLeg: true // todo: #49 } module.exports = dbProfile From 6063fa6651aa47c661f4267cc2514d8d68b13835 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Thu, 24 May 2018 21:54:25 +0200 Subject: [PATCH 19/24] journeys: fix polylines option :bug: --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index eeecdd0e..69f831ea 100644 --- a/index.js +++ b/index.js @@ -149,7 +149,7 @@ const createClient = (profile, request = _request) => { .then((d) => { if (!Array.isArray(d.outConL)) return [] - const polylines = opt.polyline && d.common.polyL || [] + const polylines = opt.polylines && d.common.polyL || [] const parse = profile.parseJourney(profile, d.locations, d.lines, d.remarks, polylines) if (!journeys.earlierRef) journeys.earlierRef = d.outCtxScrB From 187d2ac04e0ad72d48519325c47e92502d68901c Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 23 Apr 2018 15:58:27 +0200 Subject: [PATCH 20/24] Nah.sh: enable radar despite failing test see #34 for more details --- p/nahsh/index.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/p/nahsh/index.js b/p/nahsh/index.js index 563f1ad2..28d1a08d 100644 --- a/p/nahsh/index.js +++ b/p/nahsh/index.js @@ -2,6 +2,7 @@ const _parseLocation = require('../../parse/location') const _createParseJourney = require('../../parse/journey') +const _createParseMovement = require('../../parse/movement') const products = require('./products') @@ -77,6 +78,19 @@ const createParseJourney = (profile, stations, lines, remarks) => { return parseJourneyWithTickets } +const createParseMovement = (profile, locations, lines, remarks) => { + const _parseMovement = _createParseMovement(profile, locations, lines, remarks) + const parseMovement = (m) => { + const res = _parseMovement(m) + // filter out empty nextStops entries + res.nextStops = res.nextStops.filter((f) => { + return f.station !== null || f.arrival !== null || f.departure !== null + }) + return res + } + return parseMovement +} + const nahshProfile = { locale: 'de-DE', timezone: 'Europe/Berlin', @@ -87,9 +101,10 @@ const nahshProfile = { parseLocation, parseJourney: createParseJourney, + parseMovement: createParseMovement, journeyLeg: true, - radar: false // todo: see #34 + radar: true // todo: see #34 } module.exports = nahshProfile From 7541bcae667c90c2ff0b7f126340b74fe8c4c4a3 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 7 May 2018 11:47:49 +0200 Subject: [PATCH 21/24] adapt nah.sh radar test to #34 --- test/nahsh.js | 53 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/test/nahsh.js b/test/nahsh.js index c125274b..3d0b491d 100644 --- a/test/nahsh.js +++ b/test/nahsh.js @@ -379,9 +379,41 @@ test('location', co(function* (t) { t.end() })) -// todo: see #34 -test.skip('radar Kiel', co(function* (t) { - const vehicles = yield client.radar(54.4, 10.0, 54.2, 10.2, { +test('radar Kiel', co(function* (t) { + const fakeStation = (s) => { + const fake = Object.assign({ + products: { + nationalExp: true, + national: false, + interregional: true, + regional: false, + suburban: true, + bus: false, + ferry: true, + subway: false, + tram: true, + onCall: false + } + }, s) + if (s.name === null) fake.name = 'foo' + return fake + } + const _assertValidStation = (t, s, coordsOptional = false) => { + assertValidStation(t, fakeStation(s), coordsOptional) + } + const _assertValidStopover = (t, s, coordsOptional = false) => { + const fake = Object.assign({}, s, { + station: fakeStation(s.station) + }) + assertValidStopover(t, fake, coordsOptional) + } + + const vehicles = yield client.radar({ + north: 54.4, + west: 10.0, + south: 54.2, + east: 10.2 + }, { duration: 5 * 60, when }) @@ -402,7 +434,7 @@ test.skip('radar Kiel', co(function* (t) { t.ok(Array.isArray(v.nextStops)) for (let st of v.nextStops) { - assertValidStopover(t, st, true) + _assertValidStopover(t, st, true) if (st.arrival) { t.equal(typeof st.arrival, 'string') @@ -419,10 +451,15 @@ test.skip('radar Kiel', co(function* (t) { t.ok(Array.isArray(v.frames)) for (let f of v.frames) { - assertValidStation(t, f.origin, true) - assertValidStationProducts(t, f.origin.products) - assertValidStation(t, f.destination, true) - assertValidStationProducts(t, f.destination.products) + // todo: see #34 + _assertValidStation(t, f.origin, true) + if (f.origin.products) { + assertValidStationProducts(t, f.origin.products) + } + _assertValidStation(t, f.destination, true) + if (f.destination.products) { + assertValidStationProducts(t, f.destination.products) + } t.equal(typeof f.t, 'number') } } From 57f3cdb7e014e692f2b47b4e8a8ca6b1b13cbc83 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 7 May 2018 11:48:08 +0200 Subject: [PATCH 22/24] parseLocation: null as fallback station name --- parse/location.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parse/location.js b/parse/location.js index 1659ea92..44f34f5c 100644 --- a/parse/location.js +++ b/parse/location.js @@ -19,7 +19,7 @@ const parseLocation = (profile, l, lines) => { const station = { type: 'station', id: l.extId, - name: profile.parseStationName(l.name), + name: l.name ? profile.parseStationName(l.name) : null, location: 'number' === typeof res.latitude ? res : null } From 634b044bc513afd1f9e519d8fdff8bbcf0f99951 Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 28 May 2018 20:31:47 +0200 Subject: [PATCH 23/24] nah.sh tests: fix products validation :bug: --- test/nahsh.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/nahsh.js b/test/nahsh.js index 3d0b491d..c7b377af 100644 --- a/test/nahsh.js +++ b/test/nahsh.js @@ -58,10 +58,9 @@ const assertIsKielHbf = (t, s) => { // todo: DRY with assertValidStationProducts // todo: DRY with other tests -const assertValidProducts = (t, p) => { - for (let product of allProducts) { - product = product.product // wat - t.equal(typeof p[product], 'boolean', 'product ' + p + ' must be a boolean') +const assertValidProducts = (t, products) => { + for (let p of allProducts) { + t.equal(typeof products[p.id], 'boolean', `product ${p.id} must be a boolean`) } } From a356a26e2fbd34940d7b3634b1c0552d41d0108b Mon Sep 17 00:00:00 2001 From: Jannis R Date: Mon, 28 May 2018 20:32:10 +0200 Subject: [PATCH 24/24] throw if 0 products enabled :boom: This should have been the case previously, but the test didn't fail. --- format/products-filter.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/format/products-filter.js b/format/products-filter.js index 8ee8ffca..e4252f8f 100644 --- a/format/products-filter.js +++ b/format/products-filter.js @@ -16,12 +16,14 @@ const createFormatProductsFilter = (profile) => { if (!isObj(filter)) throw new Error('products filter must be an object') filter = Object.assign({}, defaultProducts, filter) - let res = 0 + let res = 0, products = 0 for (let product in filter) { if (!hasProp(filter, product) || filter[product] !== true) continue if (!byProduct[product]) throw new Error('unknown product ' + product) + products++ for (let bitmask of byProduct[product].bitmasks) res += bitmask } + if (products === 0) throw new Error('no products used') return { type: 'PROD',