mirror of
https://github.com/public-transport/db-vendo-client.git
synced 2025-02-22 22:59:35 +02:00
refactoring
This commit is contained in:
parent
bc56d41fbe
commit
ec723b3414
30 changed files with 436 additions and 769 deletions
|
@ -1,12 +0,0 @@
|
||||||
const bike = {type: 'BC', mode: 'INC'};
|
|
||||||
|
|
||||||
const accessibility = {
|
|
||||||
none: {type: 'META', mode: 'INC', meta: 'notBarrierfree'},
|
|
||||||
partial: {type: 'META', mode: 'INC', meta: 'limitedBarrierfree'},
|
|
||||||
complete: {type: 'META', mode: 'INC', meta: 'completeBarrierfree'},
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
bike,
|
|
||||||
accessibility,
|
|
||||||
};
|
|
|
@ -1,12 +0,0 @@
|
||||||
const formatLinesReq = (ctx, query) => {
|
|
||||||
return {
|
|
||||||
meth: 'LineMatch',
|
|
||||||
req: {
|
|
||||||
input: query,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
formatLinesReq,
|
|
||||||
};
|
|
|
@ -1,16 +0,0 @@
|
||||||
const formatRectangle = (profile, north, west, south, east) => {
|
|
||||||
return {
|
|
||||||
llCrd: {
|
|
||||||
x: profile.formatCoord(west),
|
|
||||||
y: profile.formatCoord(south),
|
|
||||||
},
|
|
||||||
urCrd: {
|
|
||||||
x: profile.formatCoord(east),
|
|
||||||
y: profile.formatCoord(north),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
formatRectangle,
|
|
||||||
};
|
|
|
@ -1,24 +0,0 @@
|
||||||
const formatRefreshJourneyReq = (ctx, refreshToken) => {
|
|
||||||
const {profile, opt} = ctx;
|
|
||||||
|
|
||||||
const req = {
|
|
||||||
getIST: true, // todo: make an option
|
|
||||||
getPasslist: Boolean(opt.stopovers),
|
|
||||||
getPolyline: Boolean(opt.polylines),
|
|
||||||
getTariff: Boolean(opt.tickets),
|
|
||||||
};
|
|
||||||
if (profile.refreshJourneyUseOutReconL) {
|
|
||||||
req.outReconL = [{ctx: refreshToken}];
|
|
||||||
} else {
|
|
||||||
req.ctxRecon = refreshToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
meth: 'Reconstruction',
|
|
||||||
req,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
formatRefreshJourneyReq,
|
|
||||||
};
|
|
|
@ -1,38 +0,0 @@
|
||||||
const formatRemarksReq = (ctx) => {
|
|
||||||
const {profile, opt} = ctx;
|
|
||||||
|
|
||||||
const himFltrL = [];
|
|
||||||
// todo: https://github.com/marudor/BahnhofsAbfahrten/blob/95fef0217d01344642dd423457473fe9b8b6056e/src/types/HAFAS/index.ts#L76-L91
|
|
||||||
if (opt.products) {
|
|
||||||
himFltrL.push(profile.formatProductsFilter(ctx, opt.products));
|
|
||||||
}
|
|
||||||
|
|
||||||
const req = {
|
|
||||||
himFltrL,
|
|
||||||
};
|
|
||||||
if (profile.remarksGetPolyline) {
|
|
||||||
req.getPolyline = Boolean(opt.polylines);
|
|
||||||
}
|
|
||||||
// todo: stLoc, dirLoc
|
|
||||||
// todo: comp, dept, onlyHimId, onlyToday
|
|
||||||
// todo: dailyB, dailyE
|
|
||||||
// see https://github.com/marudor/BahnhofsAbfahrten/blob/46a74957d68edc15713112df44e1a25150f5a178/src/types/HAFAS/HimSearch.ts#L3-L18
|
|
||||||
|
|
||||||
if (opt.results !== null) {
|
|
||||||
req.maxNum = opt.results;
|
|
||||||
}
|
|
||||||
if (opt.from !== null) {
|
|
||||||
req.dateB = profile.formatDate(profile, opt.from);
|
|
||||||
req.timeB = profile.formatTime(profile, opt.from);
|
|
||||||
}
|
|
||||||
if (opt.to !== null) {
|
|
||||||
req.dateE = profile.formatDate(profile, opt.to);
|
|
||||||
req.timeE = profile.formatTime(profile, opt.to);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {meth: 'HimSearch', req};
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
formatRemarksReq,
|
|
||||||
};
|
|
|
@ -1,6 +1,11 @@
|
||||||
import {formatLocationIdentifier} from './location-identifier.js';
|
import {formatLocationIdentifier} from './location-identifier.js';
|
||||||
|
|
||||||
|
const isIBNR = /^\d{6,}$/;
|
||||||
|
|
||||||
const formatStation = (id) => {
|
const formatStation = (id) => {
|
||||||
|
if (!isIBNR.test(id)) {
|
||||||
|
throw new Error('station ID must be an IBNR.');
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
type: 'S', // station
|
type: 'S', // station
|
||||||
// todo: name necessary?
|
// todo: name necessary?
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
const formatStopReq = (ctx, stopRef) => {
|
const formatStopReq = (ctx, stopRef) => {
|
||||||
return {
|
// TODO
|
||||||
// todo: there's also `StationDetails`, are there differences?
|
|
||||||
meth: 'LocDetails',
|
|
||||||
req: {
|
|
||||||
locL: [stopRef],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
2
index.js
2
index.js
|
@ -223,7 +223,7 @@ const createClient = (profile, userAgent, opt = {}) => {
|
||||||
if (opt.results !== null) {
|
if (opt.results !== null) {
|
||||||
// TODO query.numF = opt.results;
|
// TODO query.numF = opt.results;
|
||||||
}
|
}
|
||||||
const req = profile.transformJourneysQuery({profile, opt}, query);
|
const req = profile.formatJourneysReq({profile, opt}, query);
|
||||||
const {res} = await profile.request({profile, opt}, userAgent, req);
|
const {res} = await profile.request({profile, opt}, userAgent, req);
|
||||||
const ctx = {profile, opt, common, res};
|
const ctx = {profile, opt, common, res};
|
||||||
const verbindungen = opt.results ? res.verbindungen.slice(0, opt.results) : res.verbindungen;
|
const verbindungen = opt.results ? res.verbindungen.slice(0, opt.results) : res.verbindungen;
|
||||||
|
|
|
@ -1,17 +1,15 @@
|
||||||
import {request} from '../lib/request.js';
|
import {request} from '../lib/request.js';
|
||||||
|
import {products} from '../lib/products.js';
|
||||||
|
import {ageGroup, ageGroupFromAge, ageGroupLabel} from './age-group.js';
|
||||||
|
|
||||||
import {formatStationBoardReq} from '../format/station-board-req.js';
|
import {formatStationBoardReq} from '../format/station-board-req.js';
|
||||||
import {formatLocationsReq} from '../format/locations-req.js';
|
|
||||||
import {formatStopReq} from '../format/stop-req.js';
|
import {formatStopReq} from '../format/stop-req.js';
|
||||||
import {formatTripReq} from '../format/trip-req.js';
|
import {formatTripReq} from '../format/trip-req.js';
|
||||||
import {formatRefreshJourneyReq} from '../format/refresh-journey-req.js';
|
|
||||||
import {formatRemarksReq} from '../format/remarks-req.js';
|
|
||||||
import {formatLinesReq} from '../format/lines-req.js';
|
|
||||||
import {formatNearbyReq} from '../format/nearby-req.js';
|
import {formatNearbyReq} from '../format/nearby-req.js';
|
||||||
|
|
||||||
import {parseDateTime} from '../parse/date-time.js';
|
import {parseDateTime} from '../parse/date-time.js';
|
||||||
import {parsePlatform} from '../parse/platform.js';
|
import {parsePlatform} from '../parse/platform.js';
|
||||||
import {parseBitmask as parseProductsBitmask} from '../parse/products-bitmask.js';
|
import {parseProducts} from '../parse/products.js';
|
||||||
import {parseWhen} from '../parse/when.js';
|
import {parseWhen} from '../parse/when.js';
|
||||||
import {parseDeparture} from '../parse/departure.js';
|
import {parseDeparture} from '../parse/departure.js';
|
||||||
import {parseArrival} from '../parse/arrival.js';
|
import {parseArrival} from '../parse/arrival.js';
|
||||||
|
@ -24,53 +22,54 @@ import {parsePolyline} from '../parse/polyline.js';
|
||||||
import {parseOperator} from '../parse/operator.js';
|
import {parseOperator} from '../parse/operator.js';
|
||||||
import {parseRemarks} from '../parse/remarks.js';
|
import {parseRemarks} from '../parse/remarks.js';
|
||||||
import {parseStopover} from '../parse/stopover.js';
|
import {parseStopover} from '../parse/stopover.js';
|
||||||
|
import {parseLoadFactor, parseArrOrDepWithLoadFactor} from '../parse/load-factor.js';
|
||||||
|
import {parseHintByCode} from '../parse/hints-by-code.js';
|
||||||
|
|
||||||
import {formatAddress} from '../format/address.js';
|
import {formatAddress} from '../format/address.js';
|
||||||
import {formatCoord} from '../format/coord.js';
|
import {formatCoord} from '../format/coord.js';
|
||||||
import {formatDate} from '../format/date.js';
|
import {formatDate} from '../format/date.js';
|
||||||
import {formatLocationFilter} from '../format/location-filter.js';
|
|
||||||
import {formatProductsFilter} from '../format/products-filter.js';
|
import {formatProductsFilter} from '../format/products-filter.js';
|
||||||
import {formatPoi} from '../format/poi.js';
|
import {formatPoi} from '../format/poi.js';
|
||||||
import {formatStation} from '../format/station.js';
|
import {formatStation} from '../format/station.js';
|
||||||
import {formatTime} from '../format/time.js';
|
import {formatTime} from '../format/time.js';
|
||||||
import {formatLocation} from '../format/location.js';
|
import {formatLocation} from '../format/location.js';
|
||||||
import {formatRectangle} from '../format/rectangle.js';
|
import {formatLoyaltyCard} from '../format/loyalty-cards.js';
|
||||||
import * as filters from '../format/filters.js';
|
|
||||||
|
|
||||||
const DEBUG = (/(^|,)hafas-client(,|$)/).test(process.env.DEBUG || '');
|
const DEBUG = (/(^|,)hafas-client(,|$)/).test(process.env.DEBUG || '');
|
||||||
const logRequest = DEBUG
|
const logRequest = DEBUG
|
||||||
? (_, req, reqId) => console.error(String(req.body))
|
? (_, req, reqId) => console.error(String(req.body))
|
||||||
: () => {};
|
: () => { };
|
||||||
const logResponse = DEBUG
|
const logResponse = DEBUG
|
||||||
? (_, res, body, reqId) => console.error(body)
|
? (_, res, body, reqId) => console.error(body)
|
||||||
: () => {};
|
: () => { };
|
||||||
|
|
||||||
const id = (ctx, x) => x;
|
const id = (_ctx, x) => x;
|
||||||
|
const notImplemented = (_ctx, _x) => {
|
||||||
|
throw new Error('NotImplemented');
|
||||||
|
};
|
||||||
|
|
||||||
const defaultProfile = {
|
const defaultProfile = {
|
||||||
request,
|
request,
|
||||||
|
products,
|
||||||
|
ageGroup, ageGroupFromAge, ageGroupLabel,
|
||||||
transformReqBody: id,
|
transformReqBody: id,
|
||||||
transformReq: id,
|
transformReq: id,
|
||||||
salt: null,
|
|
||||||
addChecksum: false,
|
|
||||||
addMicMac: false,
|
|
||||||
randomizeUserAgent: true,
|
randomizeUserAgent: true,
|
||||||
logRequest,
|
logRequest,
|
||||||
logResponse,
|
logResponse,
|
||||||
|
|
||||||
formatStationBoardReq,
|
formatStationBoardReq,
|
||||||
formatLocationsReq,
|
formatLocationsReq: notImplemented,
|
||||||
formatStopReq,
|
formatStopReq,
|
||||||
formatTripReq,
|
formatTripReq,
|
||||||
formatRefreshJourneyReq,
|
|
||||||
formatRemarksReq,
|
|
||||||
formatLinesReq,
|
|
||||||
formatNearbyReq,
|
formatNearbyReq,
|
||||||
|
formatJourneysReq: notImplemented,
|
||||||
|
formatRefreshJourneyReq: notImplemented,
|
||||||
transformJourneysQuery: id,
|
transformJourneysQuery: id,
|
||||||
|
|
||||||
parseDateTime,
|
parseDateTime,
|
||||||
parsePlatform,
|
parsePlatform,
|
||||||
parseProductsBitmask,
|
parseProducts,
|
||||||
parseWhen,
|
parseWhen,
|
||||||
parseDeparture,
|
parseDeparture,
|
||||||
parseArrival,
|
parseArrival,
|
||||||
|
@ -78,42 +77,41 @@ const defaultProfile = {
|
||||||
parseJourneyLeg,
|
parseJourneyLeg,
|
||||||
parseJourney,
|
parseJourney,
|
||||||
parseLine,
|
parseLine,
|
||||||
parseStationName: (_, name) => name,
|
parseStationName: id,
|
||||||
parseLocation,
|
parseLocation,
|
||||||
parsePolyline,
|
parsePolyline,
|
||||||
parseOperator,
|
parseOperator,
|
||||||
parseRemarks,
|
parseRemarks,
|
||||||
parseStopover,
|
parseStopover,
|
||||||
|
parseLoadFactor,
|
||||||
|
parseArrOrDepWithLoadFactor,
|
||||||
|
parseHintByCode,
|
||||||
|
parsePrice: notImplemented,
|
||||||
|
parseTickets: notImplemented,
|
||||||
|
|
||||||
formatAddress,
|
formatAddress,
|
||||||
formatCoord,
|
formatCoord,
|
||||||
formatDate,
|
formatDate,
|
||||||
formatLocationFilter,
|
formatLocation,
|
||||||
formatProductsFilter,
|
formatLocationFilter: notImplemented,
|
||||||
|
formatLoyaltyCard,
|
||||||
formatPoi,
|
formatPoi,
|
||||||
|
formatProductsFilter,
|
||||||
formatStation,
|
formatStation,
|
||||||
formatTime,
|
formatTime,
|
||||||
formatLocation,
|
formatTravellers: notImplemented,
|
||||||
formatRectangle,
|
formatRectangle: id,
|
||||||
filters,
|
|
||||||
|
|
||||||
journeysOutFrwd: true, // `journeys()` method: support for `outFrwd` field?
|
journeysOutFrwd: true,
|
||||||
// todo: https://github.com/KDE/kpublictransport/commit/c7c54304160d8f22eab0c91812a107aca82304b7
|
|
||||||
|
|
||||||
// `departures()` method: support for `getPasslist` field?
|
|
||||||
departuresGetPasslist: false,
|
departuresGetPasslist: false,
|
||||||
// `departures()` method: support for `stbFltrEquiv` field?
|
|
||||||
departuresStbFltrEquiv: false,
|
departuresStbFltrEquiv: false,
|
||||||
|
trip: true,
|
||||||
trip: false,
|
|
||||||
radar: false,
|
radar: false,
|
||||||
refreshJourney: true,
|
refreshJourney: true,
|
||||||
// refreshJourney(): use `outReconL[]` instead of `ctxRecon`?
|
|
||||||
refreshJourneyUseOutReconL: false,
|
refreshJourneyUseOutReconL: false,
|
||||||
tripsByName: false,
|
tripsByName: false,
|
||||||
remarks: false,
|
remarks: false,
|
||||||
// `remarks()` method: support for `getPolyline` field?
|
remarksGetPolyline: false,
|
||||||
remarksGetPolyline: false, // `remarks()` method: support for `getPolyline` field?
|
|
||||||
lines: false,
|
lines: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
// For any type of "thing to parse", there's >=1 parse functions.
|
|
||||||
// By composing custom parse function(s) with the default ones, one
|
|
||||||
// can customize the behaviour of hafas-client. Profiles extensively
|
|
||||||
// use this mechanism.
|
|
||||||
|
|
||||||
// Each parse function has the following signature:
|
|
||||||
// ({opt, profile, common, res}, ...raw) => newParsed
|
|
||||||
|
|
||||||
// Compose a new/custom parse function with the old/existing parse
|
|
||||||
// function, so that the new fn will be called with the output of the
|
|
||||||
// old fn.
|
|
||||||
const parseHook = (oldParse, newParse) => {
|
|
||||||
return (ctx, ...args) => {
|
|
||||||
return newParse({
|
|
||||||
...ctx,
|
|
||||||
parsed: oldParse({...ctx, parsed: {}}, ...args),
|
|
||||||
}, ...args);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
parseHook,
|
|
||||||
};
|
|
|
@ -1,63 +0,0 @@
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
sliceLeg,
|
|
||||||
};
|
|
|
@ -11,6 +11,7 @@ const types = {
|
||||||
formatStopReq: 'function',
|
formatStopReq: 'function',
|
||||||
formatTripReq: 'function',
|
formatTripReq: 'function',
|
||||||
formatRefreshJourneyReq: 'function',
|
formatRefreshJourneyReq: 'function',
|
||||||
|
formatJourneysReq: 'function',
|
||||||
transformJourneysQuery: 'function',
|
transformJourneysQuery: 'function',
|
||||||
|
|
||||||
products: 'array',
|
products: 'array',
|
||||||
|
|
541
p/db/index.js
541
p/db/index.js
|
@ -1,540 +1,27 @@
|
||||||
// todo: use import assertions once they're supported by Node.js & ESLint
|
|
||||||
// https://github.com/tc39/proposal-import-assertions
|
|
||||||
import {createRequire} from 'module';
|
import {createRequire} from 'module';
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
import uniqBy from 'lodash/uniqBy.js';
|
|
||||||
import slugg from 'slugg';
|
|
||||||
import without from 'lodash/without.js';
|
|
||||||
import {parseHook} from '../../lib/profile-hooks.js';
|
|
||||||
|
|
||||||
import {parseJourney as _parseJourney} from '../../parse/journey.js';
|
|
||||||
import {parseJourneyLeg as _parseJourneyLeg} from '../../parse/journey-leg.js';
|
|
||||||
import {parseLine as _parseLine} from '../../parse/line.js';
|
|
||||||
import {parseArrival as _parseArrival} from '../../parse/arrival.js';
|
|
||||||
import {parseDeparture as _parseDeparture} from '../../parse/departure.js';
|
|
||||||
import {parseLocation as _parseLocation} from '../../parse/location.js';
|
|
||||||
import {formatStation as _formatStation} from '../../format/station.js';
|
|
||||||
import {parseDateTime} from '../../parse/date-time.js';
|
|
||||||
|
|
||||||
const baseProfile = require('./base.json');
|
const baseProfile = require('./base.json');
|
||||||
import {products} from './products.js';
|
import {products} from '../../lib/products.js';
|
||||||
import {formatLoyaltyCard} from './loyalty-cards.js';
|
import {formatJourneysReq, formatRefreshJourneyReq} from './journeys-req.js';
|
||||||
import {ageGroup, ageGroupFromAge, ageGroupLabel} from './ageGroup.js';
|
import {formatLocationFilter} from './location-filter.js';
|
||||||
|
import {formatLocationsReq} from './locations-req.js';
|
||||||
const transformReqBody = (ctx, body) => {
|
import {formatTravellers} from './travellers.js';
|
||||||
return body;
|
import {parseTickets, parsePrice} from './tickets.js';
|
||||||
};
|
|
||||||
|
|
||||||
const slices = (n, arr) => {
|
|
||||||
const initialState = {slices: [], count: Infinity};
|
|
||||||
return arr.reduce(({slices, count}, item) => {
|
|
||||||
if (count >= n) {
|
|
||||||
slices.push([item]);
|
|
||||||
count = 1;
|
|
||||||
} else {
|
|
||||||
slices[slices.length - 1].push(item);
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
return {slices, count};
|
|
||||||
}, initialState).slices;
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseGrid = (g) => {
|
|
||||||
// todo: g.type, e.g. `S`
|
|
||||||
// todo: respect `g.itemL[].(col|row)`?
|
|
||||||
|
|
||||||
// todo
|
|
||||||
// parseGrid is being called by parseLocWithDetails, which is being called as
|
|
||||||
// profile.parseLocation by profile.parseCommon, parseCommon hasn't finished
|
|
||||||
// resolving all references yet, so we have to resolve them manually here.
|
|
||||||
// This would be fixed if we resolve references on-the-fly or in a recursive/
|
|
||||||
// iterative process.
|
|
||||||
return {
|
|
||||||
title: g.title,
|
|
||||||
rows: slices(g.nCols, g.itemL.map(item => Array.isArray(item.hints) && item.hints[0]
|
|
||||||
|| Array.isArray(item.remarkRefs) && item.remarkRefs[0] && item.remarkRefs[0].hint
|
|
||||||
|| {},
|
|
||||||
)),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ausstattungKeys = Object.assign(Object.create(null), {
|
|
||||||
'3-s-zentrale': '3SZentrale',
|
|
||||||
'parkplatze': 'parkingLots',
|
|
||||||
'fahrrad-stellplatze': 'bicycleParkingRacks',
|
|
||||||
'opnv-anbindung': 'localPublicTransport',
|
|
||||||
'wc': 'toilets',
|
|
||||||
'schliessfacher': 'lockers',
|
|
||||||
'reisebedarf': 'travelShop',
|
|
||||||
'stufenfreier-zugang': 'stepFreeAccess',
|
|
||||||
'ein-umsteigehilfe': 'boardingAid',
|
|
||||||
'taxi-am-bahnhof': 'taxis',
|
|
||||||
});
|
|
||||||
const parseAusstattungVal = (val) => {
|
|
||||||
val = val.toLowerCase();
|
|
||||||
return val === 'ja'
|
|
||||||
? true
|
|
||||||
: val === 'nein'
|
|
||||||
? false
|
|
||||||
: val;
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseAusstattungGrid = (g) => {
|
|
||||||
// filter duplicate hint rows
|
|
||||||
const rows = uniqBy(g.rows, ([key, val]) => key + ':' + val);
|
|
||||||
|
|
||||||
const res = {};
|
|
||||||
Object.defineProperty(res, 'raw', {value: rows});
|
|
||||||
for (let [key, val] of rows) {
|
|
||||||
key = ausstattungKeys[slugg(key)];
|
|
||||||
if (key) {
|
|
||||||
res[key] = parseAusstattungVal(val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseReisezentrumÖffnungszeiten = (g) => {
|
|
||||||
const res = {};
|
|
||||||
for (const [dayOfWeek, val] of g.rows) {
|
|
||||||
res[dayOfWeek] = val;
|
|
||||||
}
|
|
||||||
res.raw = g.rows;
|
|
||||||
return res;
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseLocWithDetails = ({parsed, common}, l) => {
|
|
||||||
if (!parsed) {
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
if (parsed.type !== 'stop' && parsed.type !== 'station') {
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(l.gridL)) {
|
|
||||||
const resolveCells = grid => ({
|
|
||||||
...grid,
|
|
||||||
rows: grid.rows.map(row => row.map(cell => cell && cell.text)),
|
|
||||||
});
|
|
||||||
|
|
||||||
let grids = l.gridL
|
|
||||||
.map(grid => parseGrid(grid, common))
|
|
||||||
.map(resolveCells);
|
|
||||||
|
|
||||||
const ausstattung = grids.find(g => slugg(g.title) === 'ausstattung');
|
|
||||||
if (ausstattung) {
|
|
||||||
parsed.facilities = parseAusstattungGrid(ausstattung);
|
|
||||||
}
|
|
||||||
const öffnungszeiten = grids.find(g => slugg(g.title) === 'offnungszeiten-reisezentrum');
|
|
||||||
if (öffnungszeiten) {
|
|
||||||
parsed.reisezentrumOpeningHours = parseReisezentrumÖffnungszeiten(öffnungszeiten);
|
|
||||||
}
|
|
||||||
|
|
||||||
grids = without(grids, ausstattung, öffnungszeiten);
|
|
||||||
if (grids.length > 0) {
|
|
||||||
parsed.grids = grids;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsed;
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://www.bahn.de/p/view/service/buchung/auslastungsinformation.shtml
|
|
||||||
const loadFactors = [];
|
|
||||||
loadFactors[1] = 'low-to-medium';
|
|
||||||
loadFactors[2] = 'high';
|
|
||||||
loadFactors[3] = 'very-high';
|
|
||||||
loadFactors[4] = 'exceptionally-high';
|
|
||||||
|
|
||||||
const parseLoadFactor = (opt, auslastung) => {
|
|
||||||
if (!auslastung) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const cls = opt.firstClass
|
|
||||||
? 'KLASSE_1'
|
|
||||||
: 'KLASSE_2';
|
|
||||||
const load = auslastung.find(a => a.klasse === cls)?.stufe;
|
|
||||||
return load && loadFactors[load.r] || null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseArrOrDepWithLoadFactor = ({parsed, res, opt}, d) => {
|
|
||||||
|
|
||||||
/* const load = parseLoadFactor(opt, d);
|
|
||||||
if (load) {
|
|
||||||
parsed.loadFactor = load;
|
|
||||||
}*/ // TODO
|
|
||||||
return parsed;
|
|
||||||
};
|
|
||||||
|
|
||||||
const trfReq = (opt, refreshJourney) => {
|
|
||||||
if ('age' in opt && 'ageGroup' in opt) {
|
|
||||||
throw new TypeError(`\
|
|
||||||
opt.age and opt.ageGroup are mutually exclusive.
|
|
||||||
Pass in just opt.age, and the age group will calculated automatically.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tvlrAgeGroup = 'age' in opt
|
|
||||||
? ageGroupFromAge(opt.age)
|
|
||||||
: opt.ageGroup;
|
|
||||||
|
|
||||||
const basicCtrfReq = {
|
|
||||||
klasse: opt.firstClass === true ? 'KLASSE_1' : 'KLASSE_2',
|
|
||||||
// todo [breaking]: support multiple travelers
|
|
||||||
reisende: [{
|
|
||||||
typ: ageGroupLabel[tvlrAgeGroup || ageGroup.ADULT],
|
|
||||||
anzahl: 1,
|
|
||||||
alter: 'age' in opt
|
|
||||||
? [String(opt.age)]
|
|
||||||
: [],
|
|
||||||
ermaessigungen: [formatLoyaltyCard(opt.loyaltyCard)],
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
return basicCtrfReq;
|
|
||||||
};
|
|
||||||
|
|
||||||
const transformJourneysQuery = ({profile, opt}, query) => {
|
|
||||||
query = Object.assign(query, trfReq(opt, false));
|
|
||||||
return {
|
|
||||||
endpoint: profile.journeysEndpoint,
|
|
||||||
body: query,
|
|
||||||
method: 'post',
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatRefreshJourneyReq = (ctx, refreshToken) => {
|
|
||||||
const {profile, opt} = ctx;
|
|
||||||
let query = {
|
|
||||||
ctxRecon: refreshToken,
|
|
||||||
deutschlandTicketVorhanden: false,
|
|
||||||
nurDeutschlandTicketVerbindungen: false,
|
|
||||||
reservierungsKontingenteVorhanden: false,
|
|
||||||
};
|
|
||||||
query = Object.assign(query, trfReq(opt, true));
|
|
||||||
return {
|
|
||||||
endpoint: profile.refreshJourneysEndpoint,
|
|
||||||
body: query,
|
|
||||||
method: 'post',
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseLineWithAdditionalName = ({parsed}, l) => {
|
|
||||||
if (l.nameS && ['bus', 'tram', 'ferry'].includes(l.product)) {
|
|
||||||
parsed.name = l.nameS;
|
|
||||||
}
|
|
||||||
if (l.addName) {
|
|
||||||
parsed.additionalName = parsed.name;
|
|
||||||
parsed.name = l.addName;
|
|
||||||
}
|
|
||||||
return parsed;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mutateToAddPrice = (parsed, raw) => {
|
|
||||||
parsed.price = null;
|
|
||||||
if (raw.angebotsPreis?.betrag) {
|
|
||||||
parsed.price = {
|
|
||||||
amount: raw.angebotsPreis.betrag,
|
|
||||||
currency: raw.angebotsPreis.waehrung,
|
|
||||||
hint: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return parsed;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mutateToAddTickets = (parsed, opt, j) => {
|
|
||||||
if (!opt.tickets) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (j.reiseAngebote && j.reiseAngebote.length > 0) { // if refreshJourney()
|
|
||||||
parsed.tickets = j.reiseAngebote
|
|
||||||
.filter(s => s.typ == 'REISEANGEBOT' && !s.angebotsbeziehungList.flatMap(b => b.referenzen)
|
|
||||||
.find(r => r.referenzAngebotsoption == 'PFLICHT'))
|
|
||||||
.map((s) => {
|
|
||||||
const p = {
|
|
||||||
name: s.name,
|
|
||||||
priceObj: {
|
|
||||||
amount: Math.round(s.preis?.betrag * 100),
|
|
||||||
currency: s.preis?.waehrung,
|
|
||||||
},
|
|
||||||
firstClass: s.klasse == 'KLASSE_1',
|
|
||||||
partialFare: s.teilpreis,
|
|
||||||
};
|
|
||||||
if (s.teilpreis) {
|
|
||||||
p.addData = 'Teilpreis / partial fare';
|
|
||||||
}
|
|
||||||
if (s.konditionsAnzeigen) {
|
|
||||||
p.addDataTicketInfo = s.konditionsAnzeigen?.map(a => a.anzeigeUeberschrift)
|
|
||||||
.join('. ');
|
|
||||||
p.addDataTicketDetails = s.konditionsAnzeigen?.map(a => a.textLang)
|
|
||||||
.join(' ');
|
|
||||||
}
|
|
||||||
if (s.leuchtturmInfo) {
|
|
||||||
p.addDataTravelInfo = s.leuchtturmInfo?.text;
|
|
||||||
}
|
|
||||||
return p;
|
|
||||||
});
|
|
||||||
if (opt.generateUnreliableTicketUrls) {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (j.angebotsPreis?.betrag) { // if journeys()
|
|
||||||
parsed.tickets = [{
|
|
||||||
name: 'from',
|
|
||||||
priceObj: {
|
|
||||||
amount: Math.round(j.angebotsPreis.betrag * 100),
|
|
||||||
currency: j.angebotsPreis.waehrung,
|
|
||||||
},
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseJourneyWithPriceAndTickets = ({parsed, opt}, raw) => {
|
|
||||||
mutateToAddPrice(parsed, raw);
|
|
||||||
mutateToAddTickets(parsed, opt, raw);
|
|
||||||
return parsed;
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseJourneyLegWithLoadFactor = ({parsed, res, opt}, raw) => {
|
|
||||||
const load = parseLoadFactor(opt, raw.auslastungsmeldungen);
|
|
||||||
if (load) {
|
|
||||||
parsed.loadFactor = load;
|
|
||||||
}
|
|
||||||
return parsed;
|
|
||||||
};
|
|
||||||
|
|
||||||
// todo:
|
|
||||||
// [ { type: 'hint',
|
|
||||||
// code: 'P5',
|
|
||||||
// text: 'Es gilt ein besonderer Fahrpreis' }
|
|
||||||
const hintsByCode = Object.assign(Object.create(null), {
|
|
||||||
fb: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'bicycle-conveyance',
|
|
||||||
summary: 'bicycles conveyed',
|
|
||||||
},
|
|
||||||
fr: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'bicycle-conveyance-reservation',
|
|
||||||
summary: 'bicycles conveyed, subject to reservation',
|
|
||||||
},
|
|
||||||
nf: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'no-bicycle-conveyance',
|
|
||||||
summary: 'bicycles not conveyed',
|
|
||||||
},
|
|
||||||
k2: {
|
|
||||||
type: 'hint',
|
|
||||||
code: '2nd-class-only',
|
|
||||||
summary: '2. class only',
|
|
||||||
},
|
|
||||||
eh: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'boarding-ramp',
|
|
||||||
summary: 'vehicle-mounted boarding ramp available',
|
|
||||||
},
|
|
||||||
ro: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'wheelchairs-space',
|
|
||||||
summary: 'space for wheelchairs',
|
|
||||||
},
|
|
||||||
oa: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'wheelchairs-space-reservation',
|
|
||||||
summary: 'space for wheelchairs, subject to reservation',
|
|
||||||
},
|
|
||||||
wv: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'wifi',
|
|
||||||
summary: 'WiFi available',
|
|
||||||
},
|
|
||||||
wi: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'wifi',
|
|
||||||
summary: 'WiFi available',
|
|
||||||
},
|
|
||||||
sn: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'snacks',
|
|
||||||
summary: 'snacks available for purchase',
|
|
||||||
},
|
|
||||||
mb: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'snacks',
|
|
||||||
summary: 'snacks available for purchase',
|
|
||||||
},
|
|
||||||
mp: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'snacks',
|
|
||||||
summary: 'snacks available for purchase at the seat',
|
|
||||||
},
|
|
||||||
bf: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'barrier-free',
|
|
||||||
summary: 'barrier-free',
|
|
||||||
},
|
|
||||||
rg: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'barrier-free-vehicle',
|
|
||||||
summary: 'barrier-free vehicle',
|
|
||||||
},
|
|
||||||
bt: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'on-board-bistro',
|
|
||||||
summary: 'Bordbistro available',
|
|
||||||
},
|
|
||||||
br: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'on-board-restaurant',
|
|
||||||
summary: 'Bordrestaurant available',
|
|
||||||
},
|
|
||||||
ki: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'childrens-area',
|
|
||||||
summary: 'children\'s area available',
|
|
||||||
},
|
|
||||||
kk: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'parents-childrens-compartment',
|
|
||||||
summary: 'parent-and-children compartment available',
|
|
||||||
},
|
|
||||||
kr: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'kids-service',
|
|
||||||
summary: 'DB Kids Service available',
|
|
||||||
},
|
|
||||||
ls: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'power-sockets',
|
|
||||||
summary: 'power sockets available',
|
|
||||||
},
|
|
||||||
ev: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'replacement-service',
|
|
||||||
summary: 'replacement service',
|
|
||||||
},
|
|
||||||
kl: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'air-conditioned',
|
|
||||||
summary: 'air-conditioned vehicle',
|
|
||||||
},
|
|
||||||
r0: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'upward-escalator',
|
|
||||||
summary: 'upward escalator',
|
|
||||||
},
|
|
||||||
au: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'elevator',
|
|
||||||
summary: 'elevator available',
|
|
||||||
},
|
|
||||||
ck: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'komfort-checkin',
|
|
||||||
summary: 'Komfort-Checkin available',
|
|
||||||
},
|
|
||||||
it: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'ice-sprinter',
|
|
||||||
summary: 'ICE Sprinter service',
|
|
||||||
},
|
|
||||||
rp: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'compulsory-reservation',
|
|
||||||
summary: 'compulsory seat reservation',
|
|
||||||
},
|
|
||||||
rm: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'optional-reservation',
|
|
||||||
summary: 'optional seat reservation',
|
|
||||||
},
|
|
||||||
scl: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'all-2nd-class-seats-reserved',
|
|
||||||
summary: 'all 2nd class seats reserved',
|
|
||||||
},
|
|
||||||
acl: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'all-seats-reserved',
|
|
||||||
summary: 'all seats reserved',
|
|
||||||
},
|
|
||||||
sk: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'oversize-luggage-forbidden',
|
|
||||||
summary: 'oversize luggage not allowed',
|
|
||||||
},
|
|
||||||
hu: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'animals-forbidden',
|
|
||||||
summary: 'animals not allowed, except guide dogs',
|
|
||||||
},
|
|
||||||
ik: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'baby-cot-required',
|
|
||||||
summary: 'baby cot/child seat required',
|
|
||||||
},
|
|
||||||
ee: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'on-board-entertainment',
|
|
||||||
summary: 'on-board entertainment available',
|
|
||||||
},
|
|
||||||
toilet: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'toilet',
|
|
||||||
summary: 'toilet available',
|
|
||||||
},
|
|
||||||
oc: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'wheelchair-accessible-toilet',
|
|
||||||
summary: 'wheelchair-accessible toilet available',
|
|
||||||
},
|
|
||||||
iz: {
|
|
||||||
type: 'hint',
|
|
||||||
code: 'intercity-2',
|
|
||||||
summary: 'Intercity 2',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const parseHintByCode = (raw) => {
|
|
||||||
const hint = hintsByCode[raw.key.trim()
|
|
||||||
.toLowerCase()];
|
|
||||||
if (hint) {
|
|
||||||
return Object.assign({text: raw.value}, hint);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isIBNR = /^\d{6,}$/;
|
|
||||||
const formatStation = (id) => {
|
|
||||||
if (!isIBNR.test(id)) {
|
|
||||||
throw new Error('station ID must be an IBNR.');
|
|
||||||
}
|
|
||||||
return _formatStation(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
// todo: find option for absolute number of results
|
|
||||||
|
|
||||||
const profile = {
|
const profile = {
|
||||||
...baseProfile,
|
...baseProfile,
|
||||||
locale: 'de-DE',
|
locale: 'de-DE',
|
||||||
timezone: 'Europe/Berlin',
|
timezone: 'Europe/Berlin',
|
||||||
addChecksum: true,
|
|
||||||
|
|
||||||
transformReqBody,
|
products,
|
||||||
transformJourneysQuery,
|
formatJourneysReq,
|
||||||
formatRefreshJourneyReq,
|
formatRefreshJourneyReq,
|
||||||
|
formatLocationsReq,
|
||||||
products: products,
|
formatLocationFilter,
|
||||||
|
parsePrice,
|
||||||
parseLocation: parseHook(_parseLocation, parseLocWithDetails),
|
parseTickets,
|
||||||
parseJourney: parseHook(_parseJourney, parseJourneyWithPriceAndTickets),
|
formatTravellers,
|
||||||
parseJourneyLeg: parseHook(_parseJourneyLeg, parseJourneyLegWithLoadFactor),
|
|
||||||
parseLine: parseHook(_parseLine, parseLineWithAdditionalName),
|
|
||||||
parseArrival: parseHook(_parseArrival, parseArrOrDepWithLoadFactor),
|
|
||||||
parseDeparture: parseHook(_parseDeparture, parseArrOrDepWithLoadFactor),
|
|
||||||
parseDateTime,
|
|
||||||
parseLoadFactor,
|
|
||||||
parseHintByCode,
|
|
||||||
formatStation,
|
|
||||||
|
|
||||||
generateUnreliableTicketUrls: false,
|
generateUnreliableTicketUrls: false,
|
||||||
refreshJourneyUseOutReconL: true,
|
refreshJourneyUseOutReconL: true,
|
||||||
|
@ -542,7 +29,7 @@ const profile = {
|
||||||
journeysFromTrip: true,
|
journeysFromTrip: true,
|
||||||
radar: true,
|
radar: true,
|
||||||
reachableFrom: true,
|
reachableFrom: true,
|
||||||
lines: false, // `.svcResL[0].res.lineL[]` is missing 🤔
|
lines: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
29
p/db/journeys-req.js
Normal file
29
p/db/journeys-req.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
const formatJourneysReq = (ctx, query) => {
|
||||||
|
query = Object.assign(query, ctx.profile.formatTravellers(ctx));
|
||||||
|
return {
|
||||||
|
endpoint: ctx.profile.journeysEndpoint,
|
||||||
|
body: query,
|
||||||
|
method: 'post',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatRefreshJourneyReq = (ctx, refreshToken) => {
|
||||||
|
const {profile} = ctx;
|
||||||
|
let query = {
|
||||||
|
ctxRecon: refreshToken,
|
||||||
|
deutschlandTicketVorhanden: false,
|
||||||
|
nurDeutschlandTicketVerbindungen: false,
|
||||||
|
reservierungsKontingenteVorhanden: false,
|
||||||
|
};
|
||||||
|
query = Object.assign(query, profile.formatTravellers(ctx));
|
||||||
|
return {
|
||||||
|
endpoint: profile.refreshJourneysEndpoint,
|
||||||
|
body: query,
|
||||||
|
method: 'post',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
formatJourneysReq,
|
||||||
|
formatRefreshJourneyReq,
|
||||||
|
};
|
65
p/db/tickets.js
Normal file
65
p/db/tickets.js
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
|
||||||
|
const parsePrice = (ctx, raw) => {
|
||||||
|
if (raw.angebotsPreis?.betrag) {
|
||||||
|
return {
|
||||||
|
amount: raw.angebotsPreis.betrag,
|
||||||
|
currency: raw.angebotsPreis.waehrung,
|
||||||
|
hint: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseTickets = (ctx, j) => {
|
||||||
|
if (!ctx.opt.tickets) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let tickets = undefined;
|
||||||
|
if (j.reiseAngebote && j.reiseAngebote.length > 0) { // if refreshJourney()
|
||||||
|
tickets = j.reiseAngebote
|
||||||
|
.filter(s => s.typ == 'REISEANGEBOT' && !s.angebotsbeziehungList.flatMap(b => b.referenzen)
|
||||||
|
.find(r => r.referenzAngebotsoption == 'PFLICHT'))
|
||||||
|
.map((s) => {
|
||||||
|
const p = {
|
||||||
|
name: s.name,
|
||||||
|
priceObj: {
|
||||||
|
amount: Math.round(s.preis?.betrag * 100),
|
||||||
|
currency: s.preis?.waehrung,
|
||||||
|
},
|
||||||
|
firstClass: s.klasse == 'KLASSE_1',
|
||||||
|
partialFare: s.teilpreis,
|
||||||
|
};
|
||||||
|
if (s.teilpreis) {
|
||||||
|
p.addData = 'Teilpreis / partial fare';
|
||||||
|
}
|
||||||
|
if (s.konditionsAnzeigen) {
|
||||||
|
p.addDataTicketInfo = s.konditionsAnzeigen?.map(a => a.anzeigeUeberschrift)
|
||||||
|
.join('. ');
|
||||||
|
p.addDataTicketDetails = s.konditionsAnzeigen?.map(a => a.textLang)
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
if (s.leuchtturmInfo) {
|
||||||
|
p.addDataTravelInfo = s.leuchtturmInfo?.text;
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
});
|
||||||
|
if (ctx.opt.generateUnreliableTicketUrls) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (j.angebotsPreis?.betrag) { // if journeys()
|
||||||
|
tickets = [{
|
||||||
|
name: 'from',
|
||||||
|
priceObj: {
|
||||||
|
amount: Math.round(j.angebotsPreis.betrag * 100),
|
||||||
|
currency: j.angebotsPreis.waehrung,
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
return tickets;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
parsePrice,
|
||||||
|
parseTickets,
|
||||||
|
};
|
29
p/db/travellers.js
Normal file
29
p/db/travellers.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
const formatTravellers = ({profile, opt}) => {
|
||||||
|
if ('age' in opt && 'ageGroup' in opt) {
|
||||||
|
throw new TypeError(`\
|
||||||
|
opt.age and opt.ageGroup are mutually exclusive.
|
||||||
|
Pass in just opt.age, and the age group will calculated automatically.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tvlrAgeGroup = 'age' in opt
|
||||||
|
? profile.ageGroupFromAge(opt.age)
|
||||||
|
: opt.ageGroup;
|
||||||
|
|
||||||
|
const basicCtrfReq = {
|
||||||
|
klasse: opt.firstClass === true ? 'KLASSE_1' : 'KLASSE_2',
|
||||||
|
// todo [breaking]: support multiple travelers
|
||||||
|
reisende: [{
|
||||||
|
typ: profile.ageGroupLabel[tvlrAgeGroup || profile.ageGroup.ADULT],
|
||||||
|
anzahl: 1,
|
||||||
|
alter: 'age' in opt
|
||||||
|
? [String(opt.age)]
|
||||||
|
: [],
|
||||||
|
ermaessigungen: [profile.formatLoyaltyCard(opt.loyaltyCard)],
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
return basicCtrfReq;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
formatTravellers,
|
||||||
|
};
|
|
@ -1,5 +1,3 @@
|
||||||
import {parseRemarks} from './remarks.js';
|
|
||||||
|
|
||||||
const ARRIVAL = 'a';
|
const ARRIVAL = 'a';
|
||||||
const DEPARTURE = 'd';
|
const DEPARTURE = 'd';
|
||||||
|
|
||||||
|
@ -23,6 +21,7 @@ const createParseArrOrDep = (prefix) => {
|
||||||
remarks: [],
|
remarks: [],
|
||||||
origin: profile.parseLocation(ctx, d.transport?.origin || d.origin) || null,
|
origin: profile.parseLocation(ctx, d.transport?.origin || d.origin) || null,
|
||||||
destination: profile.parseLocation(ctx, d.transport?.destination || d.destination) || null,
|
destination: profile.parseLocation(ctx, d.transport?.destination || d.destination) || null,
|
||||||
|
// loadFactor: profile.parseArrOrDepWithLoadFactor(ctx, d)
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO pos
|
// TODO pos
|
||||||
|
@ -33,7 +32,7 @@ const createParseArrOrDep = (prefix) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opt.remarks) {
|
if (opt.remarks) {
|
||||||
res.remarks = parseRemarks(ctx, d);
|
res.remarks = profile.parseRemarks(ctx, d);
|
||||||
}
|
}
|
||||||
// TODO opt.stopovers
|
// TODO opt.stopovers
|
||||||
return res;
|
return res;
|
||||||
|
|
206
parse/hints-by-code.js
Normal file
206
parse/hints-by-code.js
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
|
||||||
|
// todo:
|
||||||
|
// [ { type: 'hint',
|
||||||
|
// code: 'P5',
|
||||||
|
// text: 'Es gilt ein besonderer Fahrpreis' }
|
||||||
|
const hintsByCode = Object.assign(Object.create(null), {
|
||||||
|
fb: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'bicycle-conveyance',
|
||||||
|
summary: 'bicycles conveyed',
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'bicycle-conveyance-reservation',
|
||||||
|
summary: 'bicycles conveyed, subject to reservation',
|
||||||
|
},
|
||||||
|
nf: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'no-bicycle-conveyance',
|
||||||
|
summary: 'bicycles not conveyed',
|
||||||
|
},
|
||||||
|
k2: {
|
||||||
|
type: 'hint',
|
||||||
|
code: '2nd-class-only',
|
||||||
|
summary: '2. class only',
|
||||||
|
},
|
||||||
|
eh: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'boarding-ramp',
|
||||||
|
summary: 'vehicle-mounted boarding ramp available',
|
||||||
|
},
|
||||||
|
ro: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'wheelchairs-space',
|
||||||
|
summary: 'space for wheelchairs',
|
||||||
|
},
|
||||||
|
oa: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'wheelchairs-space-reservation',
|
||||||
|
summary: 'space for wheelchairs, subject to reservation',
|
||||||
|
},
|
||||||
|
wv: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'wifi',
|
||||||
|
summary: 'WiFi available',
|
||||||
|
},
|
||||||
|
wi: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'wifi',
|
||||||
|
summary: 'WiFi available',
|
||||||
|
},
|
||||||
|
sn: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'snacks',
|
||||||
|
summary: 'snacks available for purchase',
|
||||||
|
},
|
||||||
|
mb: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'snacks',
|
||||||
|
summary: 'snacks available for purchase',
|
||||||
|
},
|
||||||
|
mp: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'snacks',
|
||||||
|
summary: 'snacks available for purchase at the seat',
|
||||||
|
},
|
||||||
|
bf: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'barrier-free',
|
||||||
|
summary: 'barrier-free',
|
||||||
|
},
|
||||||
|
rg: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'barrier-free-vehicle',
|
||||||
|
summary: 'barrier-free vehicle',
|
||||||
|
},
|
||||||
|
bt: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'on-board-bistro',
|
||||||
|
summary: 'Bordbistro available',
|
||||||
|
},
|
||||||
|
br: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'on-board-restaurant',
|
||||||
|
summary: 'Bordrestaurant available',
|
||||||
|
},
|
||||||
|
ki: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'childrens-area',
|
||||||
|
summary: 'children\'s area available',
|
||||||
|
},
|
||||||
|
kk: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'parents-childrens-compartment',
|
||||||
|
summary: 'parent-and-children compartment available',
|
||||||
|
},
|
||||||
|
kr: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'kids-service',
|
||||||
|
summary: 'DB Kids Service available',
|
||||||
|
},
|
||||||
|
ls: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'power-sockets',
|
||||||
|
summary: 'power sockets available',
|
||||||
|
},
|
||||||
|
ev: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'replacement-service',
|
||||||
|
summary: 'replacement service',
|
||||||
|
},
|
||||||
|
kl: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'air-conditioned',
|
||||||
|
summary: 'air-conditioned vehicle',
|
||||||
|
},
|
||||||
|
r0: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'upward-escalator',
|
||||||
|
summary: 'upward escalator',
|
||||||
|
},
|
||||||
|
au: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'elevator',
|
||||||
|
summary: 'elevator available',
|
||||||
|
},
|
||||||
|
ck: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'komfort-checkin',
|
||||||
|
summary: 'Komfort-Checkin available',
|
||||||
|
},
|
||||||
|
it: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'ice-sprinter',
|
||||||
|
summary: 'ICE Sprinter service',
|
||||||
|
},
|
||||||
|
rp: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'compulsory-reservation',
|
||||||
|
summary: 'compulsory seat reservation',
|
||||||
|
},
|
||||||
|
rm: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'optional-reservation',
|
||||||
|
summary: 'optional seat reservation',
|
||||||
|
},
|
||||||
|
scl: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'all-2nd-class-seats-reserved',
|
||||||
|
summary: 'all 2nd class seats reserved',
|
||||||
|
},
|
||||||
|
acl: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'all-seats-reserved',
|
||||||
|
summary: 'all seats reserved',
|
||||||
|
},
|
||||||
|
sk: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'oversize-luggage-forbidden',
|
||||||
|
summary: 'oversize luggage not allowed',
|
||||||
|
},
|
||||||
|
hu: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'animals-forbidden',
|
||||||
|
summary: 'animals not allowed, except guide dogs',
|
||||||
|
},
|
||||||
|
ik: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'baby-cot-required',
|
||||||
|
summary: 'baby cot/child seat required',
|
||||||
|
},
|
||||||
|
ee: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'on-board-entertainment',
|
||||||
|
summary: 'on-board entertainment available',
|
||||||
|
},
|
||||||
|
toilet: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'toilet',
|
||||||
|
summary: 'toilet available',
|
||||||
|
},
|
||||||
|
oc: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'wheelchair-accessible-toilet',
|
||||||
|
summary: 'wheelchair-accessible toilet available',
|
||||||
|
},
|
||||||
|
iz: {
|
||||||
|
type: 'hint',
|
||||||
|
code: 'intercity-2',
|
||||||
|
summary: 'Intercity 2',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const parseHintByCode = (raw) => {
|
||||||
|
const hint = hintsByCode[raw.key.trim()
|
||||||
|
.toLowerCase()];
|
||||||
|
if (hint) {
|
||||||
|
return Object.assign({text: raw.value}, hint);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
hintsByCode,
|
||||||
|
parseHintByCode,
|
||||||
|
};
|
|
@ -96,6 +96,11 @@ const parseJourneyLeg = (ctx, pt, date, fallbackLocations) => { // pt = raw leg
|
||||||
Object.defineProperty(res, 'canceled', {value: true});
|
Object.defineProperty(res, 'canceled', {value: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const load = profile.parseLoadFactor(opt, pt.auslastungsmeldungen);
|
||||||
|
if (load) {
|
||||||
|
res.loadFactor = load;
|
||||||
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,12 @@ const parseJourney = (ctx, j) => { // j = raw journey
|
||||||
// res.scheduledDays = profile.parseScheduledDays(ctx, j.serviceDays);
|
// res.scheduledDays = profile.parseScheduledDays(ctx, j.serviceDays);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res.price = profile.parsePrice(ctx, j);
|
||||||
|
const tickets = profile.parseTickets(ctx, j);
|
||||||
|
if (tickets) {
|
||||||
|
res.tickets = tickets;
|
||||||
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
31
parse/load-factor.js
Normal file
31
parse/load-factor.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
// https://www.bahn.de/p/view/service/buchung/auslastungsinformation.shtml
|
||||||
|
const loadFactors = [];
|
||||||
|
loadFactors[1] = 'low-to-medium';
|
||||||
|
loadFactors[2] = 'high';
|
||||||
|
loadFactors[3] = 'very-high';
|
||||||
|
loadFactors[4] = 'exceptionally-high';
|
||||||
|
|
||||||
|
const parseLoadFactor = (opt, auslastung) => {
|
||||||
|
if (!auslastung) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const cls = opt.firstClass
|
||||||
|
? 'KLASSE_1'
|
||||||
|
: 'KLASSE_2';
|
||||||
|
const load = auslastung.find(a => a.klasse === cls)?.stufe;
|
||||||
|
return load && loadFactors[load.r] || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseArrOrDepWithLoadFactor = (ctx, d) => {
|
||||||
|
|
||||||
|
/* const load = parseLoadFactor(opt, d);
|
||||||
|
if (load) {
|
||||||
|
parsed.loadFactor = load;
|
||||||
|
}*/ // TODO
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
parseArrOrDepWithLoadFactor,
|
||||||
|
parseLoadFactor,
|
||||||
|
};
|
|
@ -40,7 +40,7 @@ const parseLocation = (ctx, l) => {
|
||||||
// TODO subStops
|
// TODO subStops
|
||||||
|
|
||||||
if ('products' in l) {
|
if ('products' in l) {
|
||||||
stop.products = profile.parseProductsBitmask(ctx, l.products);
|
stop.products = profile.parseProducts(ctx, l.products);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (common && common.locations && common.locations[stop.id]) {
|
if (common && common.locations && common.locations[stop.id]) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const parseBitmask = ({profile}, bitmask) => {
|
const parseProducts = ({profile}, bitmask) => {
|
||||||
const res = {};
|
const res = {};
|
||||||
for (let product of profile.products) {
|
for (let product of profile.products) {
|
||||||
res[product.id] = Boolean(bitmask.find(p => p == product.vendo));
|
res[product.id] = Boolean(bitmask.find(p => p == product.vendo));
|
||||||
|
@ -7,5 +7,5 @@ const parseBitmask = ({profile}, bitmask) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
parseBitmask,
|
parseProducts,
|
||||||
};
|
};
|
|
@ -2,7 +2,7 @@ import tap from 'tap';
|
||||||
|
|
||||||
import {createClient} from '../../index.js';
|
import {createClient} from '../../index.js';
|
||||||
import {profile as rawProfile} from '../../p/db/index.js';
|
import {profile as rawProfile} from '../../p/db/index.js';
|
||||||
import {data as loyaltyCards} from '../../p/db/loyalty-cards.js';
|
import {data as loyaltyCards} from '../../format/loyalty-cards.js';
|
||||||
|
|
||||||
const client = createClient(rawProfile, 'public-transport/hafas-client:test');
|
const client = createClient(rawProfile, 'public-transport/hafas-client:test');
|
||||||
const {profile} = client;
|
const {profile} = client;
|
||||||
|
@ -69,7 +69,7 @@ tap.test('formats a journeys() request correctly (DB)', (t) => {
|
||||||
|
|
||||||
// transformJourneysQuery() mutates its 2nd argument!
|
// transformJourneysQuery() mutates its 2nd argument!
|
||||||
const query = {...berlinWienQuery0};
|
const query = {...berlinWienQuery0};
|
||||||
const req = profile.transformJourneysQuery(ctx, query);
|
const req = profile.formatJourneysReq(ctx, query);
|
||||||
|
|
||||||
t.same(req.body, {
|
t.same(req.body, {
|
||||||
...berlinWienQuery0,
|
...berlinWienQuery0,
|
||||||
|
@ -96,7 +96,7 @@ tap.test('formats a journeys() request with BC correctly (DB)', (t) => {
|
||||||
|
|
||||||
// transformJourneysQuery() mutates its 2nd argument!
|
// transformJourneysQuery() mutates its 2nd argument!
|
||||||
const query = {...berlinWienQuery0};
|
const query = {...berlinWienQuery0};
|
||||||
const req = profile.transformJourneysQuery(ctx, query);
|
const req = profile.formatJourneysReq(ctx, query);
|
||||||
|
|
||||||
t.same(req.body, {
|
t.same(req.body, {
|
||||||
...berlinWienQuery0,
|
...berlinWienQuery0,
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import tap from 'tap';
|
import tap from 'tap';
|
||||||
import {parseLocation as parse} from '../../parse/location.js';
|
import {parseLocation as parse} from '../../parse/location.js';
|
||||||
import {parseBitmask as parseProductsBitmask} from '../../parse/products-bitmask.js';
|
import {parseProducts} from '../../parse/products.js';
|
||||||
|
|
||||||
const profile = {
|
const profile = {
|
||||||
parseLocation: parse,
|
parseLocation: parse,
|
||||||
parseStationName: (_, name) => name.toLowerCase(),
|
parseStationName: (_, name) => name.toLowerCase(),
|
||||||
parseProductsBitmask,
|
parseProducts,
|
||||||
products: [{
|
products: [{
|
||||||
id: 'nationalExpress',
|
id: 'nationalExpress',
|
||||||
vendo: 'ICE',
|
vendo: 'ICE',
|
||||||
|
|
Loading…
Add table
Reference in a new issue