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 validateProfile = require('./lib/validate-profile')
|
||||
const {INVALID_REQUEST} = require('./lib/errors')
|
||||
const sliceLeg = require('./lib/slice-leg')
|
||||
|
||||
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 = {}) => {
|
||||
if (!isNonEmptyString(query)) {
|
||||
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.radar) client.radar = radar
|
||||
if (profile.refreshJourney) client.refreshJourney = refreshJourney
|
||||
if (profile.journeysFromTrip) client.journeysFromTrip = journeysFromTrip
|
||||
if (profile.reachableFrom) client.reachableFrom = reachableFrom
|
||||
if (profile.tripsByName) client.tripsByName = tripsByName
|
||||
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,
|
||||
refreshJourneyUseOutReconL: true,
|
||||
trip: true,
|
||||
journeysFromTrip: true,
|
||||
radar: true,
|
||||
reachableFrom: true,
|
||||
lines: false, // `.svcResL[0].res.lineL[]` is missing 🤔
|
||||
|
|
|
@ -26,6 +26,10 @@ const parseArgs = [
|
|||
['journeys', 2, parseJsObject],
|
||||
['refreshJourney', 0, toString],
|
||||
['refreshJourney', 1, parseJsObject],
|
||||
['journeysFromTrip', 0, toString],
|
||||
['journeysFromTrip', 1, parseJsObject],
|
||||
['journeysFromTrip', 2, toString],
|
||||
['journeysFromTrip', 3, parseJsObject],
|
||||
['locations', 0, toString],
|
||||
['locations', 1, parseJsObject],
|
||||
['stop', 0, toString],
|
||||
|
|
Loading…
Add table
Reference in a new issue