Merge pull request #30 from derhuerst/nah.sh

add profile for NAH.SH
This commit is contained in:
Julius Tens 2018-03-18 13:04:57 +01:00 committed by GitHub
commit 60a1dff24c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 740 additions and 0 deletions

20
p/nahsh/example.js Normal file
View file

@ -0,0 +1,20 @@
'use strict'
const createClient = require('../..')
const nahshProfile = require('.')
const client = createClient(nahshProfile)
// Flensburg Hbf to Kiel Hbf
client.journeys('8000103', '8000199', {results: 10, tickets: true})
// client.departures('8000199', {duration: 10})
// client.journeyLeg('1|30161|5|100|14032018', 'Bus 52')
// client.locations('Schleswig', {results: 1})
// client.location('706990') // Kiel Holunderbusch
// client.nearby({type: 'location', latitude: 54.295691, longitude: 10.116424}, {distance: 60})
// client.radar(54.4, 10.0, 54.2, 10.2, {results: 10})
.then((data) => {
console.log(require('util').inspect(data, {depth: null}))
})
.catch(console.error)

144
p/nahsh/index.js Normal file
View file

@ -0,0 +1,144 @@
'use strict'
const createParseBitmask = require('../../parse/products-bitmask')
const createFormatBitmask = require('../../format/products-bitmask')
const _createParseLine = require('../../parse/line')
const _parseLocation = require('../../parse/location')
const _createParseJourney = require('../../parse/journey')
const products = require('./products')
// todo: journey prices
const transformReqBody = (body) => {
body.client = {
id: 'NAHSH',
name: 'NAHSHPROD',
v: '3000700'
}
body.ver = '1.16'
body.auth = {aid: 'r0Ot9FLFNAFxijLW'}
body.lang = 'de'
return body
}
const parseLocation = (profile, l, lines) => {
const res = _parseLocation(profile, l, lines)
// weird fix for empty lines, e.g. IC/EC at Flensburg Hbf
if (res.lines) {
res.lines = res.lines.filter(x => x.id && x.name)
}
// remove leading zeroes, todo
if (res.id && res.id.length > 0) {
res.id = res.id.replace(/^0+/, '')
}
return res
}
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 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.length > 0
) {
res.tickets = []
for (let t of j.trfRes.fareSetL) {
const tariff = t.desc
if (!tariff || !Array.isArray(t.fareL)) continue
for (let v of t.fareL) {
const variant = v.name
if(!variant) continue
const ticket = {
name: [tariff, variant].join(' - '),
tariff,
variant
}
if (v.prc && Number.isInteger(v.prc) && v.cur) {
ticket.amount = v.prc/100
ticket.currency = v.cur
} else {
ticket.amount = null
ticket.hint = 'No pricing information available.'
}
res.tickets.push(ticket)
}
}
}
return res
}
return parseJourneyWithTickets
}
const defaultProducts = {
nationalExp: true,
national: true,
interregional: true,
regional: true,
suburban: true,
bus: true,
ferry: true,
subway: true,
tram: true,
onCall: true
}
const formatBitmask = createFormatBitmask(products)
const formatProducts = (products) => {
products = Object.assign(Object.create(null), defaultProducts, products)
return {
type: 'PROD',
mode: 'INC',
value: formatBitmask(products) + ''
}
}
const nahshProfile = {
locale: 'de-DE',
timezone: 'Europe/Berlin',
endpoint: 'http://nah.sh.hafas.de/bin/mgate.exe',
transformReqBody,
products: products.allProducts,
parseProducts: createParseBitmask(products.allProducts, defaultProducts),
parseLine: createParseLine,
parseLocation,
parseJourney: createParseJourney,
formatProducts,
journeyLeg: true,
radar: false // todo: see #34
}
module.exports = nahshProfile

101
p/nahsh/products.js Normal file
View file

