mirror of
https://github.com/public-transport/db-vendo-client.git
synced 2025-04-20 23:23:56 +03:00
DB: add journeysFromTrip()
This commit is contained in:
parent
b717642293
commit
ce82817631
4 changed files with 176 additions and 0 deletions
124
index.js
124
index.js
|
@ -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
47
lib/slice-leg.js
Normal 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
|
|
@ -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 🤔
|
||||||
|
|
|
@ -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],
|
||||||
|
|
Loading…
Add table
Reference in a new issue