merge next into tests-rewrite

This commit is contained in:
Jannis R 2018-05-30 14:22:23 +02:00
commit 1bb1ce4f8b
No known key found for this signature in database
GPG key ID: 0FE83946296A88A5
17 changed files with 192 additions and 43 deletions

View file

@ -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

View file

@ -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
}
}
]
}
```

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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',

View file

@ -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)
})
}

View file

@ -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,

View file

@ -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]})

View file

@ -16,6 +16,7 @@ const types = {
parseLine: 'function',
parseStationName: 'function',
parseLocation: 'function',
parsePolyline: 'function',
parseMovement: 'function',
parseNearby: 'function',
parseOperator: 'function',

View file

@ -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

View file

@ -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",

View file

@ -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') {

View file

@ -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
}

View file

@ -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
View 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

View file

@ -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)