@ -0,0 +1,101 @@
'use strict'
const p = {
nationalExp: {
bitmask: 1,
name: 'High-speed rail',
short: 'ICE/HSR',
mode: 'train',
product: 'nationalExp'
},
national: {
bitmask: 2,
name: 'InterCity & EuroCity',
short: 'IC/EC',
mode: 'train',
product: 'national'
},
interregional: { // todo: also includes EN?
bitmask: 4,
name: 'Interregional',
short: 'IR',
mode: 'train',
product: 'interregional'
},
regional: {
bitmask: 8,
name: 'Regional & RegionalExpress',
short: 'RB/RE',
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: 'watercraft',
product: 'ferry'
},
subway: {
bitmask: 128,
name: 'U-Bahn',
short: 'U',
mode: 'train',
product: 'subway'
},
tram: {
bitmask: 256,
name: 'Tram',
short: 'T',
mode: 'train',
product: 'tram'
},
onCall: {
bitmask: 512,
name: 'On-call transit',
short: 'on-call',
mode: null, // todo
product: 'onCall'
}
}
p.bitmasks = []
p.bitmasks[1] = p.nationalExp
p.bitmasks[2] = p.national
p.bitmasks[4] = p.interregional
p.bitmasks[8] = p.regional
p.bitmasks[16] = p.suburban
p.bitmasks[32] = p.bus
p.bitmasks[64] = p.ferry
p.bitmasks[128] = p.subway
p.bitmasks[256] = p.tram
p.bitmasks[512] = p.onCall
p.allProducts = [
p.nationalExp,
p.national,
p.interregional,
p.regional,
p.suburban,
p.bus,
p.ferry,
p.subway,
p.tram,
p.onCall
]
module.exports = p

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

@ -0,0 +1,18 @@
# NAH.SH profile for `hafas-client`
[*Nahverkehrsverbund Schleswig-Holstein (NAH.SH)*](https://de.wikipedia.org/wiki/Nahverkehrsverbund_Schleswig-Holstein) is the transportation authority for regional transport in [Schleswig-Holstein](https://en.wikipedia.org/wiki/Schleswig-Holstein). This profile adds *NAH.SH*-specific customizations to `hafas-client`. Consider using [`nahsh-hafas`](https://github.com/juliuste/nahsh-hafas), to always get the customized client right away.
## Usage
```js
const createClient = require('hafas-client')
const nahshProfile = require('hafas-client/p/nahsh')
// create a client with NAH.SH profile
const client = createClient(nahshProfile)
```
## Customisations
- parses *NAH.SH*-specific products

View file

@ -21,6 +21,9 @@ const createParseBitmask = (allProducts, defaultProducts) => {
res[product.product] = true res[product.product] = true
bitmask -= product.bitmask bitmask -= product.bitmask
} }
else{
res[product.product] = false
}
} }
return res return res

View file

