From 44c57db03cb16952e8194f3c3b5d1f08b3bd2842 Mon Sep 17 00:00:00 2001 From: Julius Tens Date: Wed, 14 Mar 2018 14:54:08 +0100 Subject: [PATCH] add profile and tests for nah.sh --- p/nahsh/example.js | 20 ++ p/nahsh/index.js | 105 ++++++++++ p/nahsh/products.js | 101 ++++++++++ p/nahsh/readme.md | 18 ++ readme.md | 1 + test/nahsh.js | 454 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 699 insertions(+) create mode 100644 p/nahsh/example.js create mode 100644 p/nahsh/index.js create mode 100644 p/nahsh/products.js create mode 100644 p/nahsh/readme.md create mode 100644 test/nahsh.js diff --git a/p/nahsh/example.js b/p/nahsh/example.js new file mode 100644 index 00000000..0eb94f15 --- /dev/null +++ b/p/nahsh/example.js @@ -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}) +// 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) diff --git a/p/nahsh/index.js b/p/nahsh/index.js new file mode 100644 index 00000000..7388c701 --- /dev/null +++ b/p/nahsh/index.js @@ -0,0 +1,105 @@ +'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 products = require('./products') + +// todo: journey prices + +const transformReqBody = (body) => { + // todo: all headers necessary? + body.client = { + id: 'NAHSH', + name: 'NAHSHPROD', + os: 'iOS', + type: 'IPH', + 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 trailing zeroes, todo + if(res.id && res.id.length > 2){ + while(res.id.slice(0, 1) === '0'){ + res.id = res.id.slice(1) + } + } + + 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 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, + + formatProducts, + + journeyLeg: true, + radar: false // todo: fix nameless station bug +} + +module.exports = nahshProfile diff --git a/p/nahsh/products.js b/p/nahsh/products.js new file mode 100644 index 00000000..6d970f64 --- /dev/null +++ b/p/nahsh/products.js @@ -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 diff --git a/p/nahsh/readme.md b/p/nahsh/readme.md new file mode 100644 index 00000000..9d9bdad7 --- /dev/null +++ b/p/nahsh/readme.md @@ -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 diff --git a/readme.md b/readme.md index d627004a..867415b2 100644 --- a/readme.md +++ b/readme.md @@ -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) [Ö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) +[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) [![build status](https://img.shields.io/travis/derhuerst/hafas-client.svg)](https://travis-ci.org/derhuerst/hafas-client) diff --git a/test/nahsh.js b/test/nahsh.js new file mode 100644 index 00000000..2c99674c --- /dev/null +++ b/test/nahsh.js @@ -0,0 +1,454 @@ +'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: fix nameless station bug +// test('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) +// // can contain stations in germany which don't have a products property, would break +// // assertValidStationProducts(t, f.origin.products) +// assertValidStation(t, f.destination, true) +// // can contain stations in germany which don't have a products property, would break +// // assertValidStationProducts(t, f.destination.products) +// t.equal(typeof f.t, 'number') +// } +// } +// t.end() +// }))