merge pull request #22 from jfilter/master

add INSA by NASA
This commit is contained in:
Jannis Redmann 2018-03-13 21:16:45 +01:00 committed by GitHub
commit 8802e3df97
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 623 additions and 23 deletions

View file

@ -7,7 +7,7 @@ const types = {
transformReqBody: 'function',
transformJourneysQuery: 'function',
products: 'object',
products: 'array',
parseDateTime: 'function',
parseDeparture: 'function',
@ -36,7 +36,11 @@ const types = {
const validateProfile = (profile) => {
for (let key of Object.keys(types)) {
const type = types[key]
if (type !== typeof profile[key]) {
if (type === 'array') {
if (!Array.isArray(profile[key])) {
throw new Error(`profile.${key} must be an array.`)
}
} else if (type !== typeof profile[key]) {
throw new Error(`profile.${key} must be a ${type}.`)
}
if (type === 'object' && profile[key] === null) {

View file

@ -112,7 +112,8 @@ const defaultProducts = {
national: true,
nationalExp: true,
regional: true,
regionalExp: true
regionalExp: true,
taxi: false
}
const formatProducts = (products) => {
products = Object.assign(Object.create(null), defaultProducts, products)
@ -140,7 +141,7 @@ const dbProfile = {
// todo: parseLocation
parseLine: createParseLine,
parseProducts: createParseBitmask(modes.bitmasks),
parseProducts: createParseBitmask(modes.allProducts, defaultProducts),
parseJourney: createParseJourney,
formatStation,

29
p/insa/example.js Normal file
View file

@ -0,0 +1,29 @@
'use strict'
const createClient = require('../..')
const insaProfile = require('.')
const client = createClient(insaProfile)
// from Magdeburg-Neustadt to Magdeburg-Buckau
client.journeys('008010226', '008013456', {results: 1})
// client.departures('008010226', { duration: 5 })
// client.locations('Magdeburg Hbf', {results: 2})
// client.locations('Kunstmuseum Kloster Unser Lieben Frauen Magdeburg', {results: 2})
// client.location('008010226') // Magdeburg-Neustadt
// client.nearby({
// type: 'location',
// latitude: 52.148842,
// longitude: 11.641705
// }, {distance: 200})
// client.radar(52.148364, 11.600826, 52.108486, 11.651451, {results: 10})
// .then(([journey]) => {
// const leg = journey.legs[0]
// return client.journeyLeg(leg.id, leg.line.name)
// })
.then(data => {
console.log(require('util').inspect(data, { depth: null }))
})
.catch(console.error)

82
p/insa/index.js Normal file
View file

@ -0,0 +1,82 @@
'use strict'
const _createParseLine = require('../../parse/line')
const products = require('./products')
const createParseBitmask = require('../../parse/products-bitmask')
const createFormatBitmask = require('../../format/products-bitmask')
const defaultProducts = {
nationalExp: true,
national: true,
regional: true,
suburban: true,
bus: true,
tram: true,
tourismTrain: true,
}
const transformReqBody = (body) => {
body.client = {
type: 'IPH',
id: 'NASA',
v: '4000200',
name: 'nasaPROD',
os: 'iPhone OS 11.2.5'
}
body.ver = '1.11'
body.auth = {aid: "nasa-apps"}
body.lang = 'en' // todo: `de`?
return body
}
const createParseLine = (profile, operators) => {
const parseLine = _createParseLine(profile, operators)
const parseLineWithMode = (l) => {
const res = parseLine(l)
res.mode = res.product = null
if ('class' in res) {
const data = products.bitmasks[parseInt(res.class)]
if (data) {
res.mode = data.mode
res.product = data.product
}
}
return res
}
return parseLineWithMode
}
const formatProducts = (products) => {
products = Object.assign(Object.create(null), defaultProducts, products)
return {
type: 'PROD',
mode: 'INC',
value: formatBitmask(products) + ''
}
}
const formatBitmask = createFormatBitmask(products)
const insaProfile = {
locale: 'de-DE',
timezone: 'Europe/Berlin',
endpoint: 'http://reiseauskunft.insa.de/bin/mgate.exe',
transformReqBody,
products: products.allProducts,
parseProducts: createParseBitmask(products.allProducts, defaultProducts),
formatProducts,
parseLine: createParseLine,
journeyLeg: true,
radar: true
}
module.exports = insaProfile;

82
p/insa/products.js Normal file
View file

@ -0,0 +1,82 @@
'use strict'
// TODO Jannis R.: DRY
const p = {
nationalExp: {
bitmask: 1,
name: 'InterCityExpress',
short: 'ICE',
mode: 'train',
product: 'nationalExp'
},
national: {
bitmask: 2,
name: 'InterCity & EuroCity',
short: 'IC/EC',
mode: 'train',
product: 'national'
},
regional: {
bitmask: 8,
name: 'RegionalExpress & RegionalBahn',
short: 'RE/RB',
mode: 'train',
product: 'regional'
},
suburban: {
bitmask: 16,
name: 'S-Bahn',
short: 'S',
mode: 'train',
product: 'suburban'
},
tram: {
bitmask: 32,
name: 'Tram',
short: 'T',
mode: 'train',
product: 'tram'
},
bus: {
bitmask: 64+128,
name: 'Bus',
short: 'B',
mode: 'bus',
product: 'bus'
},
tourismTrain: {
bitmask: 256,
name: 'Tourism Train',
short: 'TT',
mode: 'train',
product: 'tourismTrain'
},
unknown: {
bitmask: 0,
name: 'unknown',
short: '?',
product: 'unknown'
}
}
p.bitmasks = []
p.bitmasks[1] = p.nationalExp
p.bitmasks[2] = p.national
p.bitmasks[8] = p.regional
p.bitmasks[16] = p.suburban
p.bitmasks[32] = p.tram
p.bitmasks[64] = p.bus
p.bitmasks[128] = p.bus
p.bitmasks[256] = p.tourismTrain
p.allProducts = [
p.nationalExp,
p.national,
p.regional,
p.suburban,
p.tram,
p.bus,
p.tourismTrain
]
module.exports = p

18
p/insa/readme.md Normal file
View file

@ -0,0 +1,18 @@
# INSA profile for `hafas-client`
The [Nahverkehr Sachsen-Anhalt (NASA)](https://de.wikipedia.org/wiki/Nahverkehrsservice_Sachsen-Anhalt) offers [Informationssystem Nahverkehr Sachsen-Anhalt (INSA)](https://insa.de) to distribute their public transport data.
## Usage
```js
const createClient = require('hafas-client')
const insaProfile = require('hafas-client/p/insa')
// create a client with INSA profile
const client = createClient(insaProfile)
```
## Customisations
- parses *INSA*-specific products (such as *Tourism Train*)

View file

@ -113,7 +113,7 @@ const oebbProfile = {
products: products.allProducts,
parseProducts: createParseBitmask(products.bitmasks),
parseProducts: createParseBitmask(products.allProducts, defaultProducts),
parseLine: createParseLine,
parseLocation,
parseMovement: createParseMovement,

View file

@ -181,7 +181,7 @@ const vbbProfile = {
parseStationName: shorten,
parseLocation,
parseLine: createParseLine,
parseProducts: createParseBitmask(modes.bitmasks),
parseProducts: createParseBitmask(modes.allProducts, defaultProducts),
parseJourney: createParseJourney,
parseDeparture: createParseDeparture,
parseStopover: createParseStopover,

View file

@ -20,7 +20,7 @@ const parseLocation = (profile, l, lines) => {
type: 'station',
id: l.extId,
name: l.name,
location: res
location: 'number' === typeof res.latitude ? res : null
}
if ('pCls' in l) station.products = profile.parseProducts(l.pCls)

View file

@ -1,14 +1,29 @@
'use strict'
const createParseBitmask = (bitmasks) => {
const createParseBitmask = (allProducts, defaultProducts) => {
allProducts = allProducts.sort((p1, p2) => p2.bitmask - p1.bitmask) // desc
if (allProducts.length === 0) throw new Error('allProducts is empty.')
for (let product of allProducts) {
if ('string' !== typeof product.product) {
throw new Error('allProducts[].product must be a string.')
}
if ('number' !== typeof product.bitmask) {
throw new Error(product.product + '.bitmask must be a number.')
}
}
const parseBitmask = (bitmask) => {
const products = {}
let i = 1
do {
products[bitmasks[i].product] = products[bitmasks[i].product] || !!(bitmask & i)
i *= 2
} while (bitmasks[i] && bitmasks[i].product)
return products
const res = Object.assign({}, defaultProducts)
for (let product of allProducts) {
if (bitmask === 0) break
if ((product.bitmask & bitmask) > 0) {
res[product.product] = true
bitmask -= product.bitmask
}
}
return res
}
return parseBitmask
}

View file

@ -7,6 +7,7 @@ HAFAS endpoint | wrapper library? | docs | example code | source code
[Deutsche Bahn](https://en.wikipedia.org/wiki/Deutsche_Bahn) | [`db-hafas`](https://github.com/derhuerst/db-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) | [`vbb-hafas`](https://github.com/derhuerst/vbb-hafas), which has additional features | [docs](p/vbb/readme.md) | [example code](p/vbb/example.js) | [src](p/vbb/index.js)
[Österreichische Bundesbahnen (ÖBB)](https://en.wikipedia.org/wiki/Austrian_Federal_Railways) | | [docs](p/oebb/readme.md) | [example code](p/oebb/example.js) | [src](p/oebb/index.js)
[Nahverkehr Sachsen-Anhalt (NASA)](https://de.wikipedia.org/wiki/Nahverkehrsservice_Sachsen-Anhalt)/[INSA](https://insa.de) | | [docs](p/insa/readme.md) | [example code](p/insa/example.js) | [src](p/insa/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)

View file

@ -8,7 +8,7 @@ const isRoughlyEqual = require('is-roughly-equal')
const co = require('./co')
const createClient = require('..')
const dbProfile = require('../p/db')
const modes = require('../p/db/modes')
const {allProducts} = require('../p/db/modes')
const {
assertValidStation,
assertValidPoi,
@ -69,11 +69,12 @@ const assertIsJungfernheide = (t, s) => {
t.ok(isRoughlyEqual(s.location.longitude, 13.299424, .0005))
}
// todo: this doesnt seem to work
// todo: DRY with assertValidStationProducts
// todo: DRY with other tests
const assertValidProducts = (t, p) => {
for (let k of Object.keys(modes)) {
t.ok('boolean', typeof modes[k], 'mode ' + k + ' must be a boolean')
for (let product of allProducts) {
product = product.product // wat
t.equal(typeof p[product], 'boolean', 'product ' + p + ' must be a boolean')
}
}

View file

@ -3,4 +3,5 @@
require('./db')
require('./vbb')
require('./oebb')
require('./insa')
require('./throttle')

365
test/insa.js Normal file
View file

@ -0,0 +1,365 @@
'use strict'
const tapePromise = require('tape-promise').default
const tape = require('tape')
const isRoughlyEqual = require('is-roughly-equal')
const validateFptf = require('validate-fptf')
const co = require('./co')
const createClient = require('..')
const insaProfile = require('../p/insa')
const {allProducts} = require('../p/insa/products')
const {
assertValidStation,
assertValidPoi,
assertValidAddress,
assertValidLocation,
assertValidLine,
assertValidStopover,
hour,
createWhen,
assertValidWhen
} = require('./util.js')
const when = createWhen('Europe/Berlin', 'de-DE')
const assertValidStationProducts = (t, p) => {
t.ok(p)
t.equal(typeof p.nationalExp, 'boolean')
t.equal(typeof p.national, 'boolean')
t.equal(typeof p.regional, 'boolean')
t.equal(typeof p.suburban, 'boolean')
t.equal(typeof p.tram, 'boolean')
t.equal(typeof p.bus, 'boolean')
t.equal(typeof p.tourismTrain, 'boolean')
}
const isMagdeburgHbf = s => {
return (
s.type === 'station' &&
(s.id === '8010224' || s.id === '008010224') &&
s.name === 'Magdeburg Hbf' &&
s.location &&
isRoughlyEqual(s.location.latitude, 52.130352, 0.001) &&
isRoughlyEqual(s.location.longitude, 11.626891, 0.001)
)
}
const assertIsMagdeburgHbf = (t, s) => {
t.equal(s.type, 'station')
t.ok(s.id === '8010224' || s.id === '008010224', 'id should be 8010224')
t.equal(s.name, 'Magdeburg Hbf')
t.ok(s.location)
t.ok(isRoughlyEqual(s.location.latitude, 52.130352, 0.001))
t.ok(isRoughlyEqual(s.location.longitude, 11.626891, 0.001))
}
// 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 test = tapePromise(tape)
const client = createClient(insaProfile)
test('Magdeburg Hbf to Magdeburg-Buckau', co(function*(t) {
const magdeburgHbf = '8010224'
const magdeburgBuckau = '8013456'
const journeys = yield client.journeys(magdeburgHbf, magdeburgBuckau, {
when,
passedStations: true
})
t.ok(Array.isArray(journeys))
t.ok(journeys.length > 0, 'no journeys')
for (let journey of journeys) {
assertValidStation(t, journey.origin)
assertValidStationProducts(t, journey.origin.products)
if (journey.origin.products) {
assertValidProducts(t, journey.origin.products)
}
assertValidWhen(t, journey.departure, when)
assertValidStation(t, journey.destination)
assertValidStationProducts(t, journey.origin.products)
if (journey.destination.products) {
assertValidProducts(t, journey.destination.products)
}
assertValidWhen(t, journey.arrival, when)
t.ok(Array.isArray(journey.legs))
t.ok(journey.legs.length > 0, 'no legs')
const leg = journey.legs[0]
assertValidStation(t, leg.origin)
assertValidStationProducts(t, leg.origin.products)
assertValidWhen(t, leg.departure, when)
t.equal(typeof leg.departurePlatform, 'string')
assertValidStation(t, leg.destination)
assertValidStationProducts(t, leg.origin.products)
assertValidWhen(t, leg.arrival, when)
t.equal(typeof leg.arrivalPlatform, 'string')
assertValidLine(t, leg.line)
t.ok(Array.isArray(leg.passed))
for (let stopover of leg.passed) assertValidStopover(t, stopover)
}
t.end()
}))
test('Magdeburg Hbf to 39104 Magdeburg, Sternstr. 10', co(function*(t) {
const magdeburgHbf = '8010224'
const sternStr = {
type: 'location',
latitude: 52.118414,
longitude: 11.422332,
address: 'Magdeburg - Altenstadt, Sternstraße 10'
}
const journeys = yield client.journeys(magdeburgHbf, sternStr, {
when
})
t.ok(Array.isArray(journeys))
t.ok(journeys.length >= 1, 'no journeys')
const journey = journeys[0]
const firstLeg = journey.legs[0]
const lastLeg = journey.legs[journey.legs.length - 1]
assertValidStation(t, firstLeg.origin)
assertValidStationProducts(t, firstLeg.origin.products)
if (firstLeg.origin.products)
assertValidProducts(t, firstLeg.origin.products)
assertValidWhen(t, firstLeg.departure, when)
assertValidWhen(t, firstLeg.arrival, when)
assertValidWhen(t, lastLeg.departure, when)
assertValidWhen(t, lastLeg.arrival, when)
const d = lastLeg.destination
assertValidAddress(t, d)
t.equal(d.address, 'Magdeburg - Altenstadt, Sternstraße 10')
t.ok(isRoughlyEqual(0.0001, d.latitude, 52.118414))
t.ok(isRoughlyEqual(0.0001, d.longitude, 11.422332))
t.end()
}))
test('Kloster Unser Lieben Frauen to Magdeburg Hbf', co(function*(t) {
const kloster = {
type: 'location',
latitude: 52.127601,
longitude: 11.636437,
name: 'Magdeburg, Kloster Unser Lieben Frauen (Denkmal)',
id: '970012223'
}
const magdeburgHbf = '8010224'
const journeys = yield client.journeys(kloster, magdeburgHbf, {
when
})
t.ok(Array.isArray(journeys))
t.ok(journeys.length >= 1, 'no journeys')
const journey = journeys[0]
const firstLeg = journey.legs[0]
const lastLeg = journey.legs[journey.legs.length - 1]
const o = firstLeg.origin
assertValidPoi(t, o)
t.equal(o.name, 'Magdeburg, Kloster Unser Lieben Frauen (Denkmal)')
t.ok(isRoughlyEqual(0.0001, o.latitude, 52.127601))
t.ok(isRoughlyEqual(0.0001, o.longitude, 11.636437))
assertValidWhen(t, firstLeg.departure, when)
assertValidWhen(t, firstLeg.arrival, when)
assertValidWhen(t, lastLeg.departure, when)
assertValidWhen(t, lastLeg.arrival, when)
assertValidStation(t, lastLeg.destination)
assertValidStationProducts(t, lastLeg.destination.products)
if (lastLeg.destination.products)
assertValidProducts(t, lastLeg.destination.products)
t.end()
}))
test('Magdeburg-Buckau to Magdeburg-Neustadt with stopover at Magdeburg Hbf', co(function*(t) {
const magdeburgBuckau = '8013456'
const magdeburgNeustadt = '8010226'
const magdeburgHbf = '8010224'
const [journey] = yield client.journeys(magdeburgBuckau, magdeburgNeustadt, {
via: magdeburgHbf,
results: 1,
when
})
const i1 = journey.legs.findIndex(leg => leg.destination.id === magdeburgHbf)
t.ok(i1 >= 0, 'no leg with Magdeburg Hbf as destination')
const i2 = journey.legs.findIndex(leg => leg.origin.id === magdeburgHbf)
t.ok(i2 >= 0, 'no leg with Magdeburg Hbf as origin')
t.ok(i2 > i1, 'leg with Magdeburg Hbf as origin must be after leg to it')
t.end()
}))
test('departures at Magdeburg Hbf', co(function*(t) {
const magdeburgHbf = '8010224'
const deps = yield client.departures(magdeburgHbf, {
duration: 5,
when
})
t.ok(Array.isArray(deps))
for (let dep of deps) {
assertValidStation(t, dep.station)
assertValidStationProducts(t, dep.station.products)
if (dep.station.products) {
assertValidProducts(t, dep.station.products)
}
assertValidWhen(t, dep.when, when)
}
t.end()
}))
test('nearby Magdeburg Hbf', co(function*(t) {
const magdeburgHbfPosition = {
type: 'location',
latitude: 52.130352,
longitude: 11.626891
}
const nearby = yield client.nearby(magdeburgHbfPosition, {
results: 2,
distance: 400
})
t.ok(Array.isArray(nearby))
t.equal(nearby.length, 2)
assertIsMagdeburgHbf(t, nearby[0])
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()
}))
test('journey leg details', co(function* (t) {
const magdeburgHbf = '8010224'
const magdeburgBuckau = '8013456'
const [journey] = yield client.journeys(magdeburgHbf, magdeburgBuckau, {
results: 1, when
})
const p = journey.legs[0]
t.ok(p, 'missing legs[0]')
t.ok(p.id, 'missing legs[0].id')
t.ok(p.line, 'missing legs[0].line')
t.ok(p.line.name, 'missing legs[0].line.name')
const leg = yield client.journeyLeg(p.id, p.line.name, {when})
t.equal(typeof leg.id, 'string')
t.ok(leg.id)
assertValidLine(t, leg.line)
t.equal(typeof leg.direction, 'string')
t.ok(leg.direction)
t.ok(Array.isArray(leg.passed))
for (let passed of leg.passed) assertValidStopover(t, passed)
t.end()
}))
test('locations named Magdeburg', co(function*(t) {
const locations = yield client.locations('Magdeburg', {
results: 10
})
t.ok(Array.isArray(locations))
t.ok(locations.length > 0)
t.ok(locations.length <= 10)
for (let l of locations) {
if (l.type === 'station') assertValidStation(t, l)
else assertValidLocation(t, l)
}
t.ok(locations.some(isMagdeburgHbf))
t.end()
}))
test('location', co(function*(t) {
const magdeburgBuckau = '8013456'
const loc = yield client.location(magdeburgBuckau)
assertValidStation(t, loc)
t.equal(loc.id, magdeburgBuckau)
t.end()
}))
test('radar', co(function* (t) {
const north = 52.148364
const west = 11.600826
const south = 52.108486
const east = 11.651451
const vehicles = yield client.radar(north, west, south, east, {
duration: 5 * 60, when, results: 10
})
t.ok(Array.isArray(vehicles))
t.ok(vehicles.length > 0)
for (let v of vehicles) {
assertValidLine(t, v.line)
t.equal(typeof v.location.latitude, 'number')
t.ok(v.location.latitude <= 57, 'vehicle is too far away')
t.ok(v.location.latitude >= 47, 'vehicle is too far away')
t.equal(typeof v.location.longitude, 'number')
t.ok(v.location.longitude >= 8, 'vehicle is too far away')
t.ok(v.location.longitude <= 14, 'vehicle is too far away')
t.ok(Array.isArray(v.nextStops))
for (let st of v.nextStops) {
assertValidStopover(t, st, true)
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 (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))
}
}
t.ok(Array.isArray(v.frames))
for (let f of v.frames) {
// see #28
// todo: check if this works by now
assertValidStation(t, f.origin, true)
assertValidStationProducts(t, f.origin.products)
assertValidStation(t, f.destination, true)
assertValidStationProducts(t, f.destination.products)
t.equal(typeof f.t, 'number')
}
}
t.end()
}))

View file

@ -12,7 +12,7 @@ const validateLineWithoutMode = require('./validate-line-without-mode')
const co = require('./co')
const createClient = require('..')
const oebbProfile = require('../p/oebb')
const products = require('../p/oebb/products')
const {allProducts} = require('../p/oebb/products')
const {
assertValidStation,
assertValidPoi,
@ -73,11 +73,12 @@ const assertIsSalzburgHbf = (t, s) => {
t.ok(isRoughlyEqual(s.location.longitude, 13.045604, .0005))
}
// todo: this doesnt seem to work
// todo: DRY with assertValidStationProducts
// todo: DRY with other tests
const assertValidProducts = (t, p) => {
for (let k of Object.keys(products)) {
t.ok('boolean', typeof products[k], 'mode ' + k + ' must be a boolean')
for (let product of allProducts) {
product = product.product // wat
t.equal(typeof p[product], 'boolean', 'product ' + p + ' must be a boolean')
}
}