mirror of
				https://github.com/public-transport/db-vendo-client.git
				synced 2025-10-31 08:06:33 +02: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