diff --git a/format/index.js b/format/index.js index 75f69239..a119dd37 100644 --- a/format/index.js +++ b/format/index.js @@ -8,5 +8,6 @@ module.exports = { address: require('./address'), poi: require('./poi'), location: require('./location'), - locationFilter: require('./location-filter') + locationFilter: require('./location-filter'), + rectangle: require('./rectangle') } diff --git a/format/rectangle.js b/format/rectangle.js new file mode 100644 index 00000000..51f83e9e --- /dev/null +++ b/format/rectangle.js @@ -0,0 +1,16 @@ +'use strict' + +const formatRectangle = (profile, north, west, south, east) => { + return { + llCrd: { + x: profile.formatCoord(west), + y: profile.formatCoord(south) + }, + urCrd: { + x: profile.formatCoord(east), + y: profile.formatCoord(north) + } + } +} + +module.exports = formatRectangle diff --git a/index.js b/index.js index c2227c61..68559d43 100644 --- a/index.js +++ b/index.js @@ -184,7 +184,48 @@ const createClient = (profile) => { }) } - return {departures, journeys, locations, nearby, journeyPart} + const radar = (north, west, south, east, opt) => { + if ('number' !== typeof north) throw new Error('north must be a number.') + 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.') + + 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 + }, opt || {}) + opt.when = opt.when || new Date() + + const durationPerStep = opt.duration / Math.max(opt.frames, 1) * 1000 + return request(profile, { + meth: 'JourneyGeoPos', + req: { + maxJny: opt.results, + onlyRT: false, // todo: does this mean "only realtime"? + date: profile.formatDate(profile, opt.when), + time: profile.formatTime(profile, opt.when), + // todo: would a ring work here as well? + rect: profile.formatRectangle(profile, north, west, south, east), + perSize: opt.duration * 1000, + perStep: Math.round(durationPerStep), + ageOfReport: true, // todo: what is this? + jnyFltrL: [ + // todo: use `profile.formatProducts(opt.products || {})` + {type: 'PROD', mode: 'INC', value: '127'} + ], + trainPosMode: 'CALC' // todo: what is this? what about realtime? + } + }) + .then((d) => { + if (!Array.isArray(d.jnyL)) return [] + + const parse = profile.parseMovement(profile, d.locations, d.lines, d.remarks) + return d.jnyL.map(parse) + }) + } + + return {departures, journeys, locations, nearby, journeyPart, radar} } module.exports = createClient diff --git a/lib/default-profile.js b/lib/default-profile.js index 0eff0a20..32561573 100644 --- a/lib/default-profile.js +++ b/lib/default-profile.js @@ -21,6 +21,7 @@ const formatPoi = require('../format/poi') const formatStation = require('../format/station') const formatTime = require('../format/time') const formatLocation = require('../format/location') +const formatRectangle = require('../format/rectangle') const id = x => x @@ -52,7 +53,8 @@ const defaultProfile = { formatPoi, formatStation, formatTime, - formatLocation + formatLocation, + formatRectangle } module.exports = defaultProfile diff --git a/parse/movement.js b/parse/movement.js index e9662291..67e3d591 100644 --- a/parse/movement.js +++ b/parse/movement.js @@ -27,7 +27,7 @@ const createParseMovement = (profile, locations, lines, remarks) => { } const res = { - direction: m.dirTxt, + direction: profile.parseStationName(m.dirTxt), line: lines[m.prodX] || null, coordinates: m.pos ? { latitude: m.pos.y / 1000000, diff --git a/test/util.js b/test/util.js index 36805d1e..bc023dc1 100644 --- a/test/util.js +++ b/test/util.js @@ -3,31 +3,52 @@ const isRoughlyEqual = require('is-roughly-equal') const floor = require('floordate') -const assertValidStation = (t, s) => { +const assertValidStation = (t, s, coordsOptional = false) => { + t.equal(typeof s.type, 'string') t.equal(s.type, 'station') t.equal(typeof s.id, 'string') + + t.equal(typeof s.name, 'string') + if (!coordsOptional) { + if (!s.coordinates) console.trace() + t.ok(s.coordinates) + } + if (s.coordinates) { + t.equal(typeof s.coordinates.latitude, 'number') + t.equal(typeof s.coordinates.longitude, 'number') + } } const assertValidPoi = (t, p) => { + t.equal(typeof p.type, 'string') t.equal(p.type, 'poi') t.equal(typeof p.id, 'string') + + t.equal(typeof p.name, 'string') + t.ok(p.coordinates) + if (s.coordinates) { + t.equal(typeof p.coordinates.latitude, 'number') + t.equal(typeof p.coordinates.longitude, 'number') + } } const assertValidAddress = (t, a) => { + t.equal(typeof a.type, 'string') t.equal(a.type, 'address') + + t.equal(typeof a.name, 'string') + t.ok(a.coordinates) + if (s.coordinates) { + t.equal(typeof a.coordinates.latitude, 'number') + t.equal(typeof a.coordinates.longitude, 'number') + } } const assertValidLocation = (t, l) => { - t.equal(typeof l.type, 'string') if (l.type === 'station') assertValidStation(t, l) else if (l.type === 'poi') assertValidPoi(t, l) else if (l.type === 'address') assertValidAddress(t, l) else t.fail('invalid type ' + l.type) - - t.equal(typeof l.name, 'string') - t.ok(l.coordinates) - t.equal(typeof l.coordinates.latitude, 'number') - t.equal(typeof l.coordinates.longitude, 'number') } const isValidMode = (m) => { @@ -40,7 +61,6 @@ const isValidMode = (m) => { const assertValidLine = (t, l) => { t.equal(l.type, 'line') t.equal(typeof l.name, 'string') - if (!isValidMode(l.mode)) console.error(l) t.ok(isValidMode(l.mode), 'invalid mode ' + l.mode) t.equal(typeof l.product, 'string') t.equal(l.public, true) @@ -50,14 +70,14 @@ const isValidDateTime = (w) => { return !Number.isNaN(+new Date(w)) } -const assertValidStopover = (t, s) => { +const assertValidStopover = (t, s, coordsOptional = false) => { if ('arrival' in s) t.ok(isValidDateTime(s.arrival)) if ('departure' in s) t.ok(isValidDateTime(s.departure)) if (!('arrival' in s) && !('departure' in s)) { t.fail('stopover doesn\'t contain arrival or departure') } t.ok(s.station) - assertValidStation(t, s.station) + assertValidStation(t, s.station, coordsOptional) } const minute = 60 * 1000 @@ -86,5 +106,5 @@ module.exports = { assertValidLine, isValidDateTime, assertValidStopover, - when, isValidWhen, assertValidWhen + hour, when, isValidWhen, assertValidWhen } diff --git a/test/vbb.js b/test/vbb.js index f0f1eca1..0bbd9331 100644 --- a/test/vbb.js +++ b/test/vbb.js @@ -269,8 +269,7 @@ test('locations', async (t) => { -// todo -test.skip('radar', async (t) => { +test('radar', async (t) => { const vehicles = await client.radar(52.52411, 13.41002, 52.51942, 13.41709, { duration: 5 * 60, when }) @@ -290,21 +289,19 @@ test.skip('radar', async (t) => { t.ok(v.coordinates.longitude <= 15, 'vehicle is too far away') t.ok(Array.isArray(v.nextStops)) - for (let s of v.nextStops) { - assertValidFrameStation(t, s.station) - if (!s.arrival && !s.departure) - t.ifError(new Error('neither arrival nor departure return')) - if (s.arrival) { - t.equal(typeof s.arrival, 'string') - const arr = +new Date(s.arrival) - t.ok(!Number.isNaN(arr)) + for (let st of v.nextStops) { + assertValidStopover(t, st, true) + t.strictEqual(st.station.name.indexOf('(Berlin)'), -1) + + 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 (s.departure) { - t.equal(typeof s.departure, 'string') - const dep = +new Date(s.departure) - t.ok(!Number.isNaN(dep)) + 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)) } @@ -312,8 +309,10 @@ test.skip('radar', async (t) => { t.ok(Array.isArray(v.frames)) for (let f of v.frames) { - assertValidFrameStation(t, f.origin) - assertValidFrameStation(t, f.destination) + assertValidStation(t, f.origin, true) + t.strictEqual(f.origin.name.indexOf('(Berlin)'), -1) + assertValidStation(t, f.destination, true) + t.strictEqual(f.destination.name.indexOf('(Berlin)'), -1) t.equal(typeof f.t, 'number') } }