mirror of
https://github.com/public-transport/db-vendo-client.git
synced 2025-02-23 07:09:35 +02:00
merge next into tests-rewrite
This commit is contained in:
commit
1bb1ce4f8b
17 changed files with 192 additions and 43 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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?
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -161,3 +162,5 @@ The response may look like this:
|
|||
} ]
|
||||
}, /* … */ ]
|
||||
```
|
||||
|
||||
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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
36
index.js
36
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
|
||||
|
@ -57,8 +58,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.')
|
||||
|
@ -95,8 +96,9 @@ const createClient = (profile, request = _request) => {
|
|||
tickets: false, // return tickets?
|
||||
polylines: false // return leg shapes?
|
||||
}, opt)
|
||||
if (opt.via) opt.via = profile.formatLocation(profile, opt.via)
|
||||
opt.when = opt.when || new Date()
|
||||
if (opt.via) opt.via = profile.formatLocation(profile, opt.via, 'opt.via')
|
||||
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 || {})
|
||||
|
@ -147,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.polylines && d.common.polyL || []
|
||||
const parse = profile.parseJourney(profile, d.locations, d.lines, d.remarks, polylines)
|
||||
|
||||
if (!journeys.earlierRef) journeys.earlierRef = d.outCtxScrB
|
||||
|
@ -281,7 +280,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'},
|
||||
|
@ -295,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
|
||||
|
@ -316,14 +313,18 @@ 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
|
||||
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()
|
||||
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, {
|
||||
|
@ -347,7 +348,8 @@ const createClient = (profile, request = _request) => {
|
|||
.then((d) => {
|
||||
if (!Array.isArray(d.jnyL)) return []
|
||||
|
||||
const parse = profile.parseMovement(profile, d.locations, d.lines, d.remarks)
|
||||
const polylines = opt.polyline && d.common.polyL || []
|
||||
const parse = profile.parseMovement(profile, d.locations, d.lines, d.remarks, polylines)
|
||||
return d.jnyL.map(parse)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]})
|
||||
|
|
|
@ -16,6 +16,7 @@ const types = {
|
|||
parseLine: 'function',
|
||||
parseStationName: 'function',
|
||||
parseLocation: 'function',
|
||||
parsePolyline: 'function',
|
||||
parseMovement: 'function',
|
||||
parseNearby: 'function',
|
||||
parseOperator: 'function',
|
||||
|
|
|
@ -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
|
||||
|
@ -102,7 +102,7 @@ const dbProfile = {
|
|||
|
||||
formatStation,
|
||||
|
||||
journeyLeg: true
|
||||
journeyLeg: true // todo: #49
|
||||
}
|
||||
|
||||
module.exports = dbProfile
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "hafas-client",
|
||||
"description": "JavaScript client for HAFAS public transport APIs.",
|
||||
"version": "2.6.0",
|
||||
"version": "2.7.3",
|
||||
"main": "index.js",
|
||||
"files": [
|
||||
"index.js",
|
||||
|
@ -32,8 +32,11 @@
|
|||
"node": ">=6"
|
||||
},
|
||||
"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",
|
||||
"luxon": "^0.5.8",
|
||||
"p-throttle": "^1.1.0",
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
'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)
|
||||
|
||||
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 ? {
|
||||
|
@ -24,13 +24,26 @@ 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) {
|
||||
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
|
||||
const parse = profile.parsePolyline(locations)
|
||||
res.polyline = p && parse(p) || null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
53
parse/polyline.js
Normal file
53
parse/polyline.js
Normal file
|
@ -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
|
|
@ -13,7 +13,7 @@ HAFAS endpoint | wrapper library | docs | example code | source code
|
|||
[](https://www.npmjs.com/package/hafas-client)
|
||||
[](https://travis-ci.org/public-transport/hafas-client)
|
||||

|
||||
[](https://gitter.im/derhuerst)
|
||||
[](https://gitter.im/public-transport/Lobby)
|
||||
[](https://patreon.com/derhuerst)
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue