parseProductsBitmask via profile, pass ctx into parse fns 💥

This commit is contained in:
Jannis R 2019-10-20 00:19:11 +02:00
parent 29d7bd4299
commit fb7a5653e3
No known key found for this signature in database
GPG key ID: 0FE83946296A88A5
30 changed files with 501 additions and 487 deletions

View file

@ -7,7 +7,6 @@ const sortBy = require('lodash/sortBy')
const pRetry = require('p-retry') const pRetry = require('p-retry')
const defaultProfile = require('./lib/default-profile') const defaultProfile = require('./lib/default-profile')
const createParseBitmask = require('./parse/products-bitmask')
const createFormatProductsFilter = require('./format/products-filter') const createFormatProductsFilter = require('./format/products-filter')
const validateProfile = require('./lib/validate-profile') const validateProfile = require('./lib/validate-profile')
const _request = require('./lib/request') const _request = require('./lib/request')
@ -32,9 +31,6 @@ const defaults = {
const createClient = (profile, userAgent, opt = {}) => { const createClient = (profile, userAgent, opt = {}) => {
profile = Object.assign({}, defaultProfile, profile) profile = Object.assign({}, defaultProfile, profile)
if (!profile.parseProducts) {
profile.parseProducts = createParseBitmask(profile)
}
if (!profile.formatProductsFilter) { if (!profile.formatProductsFilter) {
profile.formatProductsFilter = createFormatProductsFilter(profile) profile.formatProductsFilter = createFormatProductsFilter(profile)
} }

View file

@ -2,6 +2,7 @@
const parseDateTime = require('../parse/date-time') const parseDateTime = require('../parse/date-time')
const parsePlatform = require('../parse/platform') const parsePlatform = require('../parse/platform')
const parseProductsBitmask = require('../parse/products-bitmask')
const parseIcon = require('../parse/icon') const parseIcon = require('../parse/icon')
const parseWhen = require('../parse/when') const parseWhen = require('../parse/when')
const parseDeparture = require('../parse/departure') const parseDeparture = require('../parse/departure')
@ -44,6 +45,7 @@ const defaultProfile = {
parseDateTime, parseDateTime,
parsePlatform, parsePlatform,
parseProductsBitmask,
parseIcon, parseIcon,
parseWhen, parseWhen,
parseDeparture, parseDeparture,
@ -51,7 +53,7 @@ const defaultProfile = {
parseJourneyLeg, parseJourneyLeg,
parseJourney, parseJourney,
parseLine, parseLine,
parseStationName: id, parseStationName: (_, name) => name,
parseLocation, parseLocation,
parseCommon, parseCommon,
parsePolyline, parsePolyline,

View file

@ -10,22 +10,26 @@ const DEPARTURE = 'd'
// todo: what is d.jny.dirFlg? // todo: what is d.jny.dirFlg?
// todo: d.stbStop.dProgType/d.stbStop.aProgType // todo: d.stbStop.dProgType/d.stbStop.aProgType
const createParseArrOrDep = (profile, opt, data, prefix) => { const createParseArrOrDep = (prefix) => {
if (prefix !== ARRIVAL && prefix !== DEPARTURE) throw new Error('invalid prefix') if (prefix !== ARRIVAL && prefix !== DEPARTURE) throw new Error('invalid prefix')
const parseArrOrDep = (d) => { const parseArrOrDep = (ctx, d) => { // d = raw arrival/departure
const {profile, opt} = ctx
const tPlanned = d.stbStop[prefix + 'TimeS'] const tPlanned = d.stbStop[prefix + 'TimeS']
const tPrognosed = d.stbStop[prefix + 'TimeR'] const tPrognosed = d.stbStop[prefix + 'TimeR']
const tzOffset = d.stbStop[prefix + 'TZOffset'] || null const tzOffset = d.stbStop[prefix + 'TZOffset'] || null
const cancelled = !!d.stbStop[prefix + 'Cncl'] const cancelled = !!d.stbStop[prefix + 'Cncl']
const plPlanned = d.stbStop[prefix + 'PlatfS']
const plPrognosed = d.stbStop[prefix + 'PlatfR']
const res = { const res = {
tripId: d.jid, tripId: d.jid,
stop: d.stbStop.location || null, stop: d.stbStop.location || null,
...parseWhen(profile, d.date, tPlanned, tPrognosed, tzOffset, cancelled), ...profile.parseWhen(ctx, d.date, tPlanned, tPrognosed, tzOffset, cancelled),
...parsePlatform(profile, d.stbStop[prefix + 'PlatfS'], d.stbStop[prefix + 'PlatfR'], cancelled), ...profile.parsePlatform(ctx, plPlanned, plPrognosed, cancelled),
// todo: for arrivals, this is the *origin*, not the *direction* // todo: for arrivals, this is the *origin*, not the *direction*
direction: prefix === DEPARTURE && d.dirTxt && profile.parseStationName(d.dirTxt) || null, direction: prefix === DEPARTURE && d.dirTxt && profile.parseStationName(ctx, d.dirTxt) || null,
line: d.line || null, line: d.line || null,
remarks: [] remarks: []
} }
@ -43,9 +47,10 @@ const createParseArrOrDep = (profile, opt, data, prefix) => {
} }
if (opt.stopovers && Array.isArray(d.stopL)) { if (opt.stopovers && Array.isArray(d.stopL)) {
const parse = profile.parseStopover(profile, opt, data, d.date)
// Filter stations the train passes without stopping, as this doesn't comply with FPTF (yet). // Filter stations the train passes without stopping, as this doesn't comply with FPTF (yet).
const stopovers = d.stopL.map(parse).filter(st => !st.passBy) const stopovers = d.stopL
.map(st => profile.parseStopover(ctx, st, d.date))
.filter(st => !st.passBy)
if (prefix === ARRIVAL) res.previousStopovers = stopovers if (prefix === ARRIVAL) res.previousStopovers = stopovers
else if (prefix === DEPARTURE) res.nextStopovers = stopovers else if (prefix === DEPARTURE) res.nextStopovers = stopovers
} }

View file

@ -3,8 +3,6 @@
const createParseArrOrDep = require('./arrival-or-departure') const createParseArrOrDep = require('./arrival-or-departure')
const ARRIVAL = 'a' const ARRIVAL = 'a'
const createParseArrival = (profile, opt, data) => { const parseArrival = createParseArrOrDep(ARRIVAL)
return createParseArrOrDep(profile, opt, data, ARRIVAL)
}
module.exports = createParseArrival module.exports = parseArrival

View file

@ -3,102 +3,103 @@
const get = require('lodash/get') const get = require('lodash/get')
const findInTree = require('../lib/find-in-tree') const findInTree = require('../lib/find-in-tree')
const parseCommonData = (profile, opt, res) => { const parseCommonData = (_ctx) => {
const {profile, opt, res} = _ctx
const c = res.common || {} const c = res.common || {}
res.operators = [] const common = {}
const ctx = {..._ctx, common}
common.operators = []
if (Array.isArray(c.opL)) { if (Array.isArray(c.opL)) {
res.operators = c.opL.map(op => profile.parseOperator(profile, op)) common.operators = c.opL.map(op => profile.parseOperator(ctx, op))
findInTree(res, '**.oprX', (idx, parent) => { findInTree(res, '**.oprX', (idx, parent) => {
if ('number' === typeof idx) parent.operator = res.operators[idx] if ('number' === typeof idx) parent.operator = common.operators[idx]
}) })
} }
res.icons = [] common.icons = []
if (Array.isArray(c.icoL)) { if (Array.isArray(c.icoL)) {
res.icons = c.icoL.map(icon => profile.parseIcon(profile, icon)) common.icons = c.icoL.map(icon => profile.parseIcon(ctx, icon))
findInTree(res, '**.icoX', (idx, parent) => { findInTree(res, '**.icoX', (idx, parent) => {
if ('number' === typeof idx) parent.icon = res.icons[idx] if ('number' === typeof idx) parent.icon = common.icons[idx]
}) })
} }
res.lines = [] common.lines = []
if (Array.isArray(c.prodL)) { if (Array.isArray(c.prodL)) {
const parse = profile.parseLine(profile, opt, res) common.lines = c.prodL.map(l => profile.parseLine(ctx, l))
res.lines = c.prodL.map(parse)
findInTree(res, '**.prodX', (idx, parent) => { findInTree(res, '**.prodX', (idx, parent) => {
if ('number' === typeof idx) parent.line = res.lines[idx] if ('number' === typeof idx) parent.line = common.lines[idx]
}) })
findInTree(res, '**.pRefL', (idxs, parent) => { findInTree(res, '**.pRefL', (idxs, parent) => {
parent.lines = idxs.filter(idx => !!res.lines[idx]).map(idx => res.lines[idx]) parent.lines = idxs.filter(idx => !!common.lines[idx]).map(idx => common.lines[idx])
}) })
// todo // todo
// **.dep.dProdX: departureLine -> res.lines[idx] // **.dep.dProdX: departureLine -> common.lines[idx]
// **.arr.aProdX: arrivalLine -> res.lines[idx] // **.arr.aProdX: arrivalLine -> common.lines[idx]
} }
res.locations = [] common.locations = []
if (Array.isArray(c.locL)) { if (Array.isArray(c.locL)) {
const parse = loc => profile.parseLocation(profile, opt, res, loc) common.locations = c.locL.map(loc => profile.parseLocation(ctx, loc))
res.locations = c.locL.map(parse)
for (let i = 0; i < res.locations.length; i++) { for (let i = 0; i < common.locations.length; i++) {
const raw = c.locL[i] const raw = c.locL[i]
const loc = res.locations[i] const loc = common.locations[i]
if ('number' === typeof raw.mMastLocX) { if ('number' === typeof raw.mMastLocX) {
loc.station = Object.assign({}, res.locations[raw.mMastLocX]) loc.station = Object.assign({}, common.locations[raw.mMastLocX])
loc.station.type = 'station' loc.station.type = 'station'
} else if (raw.isMainMast) loc.type = 'station' } else if (raw.isMainMast) loc.type = 'station'
} }
// todo: correct props? // todo: correct props?
findInTree(res, '**.locX', (idx, parent) => { findInTree(res, '**.locX', (idx, parent) => {
if ('number' === typeof idx) parent.location = res.locations[idx] if ('number' === typeof idx) parent.location = common.locations[idx]
}) })
findInTree(res, '**.ani.fLocX', (idxs, parent) => { findInTree(res, '**.ani.fLocX', (idxs, parent) => {
parent.fromLocations = idxs.map(idx => res.locations[idx]) parent.fromLocations = idxs.map(idx => common.locations[idx])
}) })
findInTree(res, '**.ani.tLocX', (idxs, parent) => { findInTree(res, '**.ani.tLocX', (idxs, parent) => {
parent.toLocations = idxs.map(idx => res.locations[idx]) parent.toLocations = idxs.map(idx => common.locations[idx])
}) })
findInTree(res, '**.fLocX', (idx, parent) => { findInTree(res, '**.fLocX', (idx, parent) => {
if ('number' === typeof idx) parent.fromLocation = res.locations[idx] if ('number' === typeof idx) parent.fromLocation = common.locations[idx]
}) })
findInTree(res, '**.tLocX', (idx, parent) => { findInTree(res, '**.tLocX', (idx, parent) => {
if ('number' === typeof idx) parent.toLocation = res.locations[idx] if ('number' === typeof idx) parent.toLocation = common.locations[idx]
}) })
} }
res.hints = [] common.hints = []
if (opt.remarks && Array.isArray(c.remL)) { if (opt.remarks && Array.isArray(c.remL)) {
res.hints = c.remL.map(hint => profile.parseHint(profile, hint, {...c, ...res})) common.hints = c.remL.map(hint => profile.parseHint(ctx, hint))
findInTree(res, '**.remX', (idx, parent) => { findInTree(res, '**.remX', (idx, parent) => {
if ('number' === typeof idx) parent.hint = res.hints[idx] if ('number' === typeof idx) parent.hint = common.hints[idx]
}) })
} }
res.warnings = [] common.warnings = []
if (opt.remarks && Array.isArray(c.himL)) { if (opt.remarks && Array.isArray(c.himL)) {
res.warnings = c.himL.map(w => profile.parseWarning(profile, w, {...c, ...res})) common.warnings = c.himL.map(w => profile.parseWarning(ctx, w))
findInTree(res, '**.himX', (idx, parent) => { findInTree(res, '**.himX', (idx, parent) => {
if ('number' === typeof idx) parent.warning = res.warnings[idx] if ('number' === typeof idx) parent.warning = common.warnings[idx]
}) })
} }
res.polylines = [] common.polylines = []
if (opt.polylines && Array.isArray(c.polyL)) { if (opt.polylines && Array.isArray(c.polyL)) {
const parse = profile.parsePolyline(profile, opt, res) common.polylines = c.polyL.map(p => profile.parsePolyline(ctx, p))
res.polylines = c.polyL.map(parse)
// todo: **.ani.poly -> parsePolyline() // todo: **.ani.poly -> parsePolyline()
findInTree(res, '**.polyG.polyXL', (idxs, _, path) => { findInTree(res, '**.polyG.polyXL', (idxs, _, path) => {
const idx = idxs.find(idx => !!res.polylines[idx]) // find any given polyline const idx = idxs.find(idx => !!common.polylines[idx]) // find any given polyline
const jny = get(res, path.slice(0, -2)) const jny = get(res, path.slice(0, -2))
jny.polyline = res.polylines[idx] jny.polyline = common.polylines[idx]
}) })
} }
return res return common
} }
module.exports = parseCommonData module.exports = parseCommonData

View file

@ -4,8 +4,7 @@ const {DateTime, FixedOffsetZone, IANAZone} = require('luxon')
const timezones = new WeakMap() const timezones = new WeakMap()
// todo: change to `(profile) => (date, time) => {}` const parseDateTime = ({profile}, date, time, tzOffset = null, timestamp = false) => {
const parseDateTime = (profile, date, time, tzOffset = null, timestamp = false) => {
const pDate = [date.substr(-8, 4), date.substr(-4, 2), date.substr(-2, 2)] const pDate = [date.substr(-8, 4), date.substr(-4, 2), date.substr(-2, 2)]
if (!pDate[0] || !pDate[1] || !pDate[2]) { if (!pDate[0] || !pDate[1] || !pDate[2]) {
throw new Error('invalid date format: ' + date) throw new Error('invalid date format: ' + date)

View file

@ -3,8 +3,6 @@
const createParseArrOrDep = require('./arrival-or-departure') const createParseArrOrDep = require('./arrival-or-departure')
const DEPARTURE = 'd' const DEPARTURE = 'd'
const createParseDeparture = (profile, opt, data) => { const parseDeparture = createParseArrOrDep(DEPARTURE)
return createParseArrOrDep(profile, opt, data, DEPARTURE)
}
module.exports = createParseDeparture module.exports = parseDeparture

View file

@ -4,7 +4,6 @@ const codesByIcon = Object.assign(Object.create(null), {
cancel: 'cancelled' cancel: 'cancelled'
}) })
// todo: is passing in profile necessary?
// todo: pass in tag list from hint reference, e.g.: // todo: pass in tag list from hint reference, e.g.:
// "tagL": [ // "tagL": [
// "RES_JNY_H3" // H3 = level 3 heading? shown on overview // "RES_JNY_H3" // H3 = level 3 heading? shown on overview
@ -13,7 +12,7 @@ const codesByIcon = Object.assign(Object.create(null), {
// "RES_JNY_DTL" // only shown in journey detail // "RES_JNY_DTL" // only shown in journey detail
// ] // ]
// todo: https://github.com/public-transport/hafas-client/issues/5 // todo: https://github.com/public-transport/hafas-client/issues/5
const parseHint = (profile, h, _) => { const parseHint = (ctx, h) => {
// todo: C // todo: C
const text = h.txtN && h.txtN.trim() || '' const text = h.txtN && h.txtN.trim() || ''

View file

@ -1,6 +1,6 @@
'use strict' 'use strict'
const parseIcon = (profile, i) => { const parseIcon = (ctx, i) => {
const res = { const res = {
type: i.res || null, type: i.res || null,
title: i.text || i.txt || i.txtS || null title: i.text || i.txt || i.txtS || null

View file

@ -32,120 +32,116 @@ const applyRemarks = (leg, refs) => {
} }
} }
const createParseJourneyLeg = (profile, opt, data) => { // todo: pt.status, pt.isPartCncl
// todo: pt.status, pt.isPartCncl // todo: pt.isRchbl, pt.chRatingRT, pt.chgDurR, pt.minChg
// todo: pt.isRchbl, pt.chRatingRT, pt.chgDurR, pt.minChg // todo: pt.dep.dProgType, pt.arr.dProgType
// todo: pt.dep.dProgType, pt.arr.dProgType // todo: what is pt.jny.dirFlg?
// todo: what is pt.jny.dirFlg? // todo: what is pt.recState?
// todo: what is pt.recState? // todo: what is `sty: 'UNDEF'`?
// todo: what is `sty: 'UNDEF'`? // todo: pt.prodL
// todo: pt.prodL // todo: pt.parJnyL (list of coupled trains)
// todo: pt.parJnyL (list of coupled trains) // todo: pt.planrtTS
// j = journey, pt = part const parseJourneyLeg = (ctx, pt, date) => { // pt = raw leg
// todo: pt.planrtTS const {profile, opt} = ctx
const parseJourneyLeg = (j, pt, parseStopovers = true) => {
const res = {
origin: clone(pt.dep.location) || null,
destination: clone(pt.arr.location)
}
const arr = parseWhen(profile, j.date, pt.arr.aTimeS, pt.arr.aTimeR, pt.arr.aTZOffset, pt.arr.aCncl) const res = {
res.arrival = arr.when origin: clone(pt.dep.location) || null,
res.plannedArrival = arr.plannedWhen destination: clone(pt.arr.location)
res.arrivalDelay = arr.delay
if (arr.prognosedWhen) res.prognosedArrival = arr.prognosedWhen
const dep = parseWhen(profile, j.date, pt.dep.dTimeS, pt.dep.dTimeR, pt.dep.dTZOffset, pt.dep.dCncl)
res.departure = dep.when
res.plannedDeparture = dep.plannedWhen
res.departureDelay = dep.delay
if (dep.prognosedWhen) res.prognosedDeparture = dep.prognosedWhen
if (pt.jny) {
res.reachable = !!pt.jny.isRchbl
}
if (pt.jny && pt.jny.polyline) {
res.polyline = pt.jny.polyline || null
}
if (pt.type === 'WALK' || pt.type === 'TRSF') {
res.public = true
res.walking = true
res.distance = pt.gis && pt.gis.dist || null
if (pt.type === 'TRSF') res.transfer = true
// https://gist.github.com/derhuerst/426d4b95aeae701843b1e9c23105b8d4#file-tripsearch-2018-12-05-http-L4207-L4229
if (opt.remarks && Array.isArray(pt.gis.msgL)) {
applyRemarks(res, pt.gis.msgL)
}
} else if (pt.type === 'JNY') {
// todo: pull `public` value from `profile.products`
res.tripId = pt.jny.jid
res.line = pt.jny.line || null
res.direction = pt.jny.dirTxt && profile.parseStationName(pt.jny.dirTxt) || null
const arrPl = parsePlatform(profile, pt.arr.aPlatfS, pt.arr.aPlatfR, pt.arr.aCncl)
res.arrivalPlatform = arrPl.platform
res.plannedArrivalPlatform = arrPl.plannedPlatform
if (arrPl.prognosedPlatform) res.prognosedArrivalPlatform = arrPl.prognosedPlatform
const depPl = parsePlatform(profile, pt.dep.dPlatfS, pt.dep.dPlatfR, pt.dep.dCncl)
res.departurePlatform = depPl.platform
res.plannedDeparturePlatform = depPl.plannedPlatform
if (depPl.prognosedPlatform) res.prognosedDeparturePlatform = depPl.prognosedPlatform
if (parseStopovers && pt.jny.stopL) {
const parse = profile.parseStopover(profile, opt, data, j.date)
const stopL = pt.jny.stopL
res.stopovers = stopL.map(parse)
if (opt.remarks && Array.isArray(pt.jny.msgL)) {
// todo: apply leg-wide remarks if `parseStopovers` is false
applyRemarks(res, pt.jny.msgL)
}
// filter stations the train passes without stopping, as this doesn't comply with fptf (yet)
res.stopovers = res.stopovers.filter((x) => !x.passBy)
}
const freq = pt.jny.freq || {}
// todo: expose `res.cycle` even if only one field exists (breaking)
if (freq.minC && freq.maxC) {
res.cycle = {
min: freq.minC * 60,
max: freq.maxC * 60
}
// nr of connections in this frequency, from now on
if (freq.numC) res.cycle.nr = freq.numC
}
if (freq.jnyL) {
const parseAlternative = (a) => {
// todo: parse this just like a `leg` (breaking)
// todo: parse `a.stopL`, `a.ctxRecon`, `a.msgL`
const st0 = a.stopL[0] || {}
return {
tripId: a.jid,
line: a.line || null,
direction: a.dirTxt || null,
...parseWhen(profile, j.date, st0.dTimeS, st0.dTimeR, st0.dTZOffset, st0.dCncl)
}
}
res.alternatives = freq.jnyL.map(parseAlternative)
}
}
if (pt.arr.aCncl || pt.dep.dCncl) {
res.cancelled = true
Object.defineProperty(res, 'canceled', {value: true})
}
return res
} }
return parseJourneyLeg const arr = profile.parseWhen(ctx, date, pt.arr.aTimeS, pt.arr.aTimeR, pt.arr.aTZOffset, pt.arr.aCncl)
res.arrival = arr.when
res.plannedArrival = arr.plannedWhen
res.arrivalDelay = arr.delay
if (arr.prognosedWhen) res.prognosedArrival = arr.prognosedWhen
const dep = profile.parseWhen(ctx, date, pt.dep.dTimeS, pt.dep.dTimeR, pt.dep.dTZOffset, pt.dep.dCncl)
res.departure = dep.when
res.plannedDeparture = dep.plannedWhen
res.departureDelay = dep.delay
if (dep.prognosedWhen) res.prognosedDeparture = dep.prognosedWhen
if (pt.jny) {
res.reachable = !!pt.jny.isRchbl
}
if (pt.jny && pt.jny.polyline) {
res.polyline = pt.jny.polyline || null
}
if (pt.type === 'WALK' || pt.type === 'TRSF') {
res.public = true
res.walking = true
res.distance = pt.gis && pt.gis.dist || null
if (pt.type === 'TRSF') res.transfer = true
// https://gist.github.com/derhuerst/426d4b95aeae701843b1e9c23105b8d4#file-tripsearch-2018-12-05-http-L4207-L4229
if (opt.remarks && Array.isArray(pt.gis.msgL)) {
applyRemarks(res, pt.gis.msgL)
}
} else if (pt.type === 'JNY') {
// todo: pull `public` value from `profile.products`
res.tripId = pt.jny.jid
res.line = pt.jny.line || null
res.direction = pt.jny.dirTxt && profile.parseStationName(ctx, pt.jny.dirTxt) || null
const arrPl = profile.parsePlatform(ctx, pt.arr.aPlatfS, pt.arr.aPlatfR, pt.arr.aCncl)
res.arrivalPlatform = arrPl.platform
res.plannedArrivalPlatform = arrPl.plannedPlatform
if (arrPl.prognosedPlatform) res.prognosedArrivalPlatform = arrPl.prognosedPlatform
const depPl = profile.parsePlatform(ctx, pt.dep.dPlatfS, pt.dep.dPlatfR, pt.dep.dCncl)
res.departurePlatform = depPl.platform
res.plannedDeparturePlatform = depPl.plannedPlatform
if (depPl.prognosedPlatform) res.prognosedDeparturePlatform = depPl.prognosedPlatform
if (opt.stopovers && pt.jny.stopL) {
const stopL = pt.jny.stopL
res.stopovers = stopL.map(s => profile.parseStopover(ctx, s, date))
if (opt.remarks && Array.isArray(pt.jny.msgL)) {
// todo: apply leg-wide remarks if `opt.stopovers` is false
applyRemarks(res, pt.jny.msgL)
}
// filter stations the train passes without stopping, as this doesn't comply with fptf (yet)
res.stopovers = res.stopovers.filter((x) => !x.passBy)
}
const freq = pt.jny.freq || {}
// todo: expose `res.cycle` even if only one field exists (breaking)
if (freq.minC && freq.maxC) {
res.cycle = {
min: freq.minC * 60,
max: freq.maxC * 60
}
// nr of connections in this frequency, from now on
if (freq.numC) res.cycle.nr = freq.numC
}
if (freq.jnyL) {
const parseAlternative = (a) => {
// todo: parse this just like a `leg` (breaking)
// todo: parse `a.stopL`, `a.ctxRecon`, `a.msgL`
const st0 = a.stopL[0] || {}
return {
tripId: a.jid,
line: a.line || null,
direction: a.dirTxt || null,
...profile.parseWhen(ctx, date, st0.dTimeS, st0.dTimeR, st0.dTZOffset, st0.dCncl)
}
}
res.alternatives = freq.jnyL.map(parseAlternative)
}
}
if (pt.arr.aCncl || pt.dep.dCncl) {
res.cancelled = true
Object.defineProperty(res, 'canceled', {value: true})
}
return res
} }
module.exports = createParseJourneyLeg module.exports = parseJourneyLeg

View file

@ -22,47 +22,44 @@ const parseScheduledDays = (sDaysB, year, profile) => {
return res return res
} }
const createParseJourney = (profile, opt, data) => { // todo: c.conSubscr
const parseLeg = profile.parseJourneyLeg(profile, opt, data) // todo: c.trfRes x vbb-parse-ticket
// todo: c.conSubscr // todo: c.sotRating, c.isSotCon, c.sotCtxt
// todo: c.trfRes x vbb-parse-ticket // todo: c.showARSLink
// todo: c.sotRating, c.isSotCon, c.sotCtxt // todo: c.useableTime
// todo: c.showARSLink // todo: c.cksum
// todo: c.useableTime // todo: c.isNotRdbl
// todo: c.cksum // todo: c.badSecRefX
// todo: c.isNotRdbl // todo: c.bfATS, c.bfIOSTS
// todo: c.badSecRefX const parseJourney = (ctx, j) => { // j = raw jouney
// todo: c.bfATS, c.bfIOSTS const {profile, opt} = ctx
const parseJourney = (j) => {
const legs = j.secL.map(leg => parseLeg(j, leg))
const res = {
type: 'journey',
legs,
refreshToken: j.ctxRecon || null
}
const freq = j.freq || {} const legs = j.secL.map(l => profile.parseJourneyLeg(ctx, l, j.date))
if (freq.minC || freq.maxC) { const res = {
res.cycle = {} type: 'journey',
if (freq.minC) res.cycle.min = freq.minC * 60 legs,
if (freq.maxC) res.cycle.max = freq.maxC * 60 refreshToken: j.ctxRecon || null
// nr of connections in this frequency, from now on
if (freq.numC) res.cycle.nr = freq.numC
}
if (opt.remarks && Array.isArray(j.msgL)) {
res.remarks = findRemarks(j.msgL).map(([remark]) => remark)
}
if (opt.scheduledDays) {
const year = parseInt(j.date.slice(0, 4))
res.scheduledDays = parseScheduledDays(j.sDays.sDaysB, year, profile)
}
return res
} }
return parseJourney const freq = j.freq || {}
if (freq.minC || freq.maxC) {
res.cycle = {}
if (freq.minC) res.cycle.min = freq.minC * 60
if (freq.maxC) res.cycle.max = freq.maxC * 60
// nr of connections in this frequency, from now on
if (freq.numC) res.cycle.nr = freq.numC
}
if (opt.remarks && Array.isArray(j.msgL)) {
res.remarks = findRemarks(j.msgL).map(([remark]) => remark)
}
if (opt.scheduledDays) {
const year = parseInt(j.date.slice(0, 4))
res.scheduledDays = parseScheduledDays(j.sDays.sDaysB, year, profile)
}
return res
} }
module.exports = createParseJourney module.exports = parseJourney

View file

@ -2,45 +2,42 @@
const slugg = require('slugg') const slugg = require('slugg')
const createParseLine = (profile, opt, _) => { const parseLine = ({profile}, p) => {
const byBitmask = [] if (!p) return null // todo: handle this upstream
for (let product of profile.products) { const name = p.line || p.addName || p.name || null // wtf
for (let bitmask of product.bitmasks) { const res = {
byBitmask[bitmask] = product type: 'line',
// This is terrible, but FPTF demands an ID. Let's pray for HAFAS.
id: (
p.prodCtx && p.prodCtx.lineId && slugg(p.prodCtx.lineId.trim())
|| name && slugg(name.trim())
|| null
),
// todo: what is p.prodCtx.matchId? use as `id`? expose it.
fahrtNr: p.prodCtx && p.prodCtx.num || null,
name,
public: true
}
// todo: what is p.number?
// todo: what is p.prodCtx.catCode?
if ('cls' in p) {
// todo: use profile.products.find() for this
const byBitmask = []
for (let product of profile.products) {
for (let bitmask of product.bitmasks) {
byBitmask[bitmask] = product
}
} }
// todo: what if `p.cls` is the sum of two bitmasks?
const product = byBitmask[parseInt(p.cls)]
res.mode = product && product.mode || null
res.product = product && product.id || null
} }
// todo: p.himIdL if (p.operator) res.operator = p.operator // todo: move up
const parseLine = (p) => { return res
if (!p) return null // todo: handle this upstream
const name = p.line || p.addName || p.name || null // wtf
const res = {
type: 'line',
// This is terrible, but FPTF demands an ID. Let's pray for HAFAS.
id: (
p.prodCtx && p.prodCtx.lineId && slugg(p.prodCtx.lineId.trim())
|| name && slugg(name.trim())
|| null
),
// todo: what is p.prodCtx.matchId? use as `id`? expose it.
fahrtNr: p.prodCtx && p.prodCtx.num || null,
name,
public: true
}
// todo: what is p.number?
// todo: what is p.prodCtx.catCode?
if ('cls' in p) {
// todo: what if `p.cls` is the sum of two bitmasks?
const product = byBitmask[parseInt(p.cls)]
res.mode = product && product.mode || null
res.product = product && product.id || null
}
if (p.operator) res.operator = p.operator // todo: move up
return res
}
return parseLine
} }
module.exports = createParseLine module.exports = parseLine

View file

@ -9,7 +9,9 @@ const ADDRESS = 'A'
const leadingZeros = /^0+/ const leadingZeros = /^0+/
// todo: what is s.rRefL? // todo: what is s.rRefL?
const parseLocation = (profile, opt, _, l) => { const parseLocation = (ctx, l) => {
const {profile, opt} = ctx
const lid = parse(l.lid, {delimiter: '@'}) const lid = parse(l.lid, {delimiter: '@'})
const res = { const res = {
type: 'location', type: 'location',
@ -25,11 +27,11 @@ const parseLocation = (profile, opt, _, l) => {
const stop = { const stop = {
type: l.isMainMast ? 'station' : 'stop', type: l.isMainMast ? 'station' : 'stop',
id: res.id, id: res.id,
name: l.name || id.O ? profile.parseStationName(l.name || id.O) : null, name: l.name || id.O ? profile.parseStationName(ctx, l.name || id.O) : null,
location: 'number' === typeof res.latitude ? res : null // todo: remove `.id` location: 'number' === typeof res.latitude ? res : null // todo: remove `.id`
} }
if ('pCls' in l) stop.products = profile.parseProducts(l.pCls) if ('pCls' in l) stop.products = profile.parseProductsBitmask(ctx, l.pCls)
if ('meta' in l) stop.isMeta = !!l.meta if ('meta' in l) stop.isMeta = !!l.meta
if (opt.linesOfStops && Array.isArray(l.lines)) { if (opt.linesOfStops && Array.isArray(l.lines)) {

View file

@ -1,53 +1,49 @@
'use strict' 'use strict'
const createParseMovement = (profile, opt, data) => { // todo: what is m.dirGeo? maybe the speed?
// todo: what is m.dirGeo? maybe the speed? // todo: what is m.stopL?
// todo: what is m.stopL? // todo: what is m.proc? wut?
// todo: what is m.proc? wut? // todo: what is m.pos?
// todo: what is m.pos? // todo: what is m.ani.dirGeo[n]? maybe the speed?
// todo: what is m.ani.dirGeo[n]? maybe the speed? // todo: what is m.ani.proc[n]? wut?
// todo: what is m.ani.proc[n]? wut? const parseMovement = (ctx, m) => { // m = raw movement
const parseMovement = (m) => { const {profile, opt} = ctx
const pStopover = profile.parseStopover(profile, opt, data, m.date)
const res = { const res = {
direction: m.dirTxt ? profile.parseStationName(m.dirTxt) : null, direction: m.dirTxt ? profile.parseStationName(ctx, m.dirTxt) : null,
tripId: m.jid || null, tripId: m.jid || null,
line: m.line || null, line: m.line || null,
location: m.pos ? { location: m.pos ? {
type: 'location', type: 'location',
latitude: m.pos.y / 1000000, latitude: m.pos.y / 1000000,
longitude: m.pos.x / 1000000 longitude: m.pos.x / 1000000
} : null, } : null,
// todo: stopL[0] is the first of the trip! -> filter out // todo: stopL[0] is the first of the trip! -> filter out
nextStopovers: m.stopL.map(pStopover), nextStopovers: m.stopL.map(s => profile.parseStopover(ctx, s, m.date)),
frames: [] frames: []
}
if (m.ani) {
if (Array.isArray(m.ani.mSec)) {
for (let i = 0; i < m.ani.mSec.length; i++) {
res.frames.push({
origin: m.ani.fromLocations[i] || null,
destination: m.ani.toLocations[i] || null,
t: m.ani.mSec[i]
})
}
}
if (opt.polylines) {
if (m.ani.poly) {
const parse = profile.parsePolyline(profile, opt, data)
res.polyline = parse(m.ani.poly)
} else if (m.ani.polyline) {
res.polyline = m.ani.polyline
}
}
}
return res
} }
return parseMovement
if (m.ani) {
if (Array.isArray(m.ani.mSec)) {
for (let i = 0; i < m.ani.mSec.length; i++) {
res.frames.push({
origin: m.ani.fromLocations[i] || null,
destination: m.ani.toLocations[i] || null,
t: m.ani.mSec[i]
})
}
}
if (opt.polylines) {
if (m.ani.poly) {
res.polyline = profile.parsePolyline(ctx, m.ani.poly)
} else if (m.ani.polyline) {
res.polyline = m.ani.polyline
}
}
}
return res
} }
module.exports = createParseMovement module.exports = parseMovement

View file

@ -6,9 +6,8 @@
// todo: what is s.wt? // todo: what is s.wt?
// todo: what is s.dur? // todo: what is s.dur?
// todo: [breaking] change to createParseNearby(profile, data) => (n) => nearby const parseNearby = (ctx, n) => { // n = raw nearby location
const parseNearby = (profile, opt, data, n) => { const res = ctx.profile.parseLocation(ctx, n)
const res = profile.parseLocation(profile, opt, data, n)
res.distance = n.dist res.distance = n.dist
return res return res
} }

View file

@ -2,7 +2,7 @@
const slugg = require('slugg') const slugg = require('slugg')
const parseOperator = (profile, a) => { const parseOperator = (ctx, a) => {
const name = a.name && a.name.trim() const name = a.name && a.name.trim()
if (!name) return null if (!name) return null
return { return {

View file

@ -1,6 +1,6 @@
'use strict' 'use strict'
const parsePlatform = (profile, platfS, platfR, cncl = false) => { const parsePlatform = (ctx, platfS, platfR, cncl = false) => {
let planned = platfS || null let planned = platfS || null
let prognosed = platfR || null let prognosed = platfR || null

View file

@ -3,50 +3,47 @@
const {toGeoJSON} = require('@mapbox/polyline') const {toGeoJSON} = require('@mapbox/polyline')
const distance = require('gps-distance') const distance = require('gps-distance')
const createParsePolyline = (profile, opt, _) => { // todo: what is p.delta?
// todo: what is p.delta? // todo: what is p.type?
// todo: what is p.type? // todo: what is p.crdEncS?
// todo: what is p.crdEncS? // todo: what is p.crdEncF?
// todo: what is p.crdEncF? const parsePolyline = (ctx, p) => { // p = raw polyline
const parsePolyline = (p) => { const shape = toGeoJSON(p.crdEncYX)
const shape = toGeoJSON(p.crdEncYX) if (shape.coordinates.length === 0) return null
if (shape.coordinates.length === 0) return null
const res = shape.coordinates.map(crd => ({ const res = shape.coordinates.map(crd => ({
type: 'Feature', type: 'Feature',
properties: {}, properties: {},
geometry: { geometry: {
type: 'Point', type: 'Point',
coordinates: crd coordinates: crd
} }
})) }))
if (Array.isArray(p.ppLocRefL)) { if (Array.isArray(p.ppLocRefL)) {
for (let ref of p.ppLocRefL) { for (let ref of p.ppLocRefL) {
const p = res[ref.ppIdx] const p = res[ref.ppIdx]
if (p && ref.location) p.properties = ref.location if (p && ref.location) p.properties = ref.location
}
// 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 { // Often there is one more point right next to each point at a station.
type: 'FeatureCollection', // We filter them here if they are < 5m from each other.
features: res 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 parsePolyline
return {
type: 'FeatureCollection',
features: res
}
} }
module.exports = createParsePolyline module.exports = parsePolyline

View file

@ -1,22 +1,17 @@
'use strict' 'use strict'
const createParseBitmask = (profile) => { const parseBitmask = ({profile}, bitmask) => {
const defaultProducts = {} const res = {}
for (let product of profile.products) defaultProducts[product.id] = false for (let product of profile.products) res[product.id] = false
const parseBitmask = (bitmask) => { const bits = bitmask.toString(2).split('').map(i => parseInt(i)).reverse()
const res = Object.assign({}, defaultProducts) for (let i = 0; i < bits.length; i++) {
if (!bits[i]) continue // ignore `0`
const bits = bitmask.toString(2).split('').map(i => parseInt(i)).reverse() const product = profile.products.find(p => p.bitmasks.includes(Math.pow(2, i)))
for (let i = 0; i < bits.length; i++) { if (product) res[product.id] = true
if (!bits[i]) continue // ignore `0`
const product = profile.products.find(p => p.bitmasks.includes(Math.pow(2, i)))
if (product) res[product.id] = true
}
return res
} }
return parseBitmask return res
} }
module.exports = createParseBitmask module.exports = parseBitmask

View file

@ -4,48 +4,46 @@ const parseWhen = require('./when')
const parsePlatform = require('./platform') const parsePlatform = require('./platform')
const findRemarks = require('./find-remarks') const findRemarks = require('./find-remarks')
const createParseStopover = (profile, opt, data, date) => { const parseStopover = (ctx, st, date) => { // st = raw stopover
const parseStopover = (st) => { const {profile, opt} = ctx
const arr = parseWhen(profile, date, st.aTimeS, st.aTimeR, st.aTZOffset, st.aCncl)
const arrPl = parsePlatform(profile, st.aPlatfS, st.aPlatfR, st.aCncl)
const dep = parseWhen(profile, date, st.dTimeS, st.dTimeR, st.dTZOffset, st.dCncl)
const depPl = parsePlatform(profile, st.dPlatfS, st.dPlatfR, st.dCncl)
const res = { const arr = profile.parseWhen(ctx, date, st.aTimeS, st.aTimeR, st.aTZOffset, st.aCncl)
stop: st.location || null, const arrPl = profile.parsePlatform(ctx, st.aPlatfS, st.aPlatfR, st.aCncl)
arrival: arr.when, const dep = profile.parseWhen(ctx, date, st.dTimeS, st.dTimeR, st.dTZOffset, st.dCncl)
plannedArrival: arr.plannedWhen, const depPl = profile.parsePlatform(ctx, st.dPlatfS, st.dPlatfR, st.dCncl)
arrivalDelay: arr.delay,
arrivalPlatform: arrPl.platform,
plannedArrivalPlatform: arrPl.plannedPlatform,
departure: dep.when,
plannedDeparture: dep.plannedWhen,
departureDelay: dep.delay,
departurePlatform: depPl.platform,
plannedDeparturePlatform: depPl.plannedPlatform
}
if (arr.prognosedWhen) res.prognosedArrival = arr.prognosedWhen const res = {
if (arrPl.prognosedPlatform) res.prognosedArrivalPlatform = arrPl.prognosedPlatform stop: st.location || null,
if (dep.prognosedWhen) res.prognosedDeparture = dep.prognosedWhen arrival: arr.when,
if (depPl.prognosedPlatform) res.prognosedDeparturePlatform = depPl.prognosedPlatform plannedArrival: arr.plannedWhen,
arrivalDelay: arr.delay,
// mark stations the train passes without stopping arrivalPlatform: arrPl.platform,
if(st.dInS === false && st.aOutS === false) res.passBy = true plannedArrivalPlatform: arrPl.plannedPlatform,
departure: dep.when,
if (st.aCncl || st.dCncl) { plannedDeparture: dep.plannedWhen,
res.cancelled = true departureDelay: dep.delay,
Object.defineProperty(res, 'canceled', {value: true}) departurePlatform: depPl.platform,
} plannedDeparturePlatform: depPl.plannedPlatform
if (opt.remarks && Array.isArray(st.msgL)) {
res.remarks = findRemarks(st.msgL).map(([remark]) => remark)
}
return res
} }
return parseStopover if (arr.prognosedWhen) res.prognosedArrival = arr.prognosedWhen
if (arrPl.prognosedPlatform) res.prognosedArrivalPlatform = arrPl.prognosedPlatform
if (dep.prognosedWhen) res.prognosedDeparture = dep.prognosedWhen
if (depPl.prognosedPlatform) res.prognosedDeparturePlatform = depPl.prognosedPlatform
// mark stations the train passes without stopping
if(st.dInS === false && st.aOutS === false) res.passBy = true
if (st.aCncl || st.dCncl) {
res.cancelled = true
Object.defineProperty(res, 'canceled', {value: true})
}
if (opt.remarks && Array.isArray(st.msgL)) {
res.remarks = findRemarks(st.msgL).map(([remark]) => remark)
}
return res
} }
module.exports = createParseStopover module.exports = parseStopover

View file

@ -7,7 +7,7 @@ const typesByIcon = Object.assign(Object.create(null), {
HimWarn: 'status' HimWarn: 'status'
}) })
const parseMsgEdge = (profile) => (e) => { const parseMsgEdge = (ctx) => (e) => {
const res = omit(e, [ const res = omit(e, [
'icoX', 'icoX',
'fLocX', 'fromLocation', 'fLocX', 'fromLocation',
@ -19,18 +19,20 @@ const parseMsgEdge = (profile) => (e) => {
res.toLoc = e.toLocation || null res.toLoc = e.toLocation || null
return res return res
} }
const parseMsgEvent = (profile) => (e) => { const parseMsgEvent = ({profile}) => (e) => {
return { return {
// todo: rename `Loc` -> `Location` [breaking] // todo: rename `Loc` -> `Location` [breaking]
fromLoc: e.fromLocation || null, fromLoc: e.fromLocation || null,
toLoc: e.toLocation || null, toLoc: e.toLocation || null,
start: profile.parseDateTime(profile, e.fDate, e.fTime, null), start: profile.parseDateTime(ctx, e.fDate, e.fTime, null),
end: profile.parseDateTime(profile, e.tDate, e.tTime, null), end: profile.parseDateTime(ctx, e.tDate, e.tTime, null),
sections: e.sectionNums || [] // todo: parse sections: e.sectionNums || [] // todo: parse
} }
} }
const parseWarning = (profile, w, data) => { const parseWarning = (ctx, w) => {
const {profile, res: resp} = ctx
// todo: act, pub, lead, tckr, prod, comp, // todo: act, pub, lead, tckr, prod, comp,
// todo: cat (1, 2), pubChL, rRefL, impactL // todo: cat (1, 2), pubChL, rRefL, impactL
// pubChL: // pubChL:
@ -62,24 +64,24 @@ const parseWarning = (profile, w, data) => {
priority: w.prio, priority: w.prio,
category: w.cat || null // todo: parse to sth meaningful category: w.cat || null // todo: parse to sth meaningful
} }
if ('prod' in w) res.products = profile.parseProducts(w.prod) if ('prod' in w) res.products = profile.parseProductsBitmask(ctx, w.prod)
if (w.edgeRefL && data.himMsgEdgeL) { if (w.edgeRefL && resp.common && resp.common.himMsgEdgeL) {
res.edges = w.edgeRefL res.edges = w.edgeRefL
.map(i => data.himMsgEdgeL[i]) .map(i => resp.common.himMsgEdgeL[i])
.filter(e => !!e) .filter(e => !!e)
.map(parseMsgEdge(profile)) .map(parseMsgEdge(ctx))
} }
if (w.eventRefL && data.himMsgEventL) { if (w.eventRefL && resp.common && resp.common.himMsgEventL) {
res.events = w.eventRefL res.events = w.eventRefL
.map(i => data.himMsgEventL[i]) .map(i => resp.common.himMsgEventL[i])
.filter(e => !!e) .filter(e => !!e)
.map(parseMsgEvent(profile)) .map(parseMsgEvent(ctx))
} }
if (w.sDate && w.sTime) res.validFrom = profile.parseDateTime(profile, w.sDate, w.sTime, null) if (w.sDate && w.sTime) res.validFrom = profile.parseDateTime(ctx, w.sDate, w.sTime, null)
if (w.eDate && w.eTime) res.validUntil = profile.parseDateTime(profile, w.eDate, w.eTime, null) if (w.eDate && w.eTime) res.validUntil = profile.parseDateTime(ctx, w.eDate, w.eTime, null)
if (w.lModDate && w.lModTime) res.modified = profile.parseDateTime(profile, w.lModDate, w.lModTime, null) if (w.lModDate && w.lModTime) res.modified = profile.parseDateTime(ctx, w.lModDate, w.lModTime, null)
return res return res
} }

View file

@ -1,15 +1,15 @@
'use strict' 'use strict'
const parseWhen = (profile, date, timeS, timeR, tzOffset, cncl = false) => { const parseWhen = (ctx, date, timeS, timeR, tzOffset, cncl = false) => {
const parse = profile.parseDateTime const parse = ctx.profile.parseDateTime
let planned = timeS ? parse(profile, date, timeS, tzOffset, false) : null let planned = timeS ? parse(ctx, date, timeS, tzOffset, false) : null
let prognosed = timeR ? parse(profile, date, timeR, tzOffset, false) : null let prognosed = timeR ? parse(ctx, date, timeR, tzOffset, false) : null
let delay = null let delay = null
if (planned && prognosed) { if (planned && prognosed) {
const tPlanned = parse(profile, date, timeS, tzOffset, true) const tPlanned = parse(ctx, date, timeS, tzOffset, true)
const tPrognosed = parse(profile, date, timeR, tzOffset, true) const tPrognosed = parse(ctx, date, timeR, tzOffset, true)
delay = Math.round((tPrognosed - tPlanned) / 1000) delay = Math.round((tPrognosed - tPlanned) / 1000)
} }

View file

@ -3,48 +3,53 @@
const test = require('tape') const test = require('tape')
const parse = require('../../parse/date-time') const parse = require('../../parse/date-time')
const profile = { const ctx = {
timezone: 'Europe/Berlin', common: {},
locale: 'de-DE' opt: {},
profile: {
timezone: 'Europe/Berlin',
locale: 'de-DE'
}
} }
test('date & time parsing uses profile.timezone', (t) => {
const iso = parse({
...profile, timezone: 'Europe/Moscow'
}, '20190819', '203000', undefined, false)
t.equal(iso, '2019-08-19T20:30:00+03:00')
t.end()
})
test('date & time parsing returns a timestamp', (t) => { test('date & time parsing returns a timestamp', (t) => {
const iso = parse(profile, '20190819', '203000', undefined, false) const iso = parse(ctx, '20190819', '203000', undefined, false)
const ts = parse(profile, '20190819', '203000', undefined, true) const ts = parse(ctx, '20190819', '203000', undefined, true)
t.equal(ts, +new Date(iso)) t.equal(ts, +new Date(iso))
t.equal(ts, 1566239400 * 1000) t.equal(ts, 1566239400 * 1000)
t.end() t.end()
}) })
test('date & time parsing uses tzOffset', (t) => { test('date & time parsing uses tzOffset', (t) => {
const iso = parse(profile, '20190819', '203000', -120, false) const iso = parse(ctx, '20190819', '203000', -120, false)
t.equal(iso, '2019-08-19T20:30:00-02:00') t.equal(iso, '2019-08-19T20:30:00-02:00')
t.end() t.end()
}) })
test('date & time parsing works with day "overflow"', (t) => { test('date & time parsing works with day "overflow"', (t) => {
const iso = parse(profile, '20190819', '02203000', undefined, false) const iso = parse(ctx, '20190819', '02203000', undefined, false)
t.equal(iso, '2019-08-21T20:30:00+02:00') t.equal(iso, '2019-08-21T20:30:00+02:00')
t.end() t.end()
}) })
// #106 // #106
test('date & time parsing works with day "overflow" & tzOffset', (t) => { test('date & time parsing works with day "overflow" & tzOffset', (t) => {
const iso = parse(profile, '20190819', '02203000', -120, false) const iso = parse(ctx, '20190819', '02203000', -120, false)
t.equal(iso, '2019-08-21T20:30:00-02:00') t.equal(iso, '2019-08-21T20:30:00-02:00')
t.end() t.end()
}) })
test('date & time parsing works with summer & winter time', (t) => { test('date & time parsing works with summer & winter time', (t) => {
const iso = parse(profile, '20190219', '203000', undefined, false) const iso = parse(ctx, '20190219', '203000', undefined, false)
t.equal(iso, '2019-02-19T20:30:00+01:00') t.equal(iso, '2019-02-19T20:30:00+01:00')
t.end() t.end()
}) })
test('date & time parsing uses profile.timezone', (t) => {
const iso = parse({
...ctx,
profile: {...ctx.profile, timezone: 'Europe/Moscow'}
}, '20190819', '203000', undefined, false)
t.equal(iso, '2019-08-19T20:30:00+03:00')
t.end()
})

View file

@ -3,7 +3,11 @@
const test = require('tape') const test = require('tape')
const parse = require('../../parse/hint') const parse = require('../../parse/hint')
const profile = {} const ctx = {
data: {},
opt: {},
profile: {}
}
test('parses hints correctly', (t) => { test('parses hints correctly', (t) => {
const input = { const input = {
@ -18,20 +22,20 @@ test('parses hints correctly', (t) => {
text: 'some text' text: 'some text'
} }
t.deepEqual(parse(profile, input), expected) t.deepEqual(parse(ctx, input), expected)
t.deepEqual(parse(profile, { t.deepEqual(parse(ctx, {
...input, type: 'I' ...input, type: 'I'
}), expected) }), expected)
// alternative trip // alternative trip
t.deepEqual(parse(profile, { t.deepEqual(parse(ctx, {
...input, type: 'L', jid: 'trip id' ...input, type: 'L', jid: 'trip id'
}), { }), {
...expected, type: 'status', code: 'alternative-trip', tripId: 'trip id' ...expected, type: 'status', code: 'alternative-trip', tripId: 'trip id'
}) })
// type: M // type: M
t.deepEqual(parse(profile, { t.deepEqual(parse(ctx, {
...input, type: 'M', txtS: 'some summary' ...input, type: 'M', txtS: 'some summary'
}), { }), {
...expected, type: 'status', summary: 'some summary' ...expected, type: 'status', summary: 'some summary'
@ -39,18 +43,18 @@ test('parses hints correctly', (t) => {
// type: D // type: D
for (const type of ['D', 'U', 'R', 'N', 'Y']) { for (const type of ['D', 'U', 'R', 'N', 'Y']) {
t.deepEqual(parse(profile, {...input, type}), { t.deepEqual(parse(ctx, {...input, type}), {
...expected, type: 'status' ...expected, type: 'status'
}) })
} }
// .code via .icon // .code via .icon
t.deepEqual(parse(profile, { t.deepEqual(parse(ctx, {
...input, code: null, icon: {type: 'cancel'} ...input, code: null, icon: {type: 'cancel'}
}), {...expected, code: 'cancelled'}) }), {...expected, code: 'cancelled'})
// invalid // invalid
t.equal(parse(profile, {...input, type: 'X'}), null) t.equal(parse(ctx, {...input, type: 'X'}), null)
t.end() t.end()
}) })

View file

@ -3,14 +3,18 @@
const test = require('tape') const test = require('tape')
const parse = require('../../parse/icon') const parse = require('../../parse/icon')
test('parses icons correctly', (t) => { const ctx = {
const profile = {} data: {},
opt: {},
profile: {}
}
test('parses icons correctly', (t) => {
const text = { const text = {
"res": "BVG", "res": "BVG",
"text": "Berliner Verkehrsbetriebe" "text": "Berliner Verkehrsbetriebe"
} }
t.deepEqual(parse(profile, text), { t.deepEqual(parse(ctx, text), {
type: 'BVG', type: 'BVG',
title: 'Berliner Verkehrsbetriebe' title: 'Berliner Verkehrsbetriebe'
}) })
@ -19,7 +23,7 @@ test('parses icons correctly', (t) => {
"res": "PROD_BUS", "res": "PROD_BUS",
"txtS": "18" "txtS": "18"
} }
t.deepEqual(parse(profile, txtS), { t.deepEqual(parse(ctx, txtS), {
type: 'PROD_BUS', type: 'PROD_BUS',
title: '18' title: '18'
}) })
@ -28,7 +32,7 @@ test('parses icons correctly', (t) => {
"res": "RBB", "res": "RBB",
"txt": "Regionalbus Braunschweig GmbH" "txt": "Regionalbus Braunschweig GmbH"
} }
t.deepEqual(parse(profile, txt), { t.deepEqual(parse(ctx, txt), {
type: 'RBB', type: 'RBB',
title: 'Regionalbus Braunschweig GmbH' title: 'Regionalbus Braunschweig GmbH'
}) })
@ -36,7 +40,7 @@ test('parses icons correctly', (t) => {
const noText = { const noText = {
"res": "attr_bike_r" "res": "attr_bike_r"
} }
t.deepEqual(parse(profile, noText), { t.deepEqual(parse(ctx, noText), {
type: 'attr_bike_r', type: 'attr_bike_r',
title: null title: null
}) })
@ -56,7 +60,7 @@ test('parses icons correctly', (t) => {
"a": 255 "a": 255
} }
} }
t.deepEqual(parse(profile, withColor), { t.deepEqual(parse(ctx, withColor), {
type: 'prod_sub_t', type: 'prod_sub_t',
title: null, title: null,
fgColor: {r: 255, g: 255, b: 255, a: 255}, fgColor: {r: 255, g: 255, b: 255, a: 255},

View file

@ -1,7 +1,7 @@
'use strict' 'use strict'
const test = require('tape') const test = require('tape')
const parser = require('../../parse/line') const parse = require('../../parse/line')
const profile = { const profile = {
products: [ products: [
@ -10,8 +10,11 @@ const profile = {
{id: 'bus', bitmasks: [4, 8]} {id: 'bus', bitmasks: [4, 8]}
] ]
} }
const opt = {} const ctx = {
const parse = parser(profile, opt, {}) data: {},
opt: {},
profile
}
test('parses lines correctly', (t) => { test('parses lines correctly', (t) => {
const input = { const input = {
@ -29,23 +32,23 @@ test('parses lines correctly', (t) => {
public: true public: true
} }
t.deepEqual(parse(input), expected) t.deepEqual(parse(ctx, input), expected)
t.deepEqual(parse({ t.deepEqual(parse(ctx, {
...input, line: null, addName: input.line ...input, line: null, addName: input.line
}), expected) }), expected)
t.deepEqual(parse({ t.deepEqual(parse(ctx, {
...input, line: null, name: input.line ...input, line: null, name: input.line
}), expected) }), expected)
// no prodCtx.lineId // no prodCtx.lineId
t.deepEqual(parse({ t.deepEqual(parse(ctx, {
...input, prodCtx: {...input.prodCtx, lineId: null} ...input, prodCtx: {...input.prodCtx, lineId: null}
}), { }), {
...expected, id: 'foo-line' ...expected, id: 'foo-line'
}) })
// no prodCtx // no prodCtx
t.deepEqual(parse({ t.deepEqual(parse(ctx, {
...input, prodCtx: undefined ...input, prodCtx: undefined
}), { }), {
...expected, id: 'foo-line', fahrtNr: null ...expected, id: 'foo-line', fahrtNr: null

View file

@ -5,11 +5,16 @@ const omit = require('lodash/omit')
const parse = require('../../parse/location') const parse = require('../../parse/location')
const profile = { const profile = {
parseStationName: name => name.toLowerCase(), parseStationName: (_, name) => name.toLowerCase(),
parseProducts: bitmask => [bitmask] parseProductsBitmask: (_, bitmask) => [bitmask]
} }
const opt = {
linesOfStops: false const ctx = {
data: {},
opt: {
linesOfStops: false
},
profile
} }
test('parses an address correctly', (t) => { test('parses an address correctly', (t) => {
@ -20,7 +25,7 @@ test('parses an address correctly', (t) => {
crd: {x: 13418027, y: 52515503} crd: {x: 13418027, y: 52515503}
} }
const address = parse(profile, opt, null, input) const address = parse(ctx, input)
t.deepEqual(address, { t.deepEqual(address, {
type: 'location', type: 'location',
id: 'some id', id: 'some id',
@ -40,7 +45,7 @@ test('parses a POI correctly', (t) => {
crd: {x: 13418027, y: 52515503} crd: {x: 13418027, y: 52515503}
} }
const poi = parse(profile, opt, null, input) const poi = parse(ctx, input)
t.deepEqual(poi, { t.deepEqual(poi, {
type: 'location', type: 'location',
poi: true, poi: true,
@ -50,10 +55,10 @@ test('parses a POI correctly', (t) => {
longitude: 13.418027 longitude: 13.418027
}) })
const withExtId = parse(profile, opt, null, {...input, extId: 'some ext id'}) const withExtId = parse(ctx, {...input, extId: 'some ext id'})
t.equal(withExtId.id, 'some ext id') t.equal(withExtId.id, 'some ext id')
const withLeadingZero = parse(profile, opt, null, {...input, extId: '00some ext id'}) const withLeadingZero = parse(ctx, {...input, extId: '00some ext id'})
t.equal(withLeadingZero.id, 'some ext id') t.equal(withLeadingZero.id, 'some ext id')
t.end() t.end()
@ -68,7 +73,7 @@ test('parses a stop correctly', (t) => {
pCls: 123 pCls: 123
} }
const stop = parse(profile, opt, null, input) const stop = parse(ctx, input)
t.deepEqual(stop, { t.deepEqual(stop, {
type: 'stop', type: 'stop',
id: 'foo stop', id: 'foo stop',
@ -82,17 +87,20 @@ test('parses a stop correctly', (t) => {
products: [123] products: [123]
}) })
const withoutLoc = parse(profile, opt, null, omit(input, ['crd'])) const withoutLoc = parse(ctx, omit(input, ['crd']))
t.equal(withoutLoc.location, null) t.equal(withoutLoc.location, null)
const mainMast = parse(profile, opt, null, {...input, isMainMast: true}) const mainMast = parse(ctx, {...input, isMainMast: true})
t.equal(mainMast.type, 'station') t.equal(mainMast.type, 'station')
const meta = parse(profile, opt, null, {...input, meta: 1}) const meta = parse(ctx, {...input, meta: 1})
t.equal(meta.isMeta, true) t.equal(meta.isMeta, true)
const lineA = {id: 'a'} const lineA = {id: 'a'}
const withLines = parse(profile, {...opt, linesOfStops: true}, null, { const withLines = parse({
...ctx,
opt: {...ctx.opt, linesOfStops: true}
}, {
...input, lines: [lineA] ...input, lines: [lineA]
}) })
t.deepEqual(withLines.lines, [lineA]) t.deepEqual(withLines.lines, [lineA])

View file

@ -3,16 +3,19 @@
const test = require('tape') const test = require('tape')
const parse = require('../../parse/operator') const parse = require('../../parse/operator')
const ctx = {
data: {},
opt: {},
profile: {}
}
test('parses an operator correctly', (t) => { test('parses an operator correctly', (t) => {
const profile = {}
const op = { const op = {
"name": "Berliner Verkehrsbetriebe", "name": "Berliner Verkehrsbetriebe",
"icoX": 1, "icoX": 1,
"id": "Berliner Verkehrsbetriebe" "id": "Berliner Verkehrsbetriebe"
} }
t.deepEqual(parse(profile, op), { t.deepEqual(parse(ctx, op), {
type: 'operator', type: 'operator',
id: 'berliner-verkehrsbetriebe', id: 'berliner-verkehrsbetriebe',
name: 'Berliner Verkehrsbetriebe' name: 'Berliner Verkehrsbetriebe'

View file

@ -4,9 +4,14 @@ const test = require('tape')
const parse = require('../../parse/warning') const parse = require('../../parse/warning')
const profile = { const profile = {
parseProducts: bitmask => [bitmask], parseProductsBitmask: (_, bitmask) => [bitmask],
parseDateTime: (_, date, time) => date + ':' + time parseDateTime: (_, date, time) => date + ':' + time
} }
const ctx = {
data: {},
opt: {},
profile
}
test('parses warnings correctly', (t) => { test('parses warnings correctly', (t) => {
const input = { const input = {
@ -27,26 +32,26 @@ test('parses warnings correctly', (t) => {
category: 1 category: 1
} }
t.deepEqual(parse(profile, input), expected) t.deepEqual(parse(ctx, input), expected)
// without basic fields // without basic fields
t.deepEqual(parse(profile, {...input, hid: null}), {...expected, id: null}) t.deepEqual(parse(ctx, {...input, hid: null}), {...expected, id: null})
t.deepEqual(parse(profile, {...input, head: null}), {...expected, summary: null}) t.deepEqual(parse(ctx, {...input, head: null}), {...expected, summary: null})
t.deepEqual(parse(profile, {...input, text: null}), {...expected, text: null}) t.deepEqual(parse(ctx, {...input, text: null}), {...expected, text: null})
t.deepEqual(parse(profile, {...input, cat: null}), {...expected, category: null}) t.deepEqual(parse(ctx, {...input, cat: null}), {...expected, category: null})
// without icon // without icon
t.deepEqual(parse(profile, {...input, icon: null}), { t.deepEqual(parse(ctx, {...input, icon: null}), {
...expected, type: 'warning', icon: null ...expected, type: 'warning', icon: null
}) })
// with products // with products
t.deepEqual(parse(profile, {...input, prod: 123}), { t.deepEqual(parse(ctx, {...input, prod: 123}), {
...expected, products: [123] ...expected, products: [123]
}) })
// validFrom, validUntil, modified // validFrom, validUntil, modified
t.deepEqual(parse(profile, { t.deepEqual(parse(ctx, {
...input, ...input,
sDate: '20190101', sTime: '094020', sDate: '20190101', sTime: '094020',
eDate: '20190101', eTime: '114020', eDate: '20190101', eTime: '114020',

View file

@ -4,11 +4,16 @@ const test = require('tape')
const parse = require('../../parse/when') const parse = require('../../parse/when')
const profile = { const profile = {
parseDateTime: (profile, date, time, tzOffset, timestamp = false) => { parseDateTime: ({profile}, date, time, tzOffset, timestamp = false) => {
if (timestamp) return ((date + '' + time) - tzOffset * 60) * 1000 if (timestamp) return ((date + '' + time) - tzOffset * 60) * 1000
return date + ':' + time return date + ':' + time
} }
} }
const ctx = {
data: {},
opt: {},
profile
}
test('parseWhen works correctly', (t) => { test('parseWhen works correctly', (t) => {
const date = '20190606' const date = '20190606'
@ -21,15 +26,15 @@ test('parseWhen works correctly', (t) => {
delay: 130 // seconds delay: 130 // seconds
} }
t.deepEqual(parse(profile, date, timeS, timeR, tzOffset), expected) t.deepEqual(parse(ctx, date, timeS, timeR, tzOffset), expected)
// no realtime data // no realtime data
t.deepEqual(parse(profile, date, timeS, null, tzOffset), { t.deepEqual(parse(ctx, date, timeS, null, tzOffset), {
...expected, when: expected.plannedWhen, delay: null ...expected, when: expected.plannedWhen, delay: null
}) })
// cancelled // cancelled
t.deepEqual(parse(profile, date, timeS, timeR, tzOffset, true), { t.deepEqual(parse(ctx, date, timeS, timeR, tzOffset, true), {
...expected, ...expected,
when: null, when: null,
prognosedWhen: expected.when prognosedWhen: expected.when