@ -8,6 +8,7 @@ HAFAS endpoint | wrapper library | docs | example code | source code
[Berlin & Brandenburg public transport](https://en.wikipedia.org/wiki/Verkehrsverbund_Berlin-Brandenburg) | [`vbb-hafas`](https://github.com/derhuerst/vbb-hafas) | [docs](p/vbb/readme.md) | [example code](p/vbb/example.js) | [src](p/vbb/index.js) [Berlin & Brandenburg public transport](https://en.wikipedia.org/wiki/Verkehrsverbund_Berlin-Brandenburg) | [`vbb-hafas`](https://github.com/derhuerst/vbb-hafas) | [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) | [`oebb-hafas`](https://github.com/juliuste/oebb-hafas) | [docs](p/oebb/readme.md) | [example code](p/oebb/example.js) | [src](p/oebb/index.js) [Österreichische Bundesbahnen (ÖBB)](https://en.wikipedia.org/wiki/Austrian_Federal_Railways) | [`oebb-hafas`](https://github.com/juliuste/oebb-hafas) | [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) [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)
[Nahverkehrsverbund Schleswig-Holstein (NAH.SH)](https://de.wikipedia.org/wiki/Nahverkehrsverbund_Schleswig-Holstein) | [`nahsh-hafas`](https://github.com/juliuste/nahsh-hafas) | [docs](p/nahsh/readme.md) | [example code](p/nahsh/example.js) | [src](p/nahsh/index.js)
[![npm version](https://img.shields.io/npm/v/hafas-client.svg)](https://www.npmjs.com/package/hafas-client) [![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) [![build status](https://img.shields.io/travis/derhuerst/hafas-client.svg)](https://travis-ci.org/derhuerst/hafas-client)

View file

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

452
test/nahsh.js Normal file
View file

@ -0,0 +1,452 @@
'use strict'
// todo
// const getStations = require('db-stations').full
const tapePromise = require('tape-promise').default
const tape = require('tape')
const isRoughlyEqual = require('is-roughly-equal')
const validateFptf = require('validate-fptf')
const validateLineWithoutMode = require('./validate-line-without-mode')
const co = require('./co')
const createClient = require('..')
const nahshProfile = require('../p/nahsh')
const {allProducts} = require('../p/nahsh/products')
const {
assertValidStation,
assertValidPoi,
assertValidAddress,
assertValidLocation,
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.interregional, '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.onCall, 'boolean')
}
const isKielHbf = (s) => {
return s.type === 'station' &&
(s.id === '8000199') &&
s.name === 'Kiel Hbf' &&
s.location &&
isRoughlyEqual(s.location.latitude, 54.314982, .0005) &&
isRoughlyEqual(s.location.longitude, 10.131976, .0005)
}
const assertIsKielHbf = (t, s) => {
t.equal(s.type, 'station')
t.ok(s.id === '8000199', 'id should be 8000199')
t.equal(s.name, 'Kiel Hbf')
t.ok(s.location)
t.ok(isRoughlyEqual(s.location.latitude, 54.314982, .0005))
t.ok(isRoughlyEqual(s.location.longitude, 10.131976, .0005))
}
// 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 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 assertValidLine = (t, l) => { // with optional mode
const validators = Object.assign({}, validateFptf.defaultValidators, {
line: validateLineWithoutMode
})
const recurse = validateFptf.createRecurse(validators)
try {
recurse(['line'], l, 'line')
} catch (err) {
t.ifError(err)
}
}
const test = tapePromise(tape)
const client = createClient(nahshProfile)
const kielHbf = '8000199'
const flensburg = '8000103'
const luebeckHbf = '8000237'
const husum = '8000181'
const schleswig = '8005362'
test('Kiel Hbf to Flensburg', co(function* (t) {
const journeys = yield client.journeys(kielHbf, flensburg, {
when, passedStations: true
})
t.ok(Array.isArray(journeys))
t.ok(journeys.length > 0, 'no journeys')
for (let journey of journeys) {
t.equal(journey.type, 'journey')
assertValidStation(t, journey.origin)
assertValidStationProducts(t, journey.origin.products)
// todo
// if (!(yield findStation(journey.origin.id))) {
// console.error('unknown station', journey.origin.id, journey.origin.name)
// }
if (journey.origin.products) {
assertValidProducts(t, journey.origin.products)
}
assertValidWhen(t, journey.departure, when)
assertValidStation(t, journey.destination)
assertValidStationProducts(t, journey.origin.products)
// todo
// if (!(yield findStation(journey.origin.id))) {
// console.error('unknown station', journey.destination.id, journey.destination.name)
// }
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)
// todo
// if (!(yield findStation(leg.origin.id))) {
// console.error('unknown station', leg.origin.id, leg.origin.name)
// }
assertValidWhen(t, leg.departure, when)
t.equal(typeof leg.departurePlatform, 'string')
assertValidStation(t, leg.destination)
assertValidStationProducts(t, leg.origin.products)
// todo
// if (!(yield findStation(leg.destination.id))) {
// console.error('unknown station', leg.destination.id, leg.destination.name)
// }
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)
if (journey.price) assertValidPrice(t, journey.price)
}
t.end()
}))
test('Kiel Hbf to Husum, Zingel 10', co(function* (t) {
const zingel = {
type: 'location',
latitude: 54.475359,
longitude: 9.050798,
address: 'Husum, Zingel 10'
}
const journeys = yield client.journeys(kielHbf, zingel, {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)
// todo
// if (!(yield findStation(leg.origin.id))) {
// console.error('unknown station', leg.origin.id, leg.origin.name)
// }
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, 'Husum, Zingel 10')
t.ok(isRoughlyEqual(.0001, d.latitude, 54.475359))
t.ok(isRoughlyEqual(.0001, d.longitude, 9.050798))
t.end()
}))
test('Holstentor to Kiel Hbf', co(function* (t) {
const holstentor = {
type: 'location',
latitude: 53.866321,
longitude: 10.679976,
name: 'Hansestadt Lübeck, Holstentor (Denkmal)',
id: '970003547'
}
const journeys = yield client.journeys(holstentor, kielHbf, {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, 'Hansestadt Lübeck, Holstentor (Denkmal)')
t.ok(isRoughlyEqual(.0001, o.latitude, 53.866321))
t.ok(isRoughlyEqual(.0001, o.longitude, 10.679976))
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)
// todo
// if (!(yield findStation(leg.destination.id))) {
// console.error('unknown station', leg.destination.id, leg.destination.name)
// }
t.end()
}))
test('Husum to Lübeck Hbf with stopover at Husum', co(function* (t) {
const [journey] = yield client.journeys(husum, luebeckHbf, {
via: kielHbf,
results: 1,
when
})
const i1 = journey.legs.findIndex(leg => leg.destination.id === kielHbf)
t.ok(i1 >= 0, 'no leg with Kiel Hbf as destination')
const i2 = journey.legs.findIndex(leg => leg.origin.id === kielHbf)
t.ok(i2 >= 0, 'no leg with Kiel Hbf as origin')
t.ok(i2 > i1, 'leg with Kiel Hbf as origin must be after leg to it')
t.end()
}))
test('earlier/later journeys, Kiel Hbf -> Flensburg', co(function* (t) {
const model = yield client.journeys(kielHbf, flensburg, {
results: 3, when
})
t.equal(typeof model.earlierRef, 'string')
t.ok(model.earlierRef)
t.equal(typeof model.laterRef, 'string')
t.ok(model.laterRef)
// when and earlierThan/laterThan should be mutually exclusive
t.throws(() => {
client.journeys(kielHbf, flensburg, {
when, earlierThan: model.earlierRef
})
})
t.throws(() => {
client.journeys(kielHbf, flensburg, {
when, laterThan: model.laterRef
})
})
let earliestDep = Infinity, latestDep = -Infinity
for (let j of model) {
const dep = +new Date(j.departure)
if (dep < earliestDep) earliestDep = dep
else if (dep > latestDep) latestDep = dep
}
const earlier = yield client.journeys(kielHbf, flensburg, {
results: 3,
// todo: single journey ref?
earlierThan: model.earlierRef
})
for (let j of earlier) {
t.ok(new Date(j.departure) < earliestDep)
}
const later = yield client.journeys(kielHbf, flensburg, {
results: 3,
// todo: single journey ref?
laterThan: model.laterRef
})
for (let j of later) {
t.ok(new Date(j.departure) > latestDep)
}
t.end()
}))
test('leg details for Flensburg to Husum', co(function* (t) {
const journeys = yield client.journeys(flensburg, husum, {
results: 1, when
})
const p = journeys[0].legs[0]
t.ok(p.id, 'precondition failed')
t.ok(p.line.name, 'precondition failed')
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('departures at Kiel Hbf', co(function* (t) {
const deps = yield client.departures(kielHbf, {
duration: 30, when
})
t.ok(Array.isArray(deps))
for (let dep of deps) {
assertValidStation(t, dep.station)
assertValidStationProducts(t, dep.station.products)
// todo
// 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)
assertValidWhen(t, dep.when, when)
}
t.end()
}))
test('nearby Kiel Hbf', co(function* (t) {
const kielHbfPosition = {
type: 'location',
latitude: 54.314982,
longitude: 10.131976
}
const nearby = yield client.nearby(kielHbfPosition, {
results: 2, distance: 400
})
t.ok(Array.isArray(nearby))
t.equal(nearby.length, 2)
assertIsKielHbf(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('locations named Kiel', co(function* (t) {
const locations = yield client.locations('Kiel', {
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(isKielHbf))
t.end()
}))
test('location', co(function* (t) {
const loc = yield client.location(schleswig)
assertValidStation(t, loc)
t.equal(loc.id, schleswig)
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, {
duration: 5 * 60, when
})
t.ok(Array.isArray(vehicles))
t.ok(vehicles.length > 0)
for (let v of vehicles) {
// todo
// t.ok(findStation(v.direction))
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 >= 51, 'vehicle is too far away')
t.equal(typeof v.location.longitude, 'number')
t.ok(v.location.longitude >= 7, 'vehicle is too far away')
t.ok(v.location.longitude <= 13, '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)
t.ok(isRoughlyEqual(14 * hour, +when, dep))
}
}
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)
t.equal(typeof f.t, 'number')
}
}
t.end()
}))