DB: add journeysFromTrip()

This commit is contained in:
Jannis R 2018-10-02 16:36:37 +02:00
parent b717642293
commit ce82817631
4 changed files with 176 additions and 0 deletions

124
index.js
View file

@ -8,6 +8,7 @@ const omit = require('lodash/omit')
const defaultProfile = require('./lib/default-profile') const defaultProfile = require('./lib/default-profile')
const validateProfile = require('./lib/validate-profile') const validateProfile = require('./lib/validate-profile')
const {INVALID_REQUEST} = require('./lib/errors') const {INVALID_REQUEST} = require('./lib/errors')
const sliceLeg = require('./lib/slice-leg')
const isNonEmptyString = str => 'string' === typeof str && str.length > 0 const isNonEmptyString = str => 'string' === typeof str && str.length > 0
@ -262,6 +263,128 @@ const createClient = (profile, userAgent, opt = {}) => {
}) })
} }
// 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
// todo: support search by `journey.refreshToken` (a.k.a. `ctxRecon`)?
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}
return res.outConL
.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),
],
}
})
}
const locations = (query, opt = {}) => { const locations = (query, opt = {}) => {
if (!isNonEmptyString(query)) { if (!isNonEmptyString(query)) {
throw new TypeError('query must be a non-empty string.') throw new TypeError('query must be a non-empty string.')
@ -566,6 +689,7 @@ const createClient = (profile, userAgent, opt = {}) => {
if (profile.trip) client.trip = trip if (profile.trip) client.trip = trip
if (profile.radar) client.radar = radar if (profile.radar) client.radar = radar
if (profile.refreshJourney) client.refreshJourney = refreshJourney if (profile.refreshJourney) client.refreshJourney = refreshJourney
if (profile.journeysFromTrip) client.journeysFromTrip = journeysFromTrip
if (profile.reachableFrom) client.reachableFrom = reachableFrom if (profile.reachableFrom) client.reachableFrom = reachableFrom
if (profile.tripsByName) client.tripsByName = tripsByName if (profile.tripsByName) client.tripsByName = tripsByName
if (profile.remarks !== false) client.remarks = remarks if (profile.remarks !== false) client.remarks = remarks

47
lib/slice-leg.js Normal file
View file

@ -0,0 +1,47 @@
'use strict'
const findById = (needle) => {
const needleStopId = needle.id
const needleStationId = needle.station ? needle.station.id : null
return (stop) => {
if (needleStopId === stop.id) return true
const stationId = stop.station ? stop.station.id : null
if (needleStationId && stationId && needleStationId === stationId) return true
// todo: `needleStationId === stop.id`? `needleStopId === stationId`?
return false
}
}
const sliceLeg = (leg, from, to) => {
if (!Array.isArray(leg.stopovers)) throw new Error('leg.stopovers must be an array.')
const stops = leg.stopovers.map(st => st.stop)
const fromI = stops.findIndex(findById(from))
if (fromI === -1) throw new Error('from not found in stopovers')
const fromStopover = leg.stopovers[fromI]
const toI = stops.findIndex(findById(to))
if (toI === -1) throw new Error('to not found in stopovers')
const toStopover = leg.stopovers[toI]
if (fromI === 0 && toI === leg.stopovers.length - 1) return leg
const newLeg = Object.assign({}, leg)
newLeg.stopovers = leg.stopovers.slice(fromI, toI + 1)
newLeg.origin = fromStopover.stop
newLeg.departure = fromStopover.departure
newLeg.departureDelay = fromStopover.departureDelay
newLeg.scheduledDeparture = fromStopover.scheduledDeparture
newLeg.departurePlatform = fromStopover.departurePlatform
newLeg.destination = toStopover.stop
newLeg.arrival = toStopover.arrival
newLeg.arrivalDelay = toStopover.arrivalDelay
newLeg.scheduledArrival = toStopover.scheduledArrival
newLeg.arrivalPlatform = toStopover.arrivalPlatform
return newLeg
}
module.exports = sliceLeg

View file

@ -478,6 +478,7 @@ const dbProfile = {
departuresStbFltrEquiv: false, departuresStbFltrEquiv: false,
refreshJourneyUseOutReconL: true, refreshJourneyUseOutReconL: true,
trip: true, trip: true,
journeysFromTrip: true,
radar: true, radar: true,
reachableFrom: true, reachableFrom: true,
lines: false, // `.svcResL[0].res.lineL[]` is missing 🤔 lines: false, // `.svcResL[0].res.lineL[]` is missing 🤔

View file

@ -26,6 +26,10 @@ const parseArgs = [
['journeys', 2, parseJsObject], ['journeys', 2, parseJsObject],
['refreshJourney', 0, toString], ['refreshJourney', 0, toString],
['refreshJourney', 1, parseJsObject], ['refreshJourney', 1, parseJsObject],
['journeysFromTrip', 0, toString],
['journeysFromTrip', 1, parseJsObject],
['journeysFromTrip', 2, toString],
['journeysFromTrip', 3, parseJsObject],
['locations', 0, toString], ['locations', 0, toString],
['locations', 1, parseJsObject], ['locations', 1, parseJsObject],
['stop', 0, toString], ['stop', 0, toString],