2016-06-22 01:39:04 +02:00
|
|
|
|
'use strict'
|
|
|
|
|
|
2018-03-18 00:13:44 +01:00
|
|
|
|
const isObj = require('lodash/isObject')
|
2018-08-25 01:52:46 +02:00
|
|
|
|
const sortBy = require('lodash/sortBy')
|
2020-03-09 21:44:35 +01:00
|
|
|
|
const omit = require('lodash/omit')
|
2017-11-20 15:43:13 +01:00
|
|
|
|
|
2017-11-12 14:52:04 +01:00
|
|
|
|
const defaultProfile = require('./lib/default-profile')
|
2018-03-16 15:38:16 +01:00
|
|
|
|
const validateProfile = require('./lib/validate-profile')
|
2020-04-13 17:06:50 +02:00
|
|
|
|
const {INVALID_REQUEST} = require('./lib/errors')
|
2018-10-02 16:36:37 +02:00
|
|
|
|
const sliceLeg = require('./lib/slice-leg')
|
2022-05-03 23:21:44 +02:00
|
|
|
|
const {HafasError} = require('./lib/errors')
|
2016-06-22 02:09:02 +02:00
|
|
|
|
|
2018-03-04 19:53:53 +01:00
|
|
|
|
const isNonEmptyString = str => 'string' === typeof str && str.length > 0
|
2018-02-28 16:09:23 +01:00
|
|
|
|
|
2018-08-25 01:52:46 +02:00
|
|
|
|
const validateLocation = (loc, name = 'location') => {
|
|
|
|
|
if (!isObj(loc)) {
|
2019-02-13 17:55:57 +01:00
|
|
|
|
throw new TypeError(name + ' must be an object.')
|
2018-08-25 01:52:46 +02:00
|
|
|
|
} else if (loc.type !== 'location') {
|
2019-02-13 17:55:57 +01:00
|
|
|
|
throw new TypeError('invalid location object.')
|
2018-08-25 01:52:46 +02:00
|
|
|
|
} else if ('number' !== typeof loc.latitude) {
|
2019-02-13 17:55:57 +01:00
|
|
|
|
throw new TypeError(name + '.latitude must be a number.')
|
2018-08-25 01:52:46 +02:00
|
|
|
|
} else if ('number' !== typeof loc.longitude) {
|
2019-02-13 17:55:57 +01:00
|
|
|
|
throw new TypeError(name + '.longitude must be a number.')
|
2018-08-25 01:52:46 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-09 20:54:43 +01:00
|
|
|
|
const validateWhen = (when, name = 'when') => {
|
|
|
|
|
if (Number.isNaN(+when)) {
|
|
|
|
|
throw new TypeError(name + ' is invalid')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-28 22:15:22 +02:00
|
|
|
|
const createClient = (profile, userAgent, opt = {}) => {
|
2017-11-11 22:49:04 +01:00
|
|
|
|
profile = Object.assign({}, defaultProfile, profile)
|
2017-12-12 03:21:12 +01:00
|
|
|
|
validateProfile(profile)
|
2017-10-03 17:36:42 +02:00
|
|
|
|
|
2018-07-19 21:50:20 +02:00
|
|
|
|
if ('string' !== typeof userAgent) {
|
2019-02-13 17:55:57 +01:00
|
|
|
|
throw new TypeError('userAgent must be a string');
|
2018-07-19 21:50:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
2021-12-29 21:11:07 +01:00
|
|
|
|
const _stationBoard = async (station, type, resultsField, parse, opt = {}) => {
|
2018-02-28 16:09:23 +01:00
|
|
|
|
if (isObj(station)) station = profile.formatStation(station.id)
|
2018-01-04 16:19:42 +01:00
|
|
|
|
else if ('string' === typeof station) station = profile.formatStation(station)
|
2019-02-13 17:55:57 +01:00
|
|
|
|
else throw new TypeError('station must be an object or a string.')
|
2017-11-12 18:06:16 +01:00
|
|
|
|
|
2018-06-26 15:49:50 +02:00
|
|
|
|
if ('string' !== typeof type || !type) {
|
2019-02-13 17:55:57 +01:00
|
|
|
|
throw new TypeError('type must be a non-empty string.')
|
2018-06-26 15:49:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-12-30 15:21:04 +01:00
|
|
|
|
if (!profile.departuresGetPasslist && ('stopovers' in opt)) {
|
|
|
|
|
throw new Error('opt.stopovers is not supported by this endpoint')
|
|
|
|
|
}
|
|
|
|
|
if (!profile.departuresStbFltrEquiv && ('includeRelatedStations' in opt)) {
|
|
|
|
|
throw new Error('opt.includeRelatedStations is not supported by this endpoint')
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-12 18:06:16 +01:00
|
|
|
|
opt = Object.assign({
|
2019-10-28 17:42:33 +01:00
|
|
|
|
// todo: for arrivals(), this is actually a station it *has already* stopped by
|
|
|
|
|
direction: null, // only show departures stopping by this station
|
2020-03-09 21:59:17 +01:00
|
|
|
|
line: null, // filter by line ID
|
2018-06-28 13:00:33 +02:00
|
|
|
|
duration: 10, // show departures for the next n minutes
|
2020-03-25 21:54:38 +01:00
|
|
|
|
results: null, // max. number of results; `null` means "whatever HAFAS wants"
|
2020-03-18 20:04:39 +01:00
|
|
|
|
subStops: true, // parse & expose sub-stops of stations?
|
|
|
|
|
entrances: true, // parse & expose entrances of stops/stations?
|
2019-01-23 13:03:01 +08:00
|
|
|
|
linesOfStops: false, // parse & expose lines at the stop/station?
|
2018-07-10 18:59:54 +02:00
|
|
|
|
remarks: true, // parse & expose hints & warnings?
|
2018-12-27 20:17:23 +01:00
|
|
|
|
stopovers: false, // fetch & parse previous/next stopovers?
|
2018-07-10 18:59:54 +02:00
|
|
|
|
// departures at related stations
|
|
|
|
|
// e.g. those that belong together on the metro map.
|
|
|
|
|
includeRelatedStations: true
|
2017-11-12 18:06:16 +01:00
|
|
|
|
}, opt)
|
2018-05-21 17:10:42 +02:00
|
|
|
|
opt.when = new Date(opt.when || Date.now())
|
|
|
|
|
if (Number.isNaN(+opt.when)) throw new Error('opt.when is invalid')
|
2019-10-20 01:47:09 +02:00
|
|
|
|
|
2019-10-31 20:08:56 +01:00
|
|
|
|
const req = profile.formatStationBoardReq({profile, opt}, station, type)
|
|
|
|
|
|
2021-12-29 18:53:50 +01:00
|
|
|
|
const {res, common} = await profile.request({profile, opt}, userAgent, req)
|
2019-10-20 01:47:09 +02:00
|
|
|
|
|
2021-12-29 18:53:50 +01:00
|
|
|
|
const ctx = {profile, opt, common, res}
|
2021-12-29 21:11:07 +01:00
|
|
|
|
const jnyL = Array.isArray(res.jnyL) ? res.jnyL : []
|
|
|
|
|
const results = jnyL.map(res => parse(ctx, res))
|
2021-12-29 18:53:50 +01:00
|
|
|
|
.sort((a, b) => new Date(a.when) - new Date(b.when)) // todo
|
2021-12-29 21:11:07 +01:00
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
[resultsField]: results,
|
|
|
|
|
realtimeDataUpdatedAt: res.planrtTS && res.planrtTS !== '0'
|
|
|
|
|
? parseInt(res.planrtTS)
|
|
|
|
|
: null,
|
|
|
|
|
}
|
2017-11-12 18:06:16 +01:00
|
|
|
|
}
|
|
|
|
|
|
2021-12-29 18:53:50 +01:00
|
|
|
|
const departures = async (station, opt = {}) => {
|
2021-12-29 21:11:07 +01:00
|
|
|
|
return await _stationBoard(station, 'DEP', 'departures', profile.parseDeparture, opt)
|
2018-06-26 17:11:25 +02:00
|
|
|
|
}
|
2021-12-29 18:53:50 +01:00
|
|
|
|
const arrivals = async (station, opt = {}) => {
|
2021-12-29 21:11:07 +01:00
|
|
|
|
return await _stationBoard(station, 'ARR', 'arrivals', profile.parseArrival, opt)
|
2018-06-26 17:11:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
2021-12-29 18:53:50 +01:00
|
|
|
|
const journeys = async (from, to, opt = {}) => {
|
2018-04-17 18:43:42 +02:00
|
|
|
|
from = profile.formatLocation(profile, from, 'from')
|
|
|
|
|
to = profile.formatLocation(profile, to, 'to')
|
2017-11-12 20:02:32 +01:00
|
|
|
|
|
2018-03-04 19:53:53 +01:00
|
|
|
|
if (('earlierThan' in opt) && ('laterThan' in opt)) {
|
2019-02-13 17:55:57 +01:00
|
|
|
|
throw new TypeError('opt.earlierThan and opt.laterThan are mutually exclusive.')
|
2018-05-30 16:15:26 +02:00
|
|
|
|
}
|
|
|
|
|
if (('departure' in opt) && ('arrival' in opt)) {
|
2019-02-13 17:55:57 +01:00
|
|
|
|
throw new TypeError('opt.departure and opt.arrival are mutually exclusive.')
|
2018-03-04 19:53:53 +01:00
|
|
|
|
}
|
|
|
|
|
let journeysRef = null
|
|
|
|
|
if ('earlierThan' in opt) {
|
|
|
|
|
if (!isNonEmptyString(opt.earlierThan)) {
|
2019-02-13 17:55:57 +01:00
|
|
|
|
throw new TypeError('opt.earlierThan must be a non-empty string.')
|
2018-03-04 19:53:53 +01:00
|
|
|
|
}
|
2018-05-28 20:34:24 +02:00
|
|
|
|
if (('departure' in opt) || ('arrival' in opt)) {
|
2019-02-13 17:55:57 +01:00
|
|
|
|
throw new TypeError('opt.earlierThan and opt.departure/opt.arrival are mutually exclusive.')
|
2018-03-04 19:53:53 +01:00
|
|
|
|
}
|
|
|
|
|
journeysRef = opt.earlierThan
|
|
|
|
|
}
|
|
|
|
|
if ('laterThan' in opt) {
|
|
|
|
|
if (!isNonEmptyString(opt.laterThan)) {
|
2019-02-13 17:55:57 +01:00
|
|
|
|
throw new TypeError('opt.laterThan must be a non-empty string.')
|
2018-03-04 19:53:53 +01:00
|
|
|
|
}
|
2018-05-28 20:34:24 +02:00
|
|
|
|
if (('departure' in opt) || ('arrival' in opt)) {
|
2019-02-13 17:55:57 +01:00
|
|
|
|
throw new TypeError('opt.laterThan and opt.departure/opt.arrival are mutually exclusive.')
|
2018-03-04 19:53:53 +01:00
|
|
|
|
}
|
|
|
|
|
journeysRef = opt.laterThan
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-12 20:02:32 +01:00
|
|
|
|
opt = Object.assign({
|
2020-02-27 15:57:54 +01:00
|
|
|
|
results: null, // number of journeys – `null` means "whatever HAFAS returns"
|
2017-11-12 20:02:32 +01:00
|
|
|
|
via: null, // let journeys pass this station?
|
2018-06-13 20:39:33 +02:00
|
|
|
|
stopovers: false, // return stations on the way?
|
2020-08-01 13:13:31 +02:00
|
|
|
|
transfers: -1, // maximum nr of transfers
|
2017-11-12 20:02:32 +01:00
|
|
|
|
transferTime: 0, // minimum time for a single transfer in minutes
|
|
|
|
|
// todo: does this work with every endpoint?
|
|
|
|
|
accessibility: 'none', // 'none', 'partial' or 'complete'
|
|
|
|
|
bike: false, // only bike-friendly journeys
|
2018-10-25 16:11:19 +02:00
|
|
|
|
walkingSpeed: 'normal', // 'slow', 'normal', 'fast'
|
2018-06-25 18:33:22 +02:00
|
|
|
|
// Consider walking to nearby stations at the beginning of a journey?
|
2018-07-23 20:42:22 +02:00
|
|
|
|
startWithWalking: true,
|
2020-05-21 17:31:51 +02:00
|
|
|
|
tickets: false, // return tickets?
|
|
|
|
|
polylines: false, // return leg shapes?
|
2020-03-18 20:04:39 +01:00
|
|
|
|
subStops: true, // parse & expose sub-stops of stations?
|
|
|
|
|
entrances: true, // parse & expose entrances of stops/stations?
|
2020-05-21 17:31:51 +02:00
|
|
|
|
remarks: true, // parse & expose hints & warnings?
|
2022-10-24 17:30:05 +02:00
|
|
|
|
scheduledDays: false, // parse & expose dates each journey is valid on?
|
2017-11-12 20:02:32 +01:00
|
|
|
|
}, opt)
|
2018-04-17 18:43:42 +02:00
|
|
|
|
if (opt.via) opt.via = profile.formatLocation(profile, opt.via, 'opt.via')
|
2018-05-28 20:34:24 +02:00
|
|
|
|
|
|
|
|
|
if (opt.when !== undefined) {
|
|
|
|
|
throw new Error('opt.when is not supported anymore. Use opt.departure/opt.arrival.')
|
|
|
|
|
}
|
|
|
|
|
let when = new Date(), outFrwd = true
|
|
|
|
|
if (opt.departure !== undefined && opt.departure !== null) {
|
|
|
|
|
when = new Date(opt.departure)
|
2019-02-13 17:55:57 +01:00
|
|
|
|
if (Number.isNaN(+when)) throw new TypeError('opt.departure is invalid')
|
2018-05-28 20:34:24 +02:00
|
|
|
|
} else if (opt.arrival !== undefined && opt.arrival !== null) {
|
2019-12-29 22:16:45 +01:00
|
|
|
|
if (!profile.journeysOutFrwd) {
|
|
|
|
|
throw new Error('opt.arrival is unsupported')
|
|
|
|
|
}
|
2018-05-28 20:34:24 +02:00
|
|
|
|
when = new Date(opt.arrival)
|
2019-02-13 17:55:57 +01:00
|
|
|
|
if (Number.isNaN(+when)) throw new TypeError('opt.arrival is invalid')
|
2018-05-28 20:34:24 +02:00
|
|
|
|
outFrwd = false
|
|
|
|
|
}
|
2017-12-18 20:01:12 +01:00
|
|
|
|
|
|
|
|
|
const filters = [
|
2019-10-31 19:46:30 +01:00
|
|
|
|
profile.formatProductsFilter({profile}, opt.products || {})
|
2017-12-18 20:01:12 +01:00
|
|
|
|
]
|
|
|
|
|
if (
|
|
|
|
|
opt.accessibility &&
|
|
|
|
|
profile.filters &&
|
|
|
|
|
profile.filters.accessibility &&
|
|
|
|
|
profile.filters.accessibility[opt.accessibility]
|
|
|
|
|
) {
|
|
|
|
|
filters.push(profile.filters.accessibility[opt.accessibility])
|
|
|
|
|
}
|
2017-11-12 20:02:32 +01:00
|
|
|
|
|
2018-10-25 16:11:19 +02:00
|
|
|
|
if (!['slow','normal','fast'].includes(opt.walkingSpeed)) {
|
|
|
|
|
throw new Error('opt.walkingSpeed must be one of these values: "slow", "normal", "fast".')
|
|
|
|
|
}
|
2018-10-30 00:47:40 +01:00
|
|
|
|
const gisFltrL = []
|
|
|
|
|
if (profile.journeysWalkingSpeed) {
|
|
|
|
|
gisFltrL.push({
|
|
|
|
|
meta: 'foot_speed_' + opt.walkingSpeed,
|
|
|
|
|
mode: 'FB',
|
|
|
|
|
type: 'M'
|
|
|
|
|
})
|
|
|
|
|
}
|
2018-10-25 16:11:19 +02:00
|
|
|
|
|
2020-07-19 22:58:12 +02:00
|
|
|
|
const query = {
|
|
|
|
|
getPasslist: !!opt.stopovers,
|
|
|
|
|
maxChg: opt.transfers,
|
|
|
|
|
minChgTime: opt.transferTime,
|
|
|
|
|
depLocL: [from],
|
2021-10-26 00:10:54 +02:00
|
|
|
|
viaLocL: opt.via ? [{loc: opt.via}] : [],
|
2020-07-19 22:58:12 +02:00
|
|
|
|
arrLocL: [to],
|
|
|
|
|
jnyFltrL: filters,
|
|
|
|
|
gisFltrL,
|
|
|
|
|
getTariff: !!opt.tickets,
|
|
|
|
|
// todo: this is actually "take additional stations nearby the given start and destination station into account"
|
|
|
|
|
// see rest.exe docs
|
|
|
|
|
ushrp: !!opt.startWithWalking,
|
|
|
|
|
|
|
|
|
|
getPT: true, // todo: what is this?
|
|
|
|
|
getIV: false, // todo: walk & bike as alternatives?
|
|
|
|
|
getPolyline: !!opt.polylines
|
|
|
|
|
// todo: `getConGroups: false` what is this?
|
|
|
|
|
// todo: what is getEco, fwrd?
|
|
|
|
|
}
|
|
|
|
|
if (journeysRef) query.ctxScr = journeysRef
|
|
|
|
|
else {
|
|
|
|
|
query.outDate = profile.formatDate(profile, when)
|
|
|
|
|
query.outTime = profile.formatTime(profile, when)
|
|
|
|
|
}
|
2020-07-20 16:17:31 +02:00
|
|
|
|
if (opt.results !== null) query.numF = opt.results
|
2020-07-19 22:58:12 +02:00
|
|
|
|
if (profile.journeysOutFrwd) query.outFrwd = outFrwd
|
|
|
|
|
|
2021-12-29 18:53:50 +01:00
|
|
|
|
const {res, common} = await profile.request({profile, opt}, userAgent, {
|
2020-07-19 22:58:12 +02:00
|
|
|
|
cfg: {polyEnc: 'GPA'},
|
|
|
|
|
meth: 'TripSearch',
|
|
|
|
|
req: profile.transformJourneysQuery({profile, opt}, query)
|
|
|
|
|
})
|
2021-12-29 18:53:50 +01:00
|
|
|
|
if (!Array.isArray(res.outConL)) return []
|
|
|
|
|
// todo: outConGrpL
|
2018-03-05 00:23:17 +01:00
|
|
|
|
|
2021-12-29 18:53:50 +01:00
|
|
|
|
const ctx = {profile, opt, common, res}
|
|
|
|
|
const journeys = res.outConL
|
|
|
|
|
.map(j => profile.parseJourney(ctx, j))
|
2020-07-20 16:17:31 +02:00
|
|
|
|
|
2021-12-29 18:53:50 +01:00
|
|
|
|
return {
|
|
|
|
|
earlierRef: res.outCtxScrB,
|
|
|
|
|
laterRef: res.outCtxScrF,
|
|
|
|
|
journeys,
|
2021-12-29 21:14:50 +01:00
|
|
|
|
realtimeDataUpdatedAt: res.planrtTS && res.planrtTS !== '0'
|
|
|
|
|
? parseInt(res.planrtTS)
|
|
|
|
|
: null,
|
2021-12-29 18:53:50 +01:00
|
|
|
|
}
|
2017-11-12 20:02:32 +01:00
|
|
|
|
}
|
|
|
|
|
|
2021-12-29 18:53:50 +01:00
|
|
|
|
const refreshJourney = async (refreshToken, opt = {}) => {
|
2018-07-24 17:37:36 +02:00
|
|
|
|
if ('string' !== typeof refreshToken || !refreshToken) {
|
2020-04-13 17:06:50 +02:00
|
|
|
|
throw new TypeError('refreshToken must be a non-empty string.')
|
2018-07-24 17:37:36 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
opt = Object.assign({
|
|
|
|
|
stopovers: false, // return stations on the way?
|
|
|
|
|
tickets: false, // return tickets?
|
2021-01-26 22:10:44 +01:00
|
|
|
|
polylines: false, // return leg shapes? (not supported by all endpoints)
|
2020-03-18 20:04:39 +01:00
|
|
|
|
subStops: true, // parse & expose sub-stops of stations?
|
|
|
|
|
entrances: true, // parse & expose entrances of stops/stations?
|
2022-10-24 17:30:05 +02:00
|
|
|
|
remarks: true, // parse & expose hints & warnings?
|
|
|
|
|
scheduledDays: false, // parse & expose dates the journey is valid on?
|
2018-07-24 17:37:36 +02:00
|
|
|
|
}, opt)
|
|
|
|
|
|
2020-02-07 02:31:51 +01:00
|
|
|
|
const req = profile.formatRefreshJourneyReq({profile, opt}, refreshToken)
|
|
|
|
|
|
2021-12-29 18:53:50 +01:00
|
|
|
|
const {res, common} = await profile.request({profile, opt}, userAgent, req)
|
|
|
|
|
if (!Array.isArray(res.outConL) || !res.outConL[0]) {
|
2022-05-03 23:21:44 +02:00
|
|
|
|
throw new HafasError('invalid response, expected outConL[0]', null, {})
|
2021-12-29 18:53:50 +01:00
|
|
|
|
}
|
2018-07-24 17:37:36 +02:00
|
|
|
|
|
2021-12-29 18:53:50 +01:00
|
|
|
|
const ctx = {profile, opt, common, res}
|
|
|
|
|
|
|
|
|
|
return {
|
2022-04-28 22:59:19 +02:00
|
|
|
|
journey: profile.parseJourney(ctx, res.outConL[0]),
|
2021-12-29 21:14:50 +01:00
|
|
|
|
realtimeDataUpdatedAt: res.planrtTS && res.planrtTS !== '0'
|
|
|
|
|
? parseInt(res.planrtTS)
|
|
|
|
|
: null,
|
2021-12-29 18:53:50 +01:00
|
|
|
|
}
|
2017-11-12 20:02:32 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-10-02 16:36:37 +02:00
|
|
|
|
// Although the DB Navigator app passes the *first* stopover of the trip
|
|
|
|
|
// (instead of the previous one), it seems to work with the previous one as well.
|
|
|
|
|
const journeysFromTrip = async (fromTripId, previousStopover, to, opt = {}) => {
|
|
|
|
|
if (!isNonEmptyString(fromTripId)) {
|
|
|
|
|
throw new Error('fromTripId must be a non-empty string.')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ('string' === typeof to) {
|
|
|
|
|
to = profile.formatStation(to)
|
|
|
|
|
} else if (isObj(to) && (to.type === 'station' || to.type === 'stop')) {
|
|
|
|
|
to = profile.formatStation(to.id)
|
|
|
|
|
} else throw new Error('to must be a valid stop or station.')
|
|
|
|
|
|
|
|
|
|
if (!isObj(previousStopover)) throw new Error('previousStopover must be an object.')
|
|
|
|
|
|
|
|
|
|
let prevStop = previousStopover.stop
|
|
|
|
|
if (isObj(prevStop)) {
|
|
|
|
|
prevStop = profile.formatStation(prevStop.id)
|
|
|
|
|
} else if ('string' === typeof prevStop) {
|
|
|
|
|
prevStop = profile.formatStation(prevStop)
|
|
|
|
|
} else throw new Error('previousStopover.stop must be a valid stop or station.')
|
|
|
|
|
|
|
|
|
|
let depAtPrevStop = previousStopover.departure || previousStopover.plannedDeparture
|
|
|
|
|
if (!isNonEmptyString(depAtPrevStop)) {
|
|
|
|
|
throw new Error('previousStopover.(planned)departure must be a string')
|
|
|
|
|
}
|
|
|
|
|
depAtPrevStop = Date.parse(depAtPrevStop)
|
|
|
|
|
if (Number.isNaN(depAtPrevStop)) {
|
|
|
|
|
throw new Error('previousStopover.(planned)departure is invalid')
|
|
|
|
|
}
|
|
|
|
|
if (depAtPrevStop > Date.now()) {
|
|
|
|
|
throw new Error('previousStopover.(planned)departure must be in the past')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
opt = Object.assign({
|
|
|
|
|
stopovers: false, // return stations on the way?
|
|
|
|
|
transferTime: 0, // minimum time for a single transfer in minutes
|
|
|
|
|
accessibility: 'none', // 'none', 'partial' or 'complete'
|
|
|
|
|
tickets: false, // return tickets?
|
|
|
|
|
polylines: false, // return leg shapes?
|
|
|
|
|
subStops: true, // parse & expose sub-stops of stations?
|
|
|
|
|
entrances: true, // parse & expose entrances of stops/stations?
|
|
|
|
|
remarks: true, // parse & expose hints & warnings?
|
|
|
|
|
}, opt)
|
|
|
|
|
|
|
|
|
|
// make clear that `departure`/`arrival`/`when` are not supported
|
|
|
|
|
if (opt.departure) throw new Error('journeysFromTrip + opt.departure is not supported by HAFAS.')
|
|
|
|
|
if (opt.arrival) throw new Error('journeysFromTrip + opt.arrival is not supported by HAFAS.')
|
|
|
|
|
if (opt.when) throw new Error('journeysFromTrip + opt.when is not supported by HAFAS.')
|
|
|
|
|
|
|
|
|
|
const filters = [
|
|
|
|
|
profile.formatProductsFilter({profile}, opt.products || {})
|
|
|
|
|
]
|
|
|
|
|
if (
|
|
|
|
|
opt.accessibility &&
|
|
|
|
|
profile.filters &&
|
|
|
|
|
profile.filters.accessibility &&
|
|
|
|
|
profile.filters.accessibility[opt.accessibility]
|
|
|
|
|
) {
|
|
|
|
|
filters.push(profile.filters.accessibility[opt.accessibility])
|
|
|
|
|
}
|
|
|
|
|
// todo: support walking speed filter
|
|
|
|
|
|
|
|
|
|
// todo: are these supported?
|
|
|
|
|
// - getPT
|
|
|
|
|
// - getIV
|
|
|
|
|
// - trfReq
|
|
|
|
|
// features from `journeys()` not supported here:
|
|
|
|
|
// - `maxChg`: maximum nr of transfers
|
|
|
|
|
// - `bike`: only bike-friendly journeys
|
|
|
|
|
// - `numF`: how many journeys?
|
|
|
|
|
// - `via`: let journeys pass this station
|
|
|
|
|
// todo: find a way to support them
|
|
|
|
|
|
|
|
|
|
const query = {
|
|
|
|
|
// https://github.com/marudor/BahnhofsAbfahrten/blob/49ebf8b36576547112e61a6273bee770f0769660/packages/types/HAFAS/SearchOnTrip.ts#L16-L30
|
2021-10-26 14:15:17 +02:00
|
|
|
|
// todo: support search by `journey.refreshToken` (a.k.a. `ctxRecon`) via `sotMode: RC`?
|
2018-10-02 16:36:37 +02:00
|
|
|
|
sotMode: 'JI', // seach by trip ID (a.k.a. "JID")
|
|
|
|
|
jid: fromTripId,
|
|
|
|
|
locData: { // when & where the trip has been entered
|
|
|
|
|
loc: prevStop,
|
|
|
|
|
type: 'DEP', // todo: are there other values?
|
|
|
|
|
date: profile.formatDate(profile, depAtPrevStop),
|
|
|
|
|
time: profile.formatTime(profile, depAtPrevStop)
|
|
|
|
|
},
|
|
|
|
|
arrLocL: [to],
|
|
|
|
|
jnyFltrL: filters,
|
|
|
|
|
getPasslist: !!opt.stopovers,
|
|
|
|
|
getPolyline: !!opt.polylines,
|
|
|
|
|
minChgTime: opt.transferTime,
|
|
|
|
|
getTariff: !!opt.tickets,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const {res, common} = await profile.request({profile, opt}, userAgent, {
|
|
|
|
|
cfg: {polyEnc: 'GPA'},
|
|
|
|
|
meth: 'SearchOnTrip',
|
|
|
|
|
req: query,
|
|
|
|
|
})
|
|
|
|
|
if (!Array.isArray(res.outConL)) return []
|
|
|
|
|
|
|
|
|
|
const ctx = {profile, opt, common, res}
|
2021-12-29 21:14:50 +01:00
|
|
|
|
const journeys = res.outConL
|
2018-10-02 16:36:37 +02:00
|
|
|
|
.map(rawJourney => profile.parseJourney(ctx, rawJourney))
|
|
|
|
|
.map((journey) => {
|
|
|
|
|
// For the first (transit) leg, HAFAS sometimes returns *all* past
|
|
|
|
|
// stopovers of the trip, even though it should only return stopovers
|
|
|
|
|
// between the specified `depAtPrevStop` and the arrival at the
|
|
|
|
|
// interchange station. We slice the leg accordingly.
|
|
|
|
|
const fromLegI = journey.legs.findIndex(l => l.tripId === fromTripId)
|
|
|
|
|
if (fromLegI < 0) return journey
|
|
|
|
|
const fromLeg = journey.legs[fromLegI]
|
|
|
|
|
return {
|
|
|
|
|
...journey,
|
|
|
|
|
legs: [
|
|
|
|
|
...journey.legs.slice(0, fromLegI),
|
|
|
|
|
sliceLeg(fromLeg, previousStopover.stop, fromLeg.destination),
|
|
|
|
|
...journey.legs.slice(fromLegI + 2),
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
})
|
2021-12-29 21:14:50 +01:00
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
journeys,
|
|
|
|
|
realtimeDataUpdatedAt: res.planrtTS && res.planrtTS !== '0'
|
|
|
|
|
? parseInt(res.planrtTS)
|
|
|
|
|
: null,
|
|
|
|
|
}
|
2018-10-02 16:36:37 +02:00
|
|
|
|
}
|
|
|
|
|
|
2021-12-29 18:53:50 +01:00
|
|
|
|
const locations = async (query, opt = {}) => {
|
2018-03-04 19:53:53 +01:00
|
|
|
|
if (!isNonEmptyString(query)) {
|
2019-02-13 17:55:57 +01:00
|
|
|
|
throw new TypeError('query must be a non-empty string.')
|
2018-02-28 16:09:23 +01:00
|
|
|
|
}
|
2017-11-12 20:19:33 +01:00
|
|
|
|
opt = Object.assign({
|
|
|
|
|
fuzzy: true, // find only exact matches?
|
2018-12-07 17:00:24 +01:00
|
|
|
|
results: 5, // how many search results?
|
2019-01-23 12:58:05 +08:00
|
|
|
|
stops: true, // return stops/stations?
|
2017-11-12 20:19:33 +01:00
|
|
|
|
addresses: true,
|
2018-06-28 13:00:33 +02:00
|
|
|
|
poi: true, // points of interest
|
2020-03-18 20:04:39 +01:00
|
|
|
|
subStops: true, // parse & expose sub-stops of stations?
|
|
|
|
|
entrances: true, // parse & expose entrances of stops/stations?
|
2019-01-23 13:03:01 +08:00
|
|
|
|
linesOfStops: false // parse & expose lines at each stop/station?
|
2017-11-12 20:19:33 +01:00
|
|
|
|
}, opt)
|
|
|
|
|
|
2019-10-31 20:08:56 +01:00
|
|
|
|
const req = profile.formatLocationsReq({profile, opt}, query)
|
|
|
|
|
|
2021-12-29 18:53:50 +01:00
|
|
|
|
const {res, common} = await profile.request({profile, opt}, userAgent, req)
|
|
|
|
|
if (!res.match || !Array.isArray(res.match.locL)) return []
|
2019-10-20 01:47:09 +02:00
|
|
|
|
|
2021-12-29 18:53:50 +01:00
|
|
|
|
const ctx = {profile, opt, common, res}
|
|
|
|
|
return res.match.locL.map(loc => profile.parseLocation(ctx, loc))
|
2017-11-12 20:19:33 +01:00
|
|
|
|
}
|
|
|
|
|
|
2021-12-29 18:53:50 +01:00
|
|
|
|
const stop = async (stop, opt = {}) => {
|
2018-11-21 19:54:40 +01:00
|
|
|
|
if ('object' === typeof stop) stop = profile.formatStation(stop.id)
|
|
|
|
|
else if ('string' === typeof stop) stop = profile.formatStation(stop)
|
2019-02-13 17:55:57 +01:00
|
|
|
|
else throw new TypeError('stop must be an object or a string.')
|
2018-01-26 17:08:07 +01:00
|
|
|
|
|
2018-06-28 13:00:33 +02:00
|
|
|
|
opt = Object.assign({
|
2020-05-21 17:31:51 +02:00
|
|
|
|
linesOfStops: false, // parse & expose lines at the stop/station?
|
2020-03-18 20:04:39 +01:00
|
|
|
|
subStops: true, // parse & expose sub-stops of stations?
|
|
|
|
|
entrances: true, // parse & expose entrances of stops/stations?
|
2020-05-21 17:31:51 +02:00
|
|
|
|
remarks: true, // parse & expose hints & warnings?
|
2018-06-28 13:00:33 +02:00
|
|
|
|
}, opt)
|
2019-10-31 20:08:56 +01:00
|
|
|
|
|
|
|
|
|
const req = profile.formatStopReq({profile, opt}, stop)
|
|
|
|
|
|
2021-12-29 18:53:50 +01:00
|
|
|
|
const {res, common} = await profile.request({profile, opt}, userAgent, req)
|
|
|
|
|
if (!res || !Array.isArray(res.locL) || !res.locL[0]) {
|
2022-05-03 23:21:44 +02:00
|
|
|
|
throw new HafasError('invalid response, expected locL[0]', null, {
|
|
|
|
|
// This problem occurs on invalid input. 🙄
|
|
|
|
|
code: INVALID_REQUEST,
|
|
|
|
|
})
|
2021-12-29 18:53:50 +01:00
|
|
|
|
}
|
2019-10-20 01:47:09 +02:00
|
|
|
|
|
2021-12-29 18:53:50 +01:00
|
|
|
|
const ctx = {profile, opt, res, common}
|
|
|
|
|
return profile.parseLocation(ctx, res.locL[0])
|
2018-01-26 17:08:07 +01:00
|
|
|
|
}
|
|
|
|
|
|
2021-12-29 18:53:50 +01:00
|
|
|
|
const nearby = async (location, opt = {}) => {
|
2018-08-25 01:52:46 +02:00
|
|
|
|
validateLocation(location, 'location')
|
2018-01-05 14:53:03 +01:00
|
|
|
|
|
2017-11-12 20:29:57 +01:00
|
|
|
|
opt = Object.assign({
|
|
|
|
|
results: 8, // maximum number of results
|
|
|
|
|
distance: null, // maximum walking distance in meters
|
|
|
|
|
poi: false, // return points of interest?
|
2019-01-23 12:58:05 +08:00
|
|
|
|
stops: true, // return stops/stations?
|
2020-03-18 20:04:39 +01:00
|
|
|
|
subStops: true, // parse & expose sub-stops of stations?
|
|
|
|
|
entrances: true, // parse & expose entrances of stops/stations?
|
2019-01-23 13:03:01 +08:00
|
|
|
|
linesOfStops: false // parse & expose lines at each stop/station?
|
2017-11-12 20:29:57 +01:00
|
|
|
|
}, opt)
|
|
|
|
|
|
2019-10-31 20:08:56 +01:00
|
|
|
|
const req = profile.formatNearbyReq({profile, opt}, location)
|
|
|
|
|
|
2021-12-29 18:53:50 +01:00
|
|
|
|
const {res, common} = await profile.request({profile, opt}, userAgent, req)
|
|
|
|
|
if (!Array.isArray(res.locL)) return []
|
2019-10-20 01:47:09 +02:00
|
|
|
|
|
2021-12-29 18:53:50 +01:00
|
|
|
|
// todo: parse `.dur` – walking duration?
|
|
|
|
|
const ctx = {profile, opt, common, res}
|
|
|
|
|
const results = res.locL.map(loc => profile.parseNearby(ctx, loc))
|
|
|
|
|
return Number.isInteger(opt.results)
|
|
|
|
|
? results.slice(0, opt.results)
|
|
|
|
|
: results
|
2017-11-12 20:29:57 +01:00
|
|
|
|
}
|
|
|
|
|
|
2021-12-29 21:33:42 +01:00
|
|
|
|
const trip = async (id, opt = {}) => {
|
2018-06-29 14:58:43 +02:00
|
|
|
|
if (!isNonEmptyString(id)) {
|
2019-02-13 17:55:57 +01:00
|
|
|
|
throw new TypeError('id must be a non-empty string.')
|
2018-02-28 16:09:23 +01:00
|
|
|
|
}
|
2017-12-17 20:33:04 +01:00
|
|
|
|
opt = Object.assign({
|
2018-06-13 20:39:33 +02:00
|
|
|
|
stopovers: true, // return stations on the way?
|
2018-07-16 11:35:47 +02:00
|
|
|
|
polyline: false, // return a track shape?
|
2020-03-18 20:04:39 +01:00
|
|
|
|
subStops: true, // parse & expose sub-stops of stations?
|
|
|
|
|
entrances: true, // parse & expose entrances of stops/stations?
|
2022-11-16 15:18:12 +01:00
|
|
|
|
remarks: true, // parse & expose hints & warnings?
|
|
|
|
|
scheduledDays: false, // parse & expose dates trip is valid on?
|
2017-12-17 20:33:04 +01:00
|
|
|
|
}, opt)
|
2017-11-20 15:43:13 +01:00
|
|
|
|
|
2021-12-29 21:33:42 +01:00
|
|
|
|
const req = profile.formatTripReq({profile, opt}, id)
|
2019-10-31 20:08:56 +01:00
|
|
|
|
|
2021-12-29 18:53:50 +01:00
|
|
|
|
const {res, common} = await profile.request({profile, opt}, userAgent, req)
|
|
|
|
|
const ctx = {profile, opt, common, res}
|
|
|
|
|
|
2021-12-29 21:23:28 +01:00
|
|
|
|
const trip = profile.parseTrip(ctx, res.journey)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
trip,
|
|
|
|
|
realtimeDataUpdatedAt: res.planrtTS && res.planrtTS !== '0'
|
|
|
|
|
? parseInt(res.planrtTS)
|
|
|
|
|
: null,
|
|
|
|
|
}
|
2017-11-20 15:43:13 +01:00
|
|
|
|
}
|
|
|
|
|
|
2021-10-21 23:00:44 +02:00
|
|
|
|
// todo [breaking]: rename to trips()?
|
2021-12-29 18:53:50 +01:00
|
|
|
|
const tripsByName = async (lineNameOrFahrtNr = '*', opt = {}) => {
|
2019-12-17 14:12:35 +01:00
|
|
|
|
if (!isNonEmptyString(lineNameOrFahrtNr)) {
|
|
|
|
|
throw new TypeError('lineNameOrFahrtNr must be a non-empty string.')
|
|
|
|
|
}
|
|
|
|
|
opt = Object.assign({
|
2021-10-21 23:00:44 +02:00
|
|
|
|
when: null,
|
|
|
|
|
fromWhen: null, untilWhen: null,
|
|
|
|
|
onlyCurrentlyRunning: true,
|
2021-10-22 19:27:29 +02:00
|
|
|
|
products: {},
|
|
|
|
|
currentlyStoppingAt: null,
|
|
|
|
|
lineName: null,
|
|
|
|
|
operatorNames: null,
|
|
|
|
|
additionalFilters: [], // undocumented
|
2019-12-17 14:12:35 +01:00
|
|
|
|
}, opt)
|
2021-10-21 23:00:44 +02:00
|
|
|
|
|
|
|
|
|
const req = {
|
|
|
|
|
// fields: https://github.com/marudor/BahnhofsAbfahrten/blob/f619e754f212980261eb7e2b151cd73ba0213da8/packages/types/HAFAS/JourneyMatch.ts#L4-L23
|
|
|
|
|
input: lineNameOrFahrtNr,
|
|
|
|
|
onlyCR: opt.onlyCurrentlyRunning,
|
2021-10-22 19:27:29 +02:00
|
|
|
|
jnyFltrL: [
|
|
|
|
|
profile.formatProductsFilter({profile}, opt.products),
|
|
|
|
|
],
|
2021-10-21 23:00:44 +02:00
|
|
|
|
// todo: passing `tripId` yields a `CGI_READ_FAILED` error
|
|
|
|
|
// todo: passing a stop ID as `extId` yields a `PARAMETER` error
|
|
|
|
|
// todo: `onlyRT: true` reduces the number of results, but filters recent trips 🤔
|
|
|
|
|
// todo: `onlyTN: true` yields a `NO_MATCH` error
|
2021-10-22 19:27:29 +02:00
|
|
|
|
// todo: useAeqi
|
2021-10-21 23:00:44 +02:00
|
|
|
|
}
|
|
|
|
|
if (opt.when !== null) {
|
|
|
|
|
req.date = profile.formatDate(profile, new Date(opt.when))
|
|
|
|
|
req.time = profile.formatTime(profile, new Date(opt.when))
|
|
|
|
|
}
|
|
|
|
|
// todo: fromWhen doesn't work yet, but untilWhen does
|
|
|
|
|
if (opt.fromWhen !== null) {
|
|
|
|
|
req.dateB = profile.formatDate(profile, new Date(opt.fromWhen))
|
|
|
|
|
req.timeB = profile.formatTime(profile, new Date(opt.fromWhen))
|
|
|
|
|
}
|
|
|
|
|
if (opt.untilWhen !== null) {
|
|
|
|
|
req.dateE = profile.formatDate(profile, new Date(opt.untilWhen))
|
|
|
|
|
req.timeE = profile.formatTime(profile, new Date(opt.untilWhen))
|
|
|
|
|
}
|
2021-10-22 19:27:29 +02:00
|
|
|
|
const filter = (mode, type, value) => ({mode, type, value})
|
|
|
|
|
if (opt.currentlyStoppingAt !== null) {
|
|
|
|
|
if (!isNonEmptyString(opt.currentlyStoppingAt)) {
|
|
|
|
|
throw new TypeError('opt.currentlyStoppingAt must be a non-empty string.')
|
|
|
|
|
}
|
|
|
|
|
req.jnyFltrL.push(filter('INC', 'STATIONS', opt.currentlyStoppingAt))
|
|
|
|
|
}
|
|
|
|
|
if (opt.lineName !== null) {
|
|
|
|
|
if (!isNonEmptyString(opt.lineName)) {
|
|
|
|
|
throw new TypeError('opt.lineName must be a non-empty string.')
|
|
|
|
|
}
|
|
|
|
|
// todo: does this target `line` or `lineId`?
|
|
|
|
|
req.jnyFltrL.push(filter('INC', 'LINE', opt.lineName))
|
|
|
|
|
}
|
|
|
|
|
if (opt.operatorNames !== null) {
|
|
|
|
|
if (
|
|
|
|
|
!Array.isArray(opt.operatorNames)
|
|
|
|
|
|| opt.operatorNames.length === 0
|
|
|
|
|
|| !opt.operatorNames.every(isNonEmptyString)
|
|
|
|
|
) {
|
|
|
|
|
throw new TypeError('opt.operatorNames must be an array of non-empty strings.')
|
|
|
|
|
}
|
|
|
|
|
// todo: is the an escaping mechanism for ","
|
|
|
|
|
req.jnyFltrL.push(filter('INC', 'OP', opt.operatorNames.join(',')))
|
|
|
|
|
}
|
|
|
|
|
req.jnyFltrL = [...req.jnyFltrL, ...opt.additionalFilters]
|
2019-12-17 14:12:35 +01:00
|
|
|
|
|
2021-12-29 18:53:50 +01:00
|
|
|
|
const {res, common} = await profile.request({profile, opt}, userAgent, {
|
2019-12-17 14:12:35 +01:00
|
|
|
|
cfg: {polyEnc: 'GPA'},
|
|
|
|
|
meth: 'JourneyMatch',
|
2021-10-21 23:00:44 +02:00
|
|
|
|
req,
|
2019-12-17 14:12:35 +01:00
|
|
|
|
})
|
2021-10-26 14:15:17 +02:00
|
|
|
|
// todo [breaking]: catch `NO_MATCH` errors, return []
|
2021-12-29 18:53:50 +01:00
|
|
|
|
const ctx = {profile, opt, common, res}
|
|
|
|
|
|
2021-12-29 21:23:28 +01:00
|
|
|
|
const trips = res.jnyL.map(t => profile.parseTrip(ctx, t))
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
trips,
|
|
|
|
|
realtimeDataUpdatedAt: res.planrtTS && res.planrtTS !== '0'
|
|
|
|
|
? parseInt(res.planrtTS)
|
|
|
|
|
: null,
|
|
|
|
|
}
|
2019-12-17 14:12:35 +01:00
|
|
|
|
}
|
|
|
|
|
|
2021-12-29 18:53:50 +01:00
|
|
|
|
const radar = async ({north, west, south, east}, opt) => {
|
2019-02-13 17:55:57 +01:00
|
|
|
|
if ('number' !== typeof north) throw new TypeError('north must be a number.')
|
|
|
|
|
if ('number' !== typeof west) throw new TypeError('west must be a number.')
|
|
|
|
|
if ('number' !== typeof south) throw new TypeError('south must be a number.')
|
|
|
|
|
if ('number' !== typeof east) throw new TypeError('east must be a number.')
|
2018-05-16 21:53:33 +02:00
|
|
|
|
if (north <= south) throw new Error('north must be larger than south.')
|
|
|
|
|
if (east <= west) throw new Error('east must be larger than west.')
|
2017-11-20 17:37:08 +01:00
|
|
|
|
|
|
|
|
|
opt = Object.assign({
|
|
|
|
|
results: 256, // maximum number of vehicles
|
|
|
|
|
duration: 30, // compute frames for the next n seconds
|
2018-06-13 20:25:56 +02:00
|
|
|
|
// todo: what happens with `frames: 0`?
|
2017-12-12 03:28:54 +01:00
|
|
|
|
frames: 3, // nr of frames to compute
|
2018-05-15 19:39:28 +02:00
|
|
|
|
products: null, // optionally an object of booleans
|
2020-03-18 20:04:39 +01:00
|
|
|
|
polylines: true, // return a track shape for each vehicle?
|
|
|
|
|
subStops: true, // parse & expose sub-stops of stations?
|
|
|
|
|
entrances: true, // parse & expose entrances of stops/stations?
|
2017-11-20 17:37:08 +01:00
|
|
|
|
}, opt || {})
|
2018-05-21 17:10:42 +02:00
|
|
|
|
opt.when = new Date(opt.when || Date.now())
|
2019-02-13 17:55:57 +01:00
|
|
|
|
if (Number.isNaN(+opt.when)) throw new TypeError('opt.when is invalid')
|
2017-11-20 17:37:08 +01:00
|
|
|
|
|
2019-10-31 20:08:56 +01:00
|
|
|
|
const req = profile.formatRadarReq({profile, opt}, north, west, south, east)
|
|
|
|
|
|
2021-12-29 18:53:50 +01:00
|
|
|
|
const {res, common} = await profile.request({profile, opt}, userAgent, req)
|
|
|
|
|
if (!Array.isArray(res.jnyL)) return []
|
|
|
|
|
const ctx = {profile, opt, common, res}
|
2017-11-20 17:37:08 +01:00
|
|
|
|
|
2021-12-29 21:24:07 +01:00
|
|
|
|
const movements = res.jnyL.map(m => profile.parseMovement(ctx, m))
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
movements,
|
|
|
|
|
realtimeDataUpdatedAt: res.planrtTS && res.planrtTS !== '0'
|
|
|
|
|
? parseInt(res.planrtTS)
|
|
|
|
|
: null,
|
|
|
|
|
}
|
2017-11-20 17:37:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
2021-12-29 18:51:34 +01:00
|
|
|
|
const reachableFrom = async (address, opt = {}) => {
|
2018-08-27 12:06:14 +02:00
|
|
|
|
validateLocation(address, 'address')
|
2018-08-25 01:52:46 +02:00
|
|
|
|
|
|
|
|
|
opt = Object.assign({
|
|
|
|
|
when: Date.now(),
|
|
|
|
|
maxTransfers: 5, // maximum of 5 transfers
|
2018-11-01 19:38:29 +01:00
|
|
|
|
maxDuration: 20, // maximum travel duration in minutes, pass `null` for infinite
|
2020-03-18 20:04:39 +01:00
|
|
|
|
products: {},
|
|
|
|
|
subStops: true, // parse & expose sub-stops of stations?
|
|
|
|
|
entrances: true, // parse & expose entrances of stops/stations?
|
2021-01-26 22:10:44 +01:00
|
|
|
|
polylines: false, // return leg shapes?
|
2018-08-25 01:52:46 +02:00
|
|
|
|
}, opt)
|
2019-02-13 17:55:57 +01:00
|
|
|
|
if (Number.isNaN(+opt.when)) throw new TypeError('opt.when is invalid')
|
2018-08-25 01:52:46 +02:00
|
|
|
|
|
2019-10-31 20:08:56 +01:00
|
|
|
|
const req = profile.formatReachableFromReq({profile, opt}, address)
|
|
|
|
|
|
2021-12-29 18:51:34 +01:00
|
|
|
|
const {res, common} = await profile.request({profile, opt}, userAgent, req)
|
|
|
|
|
if (!Array.isArray(res.posL)) {
|
2022-05-03 23:21:44 +02:00
|
|
|
|
throw new HafasError('invalid response, expected posL[0]', null, {
|
|
|
|
|
shouldRetry: true,
|
|
|
|
|
})
|
2021-12-29 18:51:34 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const byDuration = []
|
|
|
|
|
let i = 0, lastDuration = NaN
|
|
|
|
|
for (const pos of sortBy(res.posL, 'dur')) {
|
|
|
|
|
const loc = common.locations[pos.locX]
|
|
|
|
|
if (!loc) continue
|
|
|
|
|
if (pos.dur !== lastDuration) {
|
|
|
|
|
lastDuration = pos.dur
|
|
|
|
|
i = byDuration.length
|
|
|
|
|
byDuration.push({
|
|
|
|
|
duration: pos.dur,
|
|
|
|
|
stations: [loc]
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
byDuration[i].stations.push(loc)
|
|
|
|
|
}
|
2018-09-03 20:38:56 +02:00
|
|
|
|
}
|
|
|
|
|
|
2021-12-29 21:25:14 +01:00
|
|
|
|
return {
|
|
|
|
|
reachable: byDuration,
|
|
|
|
|
realtimeDataUpdatedAt: res.planrtTS && res.planrtTS !== '0'
|
|
|
|
|
? parseInt(res.planrtTS)
|
|
|
|
|
: null,
|
|
|
|
|
}
|
2018-08-25 01:52:46 +02:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-09 20:54:43 +01:00
|
|
|
|
const remarks = async (opt = {}) => {
|
|
|
|
|
opt = {
|
|
|
|
|
results: 100, // maximum number of remarks
|
|
|
|
|
// filter by time
|
|
|
|
|
from: Date.now(),
|
|
|
|
|
to: null,
|
|
|
|
|
products: null, // filter by affected products
|
2021-01-26 22:10:44 +01:00
|
|
|
|
polylines: false, // return leg shapes? (not supported by all endpoints)
|
2020-03-09 20:54:43 +01:00
|
|
|
|
...opt
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (opt.from !== null) {
|
|
|
|
|
opt.from = new Date(opt.from)
|
|
|
|
|
validateWhen(opt.from, 'opt.from')
|
|
|
|
|
}
|
|
|
|
|
if (opt.to !== null) {
|
|
|
|
|
opt.to = new Date(opt.to)
|
|
|
|
|
validateWhen(opt.to, 'opt.to')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const req = profile.formatRemarksReq({profile, opt})
|
|
|
|
|
const {
|
|
|
|
|
res, common,
|
|
|
|
|
} = await profile.request({profile, opt}, userAgent, req)
|
|
|
|
|
|
|
|
|
|
const ctx = {profile, opt, common, res}
|
2021-12-29 21:25:49 +01:00
|
|
|
|
const remarks = (res.msgL || [])
|
2021-01-19 15:02:08 +01:00
|
|
|
|
.map(w => profile.parseWarning(ctx, w))
|
2021-12-29 21:25:49 +01:00
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
remarks,
|
|
|
|
|
realtimeDataUpdatedAt: res.planrtTS && res.planrtTS !== '0'
|
|
|
|
|
? parseInt(res.planrtTS)
|
|
|
|
|
: null,
|
|
|
|
|
}
|
2020-03-09 20:54:43 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-09 21:44:35 +01:00
|
|
|
|
const lines = async (query, opt = {}) => {
|
|
|
|
|
if (!isNonEmptyString(query)) {
|
|
|
|
|
throw new TypeError('query must be a non-empty string.')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const req = profile.formatLinesReq({profile, opt}, query)
|
|
|
|
|
const {
|
|
|
|
|
res, common,
|
|
|
|
|
} = await profile.request({profile, opt}, userAgent, req)
|
|
|
|
|
|
|
|
|
|
if (!Array.isArray(res.lineL)) return []
|
|
|
|
|
const ctx = {profile, opt, common, res}
|
2021-12-29 21:26:12 +01:00
|
|
|
|
const lines = res.lineL.map(l => {
|
2020-03-09 21:44:35 +01:00
|
|
|
|
const parseDirRef = i => (res.common.dirL[i] || {}).txt || null
|
|
|
|
|
return {
|
|
|
|
|
...omit(l.line, ['id', 'fahrtNr']),
|
|
|
|
|
id: l.lineId,
|
|
|
|
|
// todo: what is locX?
|
|
|
|
|
directions: Array.isArray(l.dirRefL)
|
|
|
|
|
? l.dirRefL.map(parseDirRef)
|
|
|
|
|
: null,
|
|
|
|
|
trips: Array.isArray(l.jnyL)
|
|
|
|
|
? l.jnyL.map(t => profile.parseTrip(ctx, t))
|
|
|
|
|
: null,
|
|
|
|
|
}
|
|
|
|
|
})
|
2021-12-29 21:26:12 +01:00
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
lines,
|
|
|
|
|
realtimeDataUpdatedAt: res.planrtTS && res.planrtTS !== '0'
|
|
|
|
|
? parseInt(res.planrtTS)
|
|
|
|
|
: null,
|
|
|
|
|
}
|
2020-03-09 21:44:35 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-09 19:47:29 +01:00
|
|
|
|
const serverInfo = async (opt = {}) => {
|
2022-02-22 20:23:54 +01:00
|
|
|
|
opt = {
|
|
|
|
|
versionInfo: true, // query HAFAS versions?
|
|
|
|
|
...opt
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-09 19:47:29 +01:00
|
|
|
|
const {res, common} = await profile.request({profile, opt}, userAgent, {
|
|
|
|
|
meth: 'ServerInfo',
|
2022-02-22 20:23:54 +01:00
|
|
|
|
req: {
|
|
|
|
|
getVersionInfo: opt.versionInfo,
|
|
|
|
|
},
|
2020-03-09 19:47:29 +01:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const ctx = {profile, opt, common, res}
|
|
|
|
|
return {
|
2022-02-22 20:23:54 +01:00
|
|
|
|
// todo: what are .serverVersion & .clientVersion?
|
|
|
|
|
hciVersion: res.hciVersion || null,
|
2020-03-09 19:47:29 +01:00
|
|
|
|
timetableStart: res.fpB || null,
|
|
|
|
|
timetableEnd: res.fpE || null,
|
|
|
|
|
serverTime: res.sD && res.sT
|
|
|
|
|
? profile.parseDateTime(ctx, res.sD, res.sT)
|
|
|
|
|
: null,
|
2022-06-18 18:19:04 +02:00
|
|
|
|
realtimeDataUpdatedAt: res.planrtTS && res.planrtTS !== '0'
|
2020-03-09 19:47:29 +01:00
|
|
|
|
? parseInt(res.planrtTS)
|
|
|
|
|
: null,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const client = {
|
|
|
|
|
departures,
|
|
|
|
|
arrivals,
|
|
|
|
|
journeys,
|
|
|
|
|
locations,
|
|
|
|
|
stop,
|
|
|
|
|
nearby,
|
|
|
|
|
serverInfo,
|
|
|
|
|
}
|
2018-06-29 15:14:26 +02:00
|
|
|
|
if (profile.trip) client.trip = trip
|
2017-11-27 19:45:33 +01:00
|
|
|
|
if (profile.radar) client.radar = radar
|
2018-07-24 18:28:33 +02:00
|
|
|
|
if (profile.refreshJourney) client.refreshJourney = refreshJourney
|
2018-10-02 16:36:37 +02:00
|
|
|
|
if (profile.journeysFromTrip) client.journeysFromTrip = journeysFromTrip
|
2018-08-25 01:52:46 +02:00
|
|
|
|
if (profile.reachableFrom) client.reachableFrom = reachableFrom
|
2019-12-17 14:12:35 +01:00
|
|
|
|
if (profile.tripsByName) client.tripsByName = tripsByName
|
2020-09-16 17:21:59 +02:00
|
|
|
|
if (profile.remarks !== false) client.remarks = remarks
|
2020-09-16 17:26:59 +02:00
|
|
|
|
if (profile.lines !== false) client.lines = lines
|
2017-12-16 07:54:39 +01:00
|
|
|
|
Object.defineProperty(client, 'profile', {value: profile})
|
2017-11-27 19:45:33 +01:00
|
|
|
|
return client
|
2016-06-22 01:39:59 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-11-12 14:52:04 +01:00
|
|
|
|
module.exports = createClient
|