mirror of
https://github.com/public-transport/db-vendo-client.git
synced 2025-02-22 22:59:35 +02:00
dbnav journeys, trips, fixes
This commit is contained in:
parent
3d998de41c
commit
87a705e966
29 changed files with 3528 additions and 129 deletions
6
index.js
6
index.js
|
@ -195,8 +195,8 @@ const createClient = (profile, userAgent, opt = {}) => {
|
||||||
.map(j => profile.parseJourney(ctx, j));
|
.map(j => profile.parseJourney(ctx, j));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
earlierRef: res.verbindungReference?.earlier || null,
|
earlierRef: res.verbindungReference?.earlier || res.frueherContext || null,
|
||||||
laterRef: res.verbindungReference?.later || null,
|
laterRef: res.verbindungReference?.later || res.spaeterContext || null,
|
||||||
journeys,
|
journeys,
|
||||||
realtimeDataUpdatedAt: null, // TODO
|
realtimeDataUpdatedAt: null, // TODO
|
||||||
};
|
};
|
||||||
|
@ -223,7 +223,7 @@ const createClient = (profile, userAgent, opt = {}) => {
|
||||||
const ctx = {profile, opt, common, res};
|
const ctx = {profile, opt, common, res};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
journey: profile.parseJourney(ctx, res.verbindungen[0]),
|
journey: profile.parseJourney(ctx, res.verbindungen && res.verbindungen[0] || res),
|
||||||
realtimeDataUpdatedAt: null, // TODO
|
realtimeDataUpdatedAt: null, // TODO
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {parseRemarks, parseCancelled} 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 {parseLoadFactor, parseArrOrDepWithLoadFactor} from '../parse/load-factor.js';
|
||||||
import {parseHintByCode} from '../parse/hints-by-code.js';
|
import {parseHintByCode} from '../parse/hints-by-code.js';
|
||||||
|
import {parseTickets, parsePrice} from '../parse/tickets.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';
|
||||||
|
@ -33,6 +34,7 @@ import {formatPoi} from '../format/poi.js';
|
||||||
import {formatStation} from '../format/station.js';
|
import {formatStation} from '../format/station.js';
|
||||||
import {formatTime, formatTimeOfDay} from '../format/time.js';
|
import {formatTime, formatTimeOfDay} from '../format/time.js';
|
||||||
import {formatLocation} from '../format/location.js';
|
import {formatLocation} from '../format/location.js';
|
||||||
|
import {formatTravellers} from '../format/travellers.js';
|
||||||
import {formatLoyaltyCard} from '../format/loyalty-cards.js';
|
import {formatLoyaltyCard} from '../format/loyalty-cards.js';
|
||||||
|
|
||||||
const DEBUG = (/(^|,)hafas-client(,|$)/).test(process.env.DEBUG || '');
|
const DEBUG = (/(^|,)hafas-client(,|$)/).test(process.env.DEBUG || '');
|
||||||
|
@ -87,8 +89,8 @@ const defaultProfile = {
|
||||||
parseLoadFactor,
|
parseLoadFactor,
|
||||||
parseArrOrDepWithLoadFactor,
|
parseArrOrDepWithLoadFactor,
|
||||||
parseHintByCode,
|
parseHintByCode,
|
||||||
parsePrice: notImplemented,
|
parsePrice,
|
||||||
parseTickets: notImplemented,
|
parseTickets,
|
||||||
|
|
||||||
formatAddress,
|
formatAddress,
|
||||||
formatCoord,
|
formatCoord,
|
||||||
|
@ -101,7 +103,7 @@ const defaultProfile = {
|
||||||
formatStation,
|
formatStation,
|
||||||
formatTime,
|
formatTime,
|
||||||
formatTimeOfDay,
|
formatTimeOfDay,
|
||||||
formatTravellers: notImplemented,
|
formatTravellers,
|
||||||
formatRectangle: id,
|
formatRectangle: id,
|
||||||
|
|
||||||
journeysOutFrwd: true,
|
journeysOutFrwd: true,
|
||||||
|
|
|
@ -6,8 +6,6 @@ import {products} from '../../lib/products.js';
|
||||||
import {formatJourneysReq, formatRefreshJourneyReq} from './journeys-req.js';
|
import {formatJourneysReq, formatRefreshJourneyReq} from './journeys-req.js';
|
||||||
import {formatLocationFilter} from './location-filter.js';
|
import {formatLocationFilter} from './location-filter.js';
|
||||||
import {formatLocationsReq} from './locations-req.js';
|
import {formatLocationsReq} from './locations-req.js';
|
||||||
import {formatTravellers} from './travellers.js';
|
|
||||||
import {parseTickets, parsePrice} from './tickets.js';
|
|
||||||
|
|
||||||
const profile = {
|
const profile = {
|
||||||
...baseProfile,
|
...baseProfile,
|
||||||
|
@ -19,9 +17,6 @@ const profile = {
|
||||||
formatRefreshJourneyReq,
|
formatRefreshJourneyReq,
|
||||||
formatLocationsReq,
|
formatLocationsReq,
|
||||||
formatLocationFilter,
|
formatLocationFilter,
|
||||||
parsePrice,
|
|
||||||
parseTickets,
|
|
||||||
formatTravellers,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
|
@ -1,65 +0,0 @@
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"journeysEndpoint": "https://app.vendo.noncd.db.de/mob/angebote/fahrplan",
|
"journeysEndpoint": "https://app.vendo.noncd.db.de/mob/angebote/fahrplan",
|
||||||
"refreshJourneysEndpointPrice": "https://app.vendo.noncd.db.de/mob/angebote/recon/autonomereservierung",
|
"refreshJourneysEndpointTickets": "https://app.vendo.noncd.db.de/mob/angebote/recon",
|
||||||
"refreshJourneysEndpointPolyline": "https://app.vendo.noncd.db.de/mob/trip/recon",
|
"refreshJourneysEndpointPolyline": "https://app.vendo.noncd.db.de/mob/trip/recon",
|
||||||
"locationsEndpoint": "https://app.vendo.noncd.db.de/mob/location/search",
|
"locationsEndpoint": "https://app.vendo.noncd.db.de/mob/location/search",
|
||||||
"nearbyEndpoint": "https://app.vendo.noncd.db.de/mob/location/nearby",
|
"nearbyEndpoint": "https://app.vendo.noncd.db.de/mob/location/nearby",
|
||||||
"tripEndpoint": "https://app.vendo.noncd.db.de/mob/zuglauf",
|
"tripEndpoint": "https://app.vendo.noncd.db.de/mob/zuglauf/",
|
||||||
"boardEndpoint": "https://app.vendo.noncd.db.de/mob/bahnhofstafel/",
|
"boardEndpoint": "https://app.vendo.noncd.db.de/mob/bahnhofstafel/",
|
||||||
"defaultLanguage": "en"
|
"defaultLanguage": "en"
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,13 +3,12 @@ const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
const baseProfile = require('./base.json');
|
const baseProfile = require('./base.json');
|
||||||
import {products} from '../../lib/products.js';
|
import {products} from '../../lib/products.js';
|
||||||
// import {formatJourneysReq, formatRefreshJourneyReq} from './journeys-req.js';
|
import {formatJourneysReq, formatRefreshJourneyReq} from './journeys-req.js';
|
||||||
|
import {formatTripReq} from './trip-req.js';
|
||||||
import {formatLocationFilter} from './location-filter.js';
|
import {formatLocationFilter} from './location-filter.js';
|
||||||
import {formatLocationsReq} from './locations-req.js';
|
import {formatLocationsReq} from './locations-req.js';
|
||||||
import {formatNearbyReq} from './nearby-req.js';
|
import {formatNearbyReq} from './nearby-req.js';
|
||||||
import {formatStationBoardReq} from './station-board-req.js';
|
import {formatStationBoardReq} from './station-board-req.js';
|
||||||
// import {formatTravellers} from './travellers.js';
|
|
||||||
// import {parseTickets, parsePrice} from './tickets.js';
|
|
||||||
|
|
||||||
const profile = {
|
const profile = {
|
||||||
...baseProfile,
|
...baseProfile,
|
||||||
|
@ -17,15 +16,13 @@ const profile = {
|
||||||
timezone: 'Europe/Berlin',
|
timezone: 'Europe/Berlin',
|
||||||
|
|
||||||
products,
|
products,
|
||||||
// formatJourneysReq,
|
formatJourneysReq,
|
||||||
// formatRefreshJourneyReq,
|
formatRefreshJourneyReq,
|
||||||
|
formatTripReq,
|
||||||
formatNearbyReq,
|
formatNearbyReq,
|
||||||
formatLocationsReq,
|
formatLocationsReq,
|
||||||
formatStationBoardReq,
|
formatStationBoardReq,
|
||||||
formatLocationFilter,
|
formatLocationFilter,
|
||||||
// parsePrice,
|
|
||||||
// parseTickets,
|
|
||||||
// formatTravellers,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
87
p/dbnav/journeys-req.js
Normal file
87
p/dbnav/journeys-req.js
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import {getHeaders} from './header.js';
|
||||||
|
|
||||||
|
const formatBaseJourneysReq = (ctx) => {
|
||||||
|
const {opt} = ctx;
|
||||||
|
|
||||||
|
// TODO opt.accessibility
|
||||||
|
// TODO routingMode
|
||||||
|
const travellers = ctx.profile.formatTravellers(ctx);
|
||||||
|
return {
|
||||||
|
autonomeReservierung: false,
|
||||||
|
einstiegsTypList: [
|
||||||
|
'STANDARD',
|
||||||
|
],
|
||||||
|
klasse: travellers.klasse,
|
||||||
|
reisendenProfil: {
|
||||||
|
reisende: travellers.reisende.map(t => {
|
||||||
|
return {
|
||||||
|
ermaessigungen: [
|
||||||
|
t.ermaessigungen[0].art + ' ' + t.ermaessigungen[0].klasse,
|
||||||
|
],
|
||||||
|
reisendenTyp: t.typ,
|
||||||
|
alter: opt.age,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
reservierungsKontingenteVorhanden: false,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatJourneysReq = (ctx, from, to, when, outFrwd, journeysRef) => {
|
||||||
|
const {profile, opt} = ctx;
|
||||||
|
|
||||||
|
from = profile.formatLocation(profile, from, 'from');
|
||||||
|
to = profile.formatLocation(profile, to, 'to');
|
||||||
|
const filters = profile.formatProductsFilter({profile}, opt.products || {}, 'dbnav');
|
||||||
|
// TODO opt.accessibility
|
||||||
|
// TODO routingMode
|
||||||
|
let query = formatBaseJourneysReq(ctx);
|
||||||
|
query.reiseHin = {
|
||||||
|
wunsch: {
|
||||||
|
abgangsLocationId: from.lid,
|
||||||
|
verkehrsmittel: filters,
|
||||||
|
zeitWunsch: {
|
||||||
|
reiseDatum: profile.formatTime(profile, when, true),
|
||||||
|
zeitPunktArt: outFrwd ? 'ABFAHRT' : 'ANKUNFT',
|
||||||
|
},
|
||||||
|
viaLocations: opt.via
|
||||||
|
? [{locationId: profile.formatLocation(profile, opt.via, 'opt.via').lid}]
|
||||||
|
: undefined,
|
||||||
|
zielLocationId: to.lid,
|
||||||
|
maxUmstiege: opt.transfers || undefined,
|
||||||
|
minUmstiegsdauer: opt.transferTime || undefined,
|
||||||
|
fahrradmitnahme: opt.bike,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (journeysRef) {
|
||||||
|
query.reiseHin.wunsch.context = journeysRef;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
endpoint: ctx.profile.journeysEndpoint,
|
||||||
|
body: query,
|
||||||
|
headers: getHeaders('application/x.db.vendo.mob.verbindungssuche.v8+json'),
|
||||||
|
method: 'post',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatRefreshJourneyReq = (ctx, refreshToken) => {
|
||||||
|
const {profile, opt} = ctx;
|
||||||
|
let query = {
|
||||||
|
reconCtx: refreshToken,
|
||||||
|
};
|
||||||
|
if (opt.tickets) {
|
||||||
|
query = formatBaseJourneysReq(ctx);
|
||||||
|
query.verbindungHin = {kontext: refreshToken};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
endpoint: opt.tickets ? profile.refreshJourneysEndpointTickets : profile.refreshJourneysEndpointPolyline,
|
||||||
|
body: query,
|
||||||
|
headers: getHeaders('application/x.db.vendo.mob.verbindungssuche.v8+json'),
|
||||||
|
method: 'post',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
formatJourneysReq,
|
||||||
|
formatRefreshJourneyReq,
|
||||||
|
};
|
14
p/dbnav/trip-req.js
Normal file
14
p/dbnav/trip-req.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import {getHeaders} from './header.js';
|
||||||
|
|
||||||
|
const formatTripReq = ({profile, opt}, id) => {
|
||||||
|
return {
|
||||||
|
endpoint: profile.tripEndpoint,
|
||||||
|
path: encodeURIComponent(id),
|
||||||
|
headers: getHeaders('application/x.db.vendo.mob.zuglauf.v2+json'),
|
||||||
|
method: 'get',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
formatTripReq,
|
||||||
|
};
|
|
@ -195,7 +195,7 @@ const parseHintByCode = (raw) => {
|
||||||
const hint = hintsByCode[raw.key.trim()
|
const hint = hintsByCode[raw.key.trim()
|
||||||
.toLowerCase()];
|
.toLowerCase()];
|
||||||
if (hint) {
|
if (hint) {
|
||||||
return Object.assign({text: raw.value}, hint);
|
return Object.assign({text: raw.value || raw.text}, hint);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,12 +14,12 @@ const parseJourneyLeg = (ctx, pt, date, fallbackLocations) => { // pt = raw leg
|
||||||
const {profile, opt} = ctx;
|
const {profile, opt} = ctx;
|
||||||
|
|
||||||
const res = {
|
const res = {
|
||||||
origin: pt.halte?.length > 0 ? profile.parseLocation(ctx, pt.halte[0]) : locationFallback(pt.abfahrtsOrtExtId, pt.abfahrtsOrt, fallbackLocations),
|
origin: pt.halte?.length > 0 && profile.parseLocation(ctx, pt.halte[0].ort || pt.halte[0]) || pt.abgangsOrt?.name && profile.parseLocation(ctx, pt.abgangsOrt) || locationFallback(pt.abfahrtsOrtExtId, pt.abfahrtsOrt, fallbackLocations),
|
||||||
destination: pt.halte?.length > 0 ? profile.parseLocation(ctx, pt.halte[pt.halte.length - 1]) : locationFallback(pt.ankunftsOrtExtId, pt.ankunftsOrt, fallbackLocations),
|
destination: pt.halte?.length > 0 && profile.parseLocation(ctx, pt.halte[pt.halte.length - 1].ort || pt.halte[pt.halte.length - 1]) || pt.ankunftsOrt?.name && profile.parseLocation(ctx, pt.ankunftsOrt) || locationFallback(pt.ankunftsOrtExtId, pt.ankunftsOrt, fallbackLocations),
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancelledDep = pt.halte?.length > 0 && profile.parseCancelled(pt.halte[0]);
|
const cancelledDep = pt.halte?.length > 0 && profile.parseCancelled(pt.halte[0]);
|
||||||
const dep = profile.parseWhen(ctx, date, pt.abfahrtsZeitpunkt, pt.ezAbfahrtsZeitpunkt, cancelledDep);
|
const dep = profile.parseWhen(ctx, date, pt.abfahrtsZeitpunkt || pt.abgangsDatum || pt.halte?.length > 0 && pt.halte[0].abgangsDatum, pt.ezAbfahrtsZeitpunkt || pt.ezAbgangsDatum || pt.halte?.length > 0 && pt.halte[0].ezAbgangsDatum, cancelledDep);
|
||||||
res.departure = dep.when;
|
res.departure = dep.when;
|
||||||
res.plannedDeparture = dep.plannedWhen;
|
res.plannedDeparture = dep.plannedWhen;
|
||||||
res.departureDelay = dep.delay;
|
res.departureDelay = dep.delay;
|
||||||
|
@ -28,7 +28,7 @@ const parseJourneyLeg = (ctx, pt, date, fallbackLocations) => { // pt = raw leg
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancelledArr = pt.halte?.length > 0 && profile.parseCancelled(pt.halte[pt.halte.length - 1]);
|
const cancelledArr = pt.halte?.length > 0 && profile.parseCancelled(pt.halte[pt.halte.length - 1]);
|
||||||
const arr = profile.parseWhen(ctx, date, pt.ankunftsZeitpunkt, pt.ezAnkunftsZeitpunkt, cancelledArr);
|
const arr = profile.parseWhen(ctx, date, pt.ankunftsZeitpunkt || pt.ankunftsDatum || pt.halte?.length > 0 && pt.halte[pt.halte.length - 1].ankunftsDatum, pt.ezAnkunftsZeitpunkt || pt.ezAnkunftsDatum || pt.halte?.length > 0 && pt.halte[pt.halte.length - 1].ezAkunftsDatum, cancelledArr);
|
||||||
res.arrival = arr.when;
|
res.arrival = arr.when;
|
||||||
res.plannedArrival = arr.plannedWhen;
|
res.plannedArrival = arr.plannedWhen;
|
||||||
res.arrivalDelay = arr.delay;
|
res.arrivalDelay = arr.delay;
|
||||||
|
@ -47,15 +47,24 @@ const parseJourneyLeg = (ctx, pt, date, fallbackLocations) => { // pt = raw leg
|
||||||
res.polyline = profile.parsePolyline(ctx, pt.polylineGroup); // TODO polylines not returned anymore, set "poly": true in request, apparently only works for /reiseloesung/verbindung
|
res.polyline = profile.parsePolyline(ctx, pt.polylineGroup); // TODO polylines not returned anymore, set "poly": true in request, apparently only works for /reiseloesung/verbindung
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pt.verkehrsmittel?.typ === 'WALK') {
|
const type = pt.verkehrsmittel?.typ || pt.typ;
|
||||||
|
if (type == 'WALK' || type == 'FUSSWEG' || type == 'TRANSFER') { // TODO invert default?
|
||||||
|
if (res.origin?.id == res.destination?.id) {
|
||||||
|
res.arrival = res.departure;
|
||||||
|
res.plannedArrival = res.plannedDeparture;
|
||||||
|
res.arrivalDelay = res.departureDelay;
|
||||||
|
}
|
||||||
res.public = true;
|
res.public = true;
|
||||||
res.walking = true;
|
res.walking = true;
|
||||||
res.distance = pt.distanz || null;
|
res.distance = pt.distanz || null;
|
||||||
|
if (type == 'TRANSFER') {
|
||||||
|
res.transfer = true;
|
||||||
|
}
|
||||||
// TODO res.transfer, res.checkin
|
// TODO res.transfer, res.checkin
|
||||||
} else {
|
} else {
|
||||||
res.tripId = pt.journeyId;
|
res.tripId = pt.journeyId || pt.zuglaufId;
|
||||||
res.line = profile.parseLine(ctx, pt) || null;
|
res.line = profile.parseLine(ctx, pt) || null;
|
||||||
res.direction = pt.verkehrsmittel?.richtung || null;
|
res.direction = pt.verkehrsmittel?.richtung || pt.richtung || null;
|
||||||
|
|
||||||
// TODO res.currentLocation
|
// TODO res.currentLocation
|
||||||
if (pt.halte?.length > 0) {
|
if (pt.halte?.length > 0) {
|
||||||
|
@ -89,12 +98,12 @@ const parseJourneyLeg = (ctx, pt, date, fallbackLocations) => { // pt = raw leg
|
||||||
// TODO cycle, alternatives
|
// TODO cycle, alternatives
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cancelledDep || cancelledArr) {
|
if (cancelledDep || cancelledArr || pt.cancelled || pt.canceled) {
|
||||||
res.cancelled = true;
|
res.cancelled = true;
|
||||||
Object.defineProperty(res, 'canceled', {value: true});
|
Object.defineProperty(res, 'canceled', {value: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
const load = profile.parseLoadFactor(opt, pt.auslastungsmeldungen);
|
const load = profile.parseLoadFactor(opt, pt.auslastungsmeldungen || pt.auslastungsInfos);
|
||||||
if (load) {
|
if (load) {
|
||||||
res.loadFactor = load;
|
res.loadFactor = load;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {parseRemarks} from './remarks.js';
|
import {parseRemarks} from './remarks.js';
|
||||||
|
|
||||||
const parseLocationsFromCtxRecon = (ctx, j) => {
|
const parseLocationsFromCtxRecon = (ctx, j) => {
|
||||||
return j.ctxRecon
|
return (j.ctxRecon || j.kontext)
|
||||||
.split('$')
|
.split('$')
|
||||||
.map(e => ctx.profile.parseLocation(ctx, {id: e}))
|
.map(e => ctx.profile.parseLocation(ctx, {id: e}))
|
||||||
.filter(e => e.latitude || e.location?.latitude)
|
.filter(e => e.latitude || e.location?.latitude)
|
||||||
|
@ -12,9 +12,9 @@ const parseLocationsFromCtxRecon = (ctx, j) => {
|
||||||
}, {});
|
}, {});
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseJourney = (ctx, j) => { // j = raw journey
|
const parseJourney = (ctx, jj) => { // j = raw journey
|
||||||
const {profile, opt} = ctx;
|
const {profile, opt} = ctx;
|
||||||
|
const j = jj.verbindung || jj;
|
||||||
const fallbackLocations = parseLocationsFromCtxRecon(ctx, j);
|
const fallbackLocations = parseLocationsFromCtxRecon(ctx, j);
|
||||||
const legs = [];
|
const legs = [];
|
||||||
for (const l of j.verbindungsAbschnitte) {
|
for (const l of j.verbindungsAbschnitte) {
|
||||||
|
@ -25,7 +25,7 @@ const parseJourney = (ctx, j) => { // j = raw journey
|
||||||
const res = {
|
const res = {
|
||||||
type: 'journey',
|
type: 'journey',
|
||||||
legs,
|
legs,
|
||||||
refreshToken: j.ctxRecon || null,
|
refreshToken: j.ctxRecon || j.kontext || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO freq
|
// TODO freq
|
||||||
|
@ -40,8 +40,8 @@ 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);
|
res.price = profile.parsePrice(ctx, jj);
|
||||||
const tickets = profile.parseTickets(ctx, j);
|
const tickets = profile.parseTickets(ctx, jj);
|
||||||
if (tickets) {
|
if (tickets) {
|
||||||
res.tickets = tickets;
|
res.tickets = tickets;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,12 @@ import slugg from 'slugg';
|
||||||
|
|
||||||
const parseLine = (ctx, p) => {
|
const parseLine = (ctx, p) => {
|
||||||
const profile = ctx.profile;
|
const profile = ctx.profile;
|
||||||
const fahrtNr = p.verkehrsmittel?.nummer || p.transport?.number || p.train?.no;
|
const fahrtNr = p.verkehrsmittel?.nummer || p.transport?.number || p.train?.no || p.verkehrsmittelNummer || ((p.mitteltext || '') + ' ').split(' ')[1];
|
||||||
const res = {
|
const res = {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
id: slugg(p.verkehrsmittel?.langText || p.transport?.journeyDescription || p.train && p.train.category + ' ' + p.train.lineName + ' ' + p.train.no || p.langtext || p.mitteltext), // TODO terrible
|
id: slugg(p.verkehrsmittel?.langText || p.transport?.journeyDescription || p.train && p.train.category + ' ' + p.train.lineName + ' ' + p.train.no || p.langtext || p.mitteltext), // TODO terrible
|
||||||
fahrtNr: fahrtNr ? String(fahrtNr) : undefined, // TODO extract from zuglaufId?
|
fahrtNr: String(fahrtNr),
|
||||||
name: p.verkehrsmittel?.name || p.zugName || p.transport?.journeyDescription || p.train && p.train.category + ' ' + p.train.lineName || p.langtext || p.mitteltext,
|
name: p.verkehrsmittel?.langText || p.verkehrsmittel?.name || p.zugName || p.transport?.journeyDescription || p.train && p.train.category + ' ' + p.train.lineName || p.langtext || p.mitteltext,
|
||||||
public: true,
|
public: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ const parseLine = (ctx, p) => {
|
||||||
res.mode = foundProduct?.mode;
|
res.mode = foundProduct?.mode;
|
||||||
res.product = foundProduct?.id;
|
res.product = foundProduct?.id;
|
||||||
|
|
||||||
res.operator = profile.parseOperator(ctx, p.verkehrsmittel?.zugattribute || p.zugattribute); // TODO regio-guide op
|
res.operator = profile.parseOperator(ctx, p.verkehrsmittel?.zugattribute || p.zugattribute || p.attributNotizen); // TODO regio-guide op
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ const parseLoadFactor = (opt, auslastung) => {
|
||||||
? 'KLASSE_1'
|
? 'KLASSE_1'
|
||||||
: 'KLASSE_2';
|
: 'KLASSE_2';
|
||||||
const load = auslastung.find(a => a.klasse === cls)?.stufe;
|
const load = auslastung.find(a => a.klasse === cls)?.stufe;
|
||||||
return load && loadFactors[load.r] || null;
|
return load && loadFactors[load] || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseArrOrDepWithLoadFactor = (ctx, d) => {
|
const parseArrOrDepWithLoadFactor = (ctx, d) => {
|
||||||
|
|
|
@ -28,7 +28,8 @@ const parseLocation = (ctx, l) => {
|
||||||
res.longitude = lid.X / 1000000;
|
res.longitude = lid.X / 1000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (l.type === STATION || l.extId || l.evaNumber || l.evaNo || l.evaNr || lid.A == '1') {
|
// addresses and pois might also have fake evaNr sometimes!
|
||||||
|
if (l.type === STATION || l.extId || l.evaNumber || l.evaNo || lid.A == '1') {
|
||||||
let stop = {
|
let stop = {
|
||||||
type: 'station',
|
type: 'station',
|
||||||
id: res.id,
|
id: res.id,
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import maxBy from 'lodash/maxBy.js';
|
import maxBy from 'lodash/maxBy.js';
|
||||||
|
|
||||||
const parsePolyline = (ctx, p) => { // p = raw polylineGroup
|
const parsePolyline = (ctx, p) => { // p = raw polylineGroup
|
||||||
if (p.polylineDescriptions.length < 1) {
|
const desc = p.polylineDescriptions || p.polylineDesc;
|
||||||
|
if (desc.length < 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const points = maxBy(p.polylineDescriptions, d => d.coordinates.length).coordinates; // TODO initial and final poly?
|
const points = maxBy(desc, d => d.coordinates.length).coordinates; // TODO initial and final poly?
|
||||||
if (points.length === 0) {
|
if (points.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -14,7 +15,7 @@ const parsePolyline = (ctx, p) => { // p = raw polylineGroup
|
||||||
properties: {},
|
properties: {},
|
||||||
geometry: {
|
geometry: {
|
||||||
type: 'Point',
|
type: 'Point',
|
||||||
coordinates: [ll.lng, ll.lat],
|
coordinates: [ll.lng || ll.longitude, ll.lat || ll.latitude],
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,13 @@ const parseStopover = (ctx, st, date) => { // st = raw stopover
|
||||||
const {profile, opt} = ctx;
|
const {profile, opt} = ctx;
|
||||||
|
|
||||||
const cancelled = profile.parseCancelled(st);
|
const cancelled = profile.parseCancelled(st);
|
||||||
const arr = profile.parseWhen(ctx, date, st.ankunftsZeitpunkt, st.ezAnkunftsZeitpunkt, cancelled);
|
const arr = profile.parseWhen(ctx, date, st.ankunftsZeitpunkt || st.ankunftsDatum, st.ezAnkunftsZeitpunkt || st.ezAnkunftsDatum, cancelled);
|
||||||
const arrPl = profile.parsePlatform(ctx, st.gleis, st.ezGleis);
|
const arrPl = profile.parsePlatform(ctx, st.gleis, st.ezGleis);
|
||||||
const dep = profile.parseWhen(ctx, date, st.abfahrtsZeitpunkt, st.ezAbfahrtsZeitpunkt, cancelled);
|
const dep = profile.parseWhen(ctx, date, st.abfahrtsZeitpunkt || st.abgangsDatum, st.ezAbfahrtsZeitpunkt || st.ezAbgangsDatum, cancelled);
|
||||||
const depPl = profile.parsePlatform(ctx, st.gleis, st.ezGleis);
|
const depPl = profile.parsePlatform(ctx, st.gleis, st.ezGleis);
|
||||||
|
|
||||||
const res = {
|
const res = {
|
||||||
stop: profile.parseLocation(ctx, st) || null,
|
stop: profile.parseLocation(ctx, st.ort || st) || null,
|
||||||
arrival: arr.when,
|
arrival: arr.when,
|
||||||
plannedArrival: arr.plannedWhen,
|
plannedArrival: arr.plannedWhen,
|
||||||
arrivalDelay: arr.delay,
|
arrivalDelay: arr.delay,
|
||||||
|
@ -36,8 +36,10 @@ const parseStopover = (ctx, st, date) => { // st = raw stopover
|
||||||
res.prognosedDeparturePlatform = depPl.prognosedPlatform;
|
res.prognosedDeparturePlatform = depPl.prognosedPlatform;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.loadFactor = profile.parseLoadFactor(opt, st.auslastungsmeldungen);
|
const load = profile.parseLoadFactor(opt, st.auslastungsmeldungen || st.auslastungsInfos);
|
||||||
|
if (load) {
|
||||||
|
res.loadFactor = load;
|
||||||
|
}
|
||||||
// mark stations the train passes without stopping
|
// mark stations the train passes without stopping
|
||||||
// TODO risNotizen key text.realtime.stop.exit.disabled?
|
// TODO risNotizen key text.realtime.stop.exit.disabled?
|
||||||
|
|
||||||
|
|
81
parse/tickets.js
Normal file
81
parse/tickets.js
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
|
||||||
|
const parsePrice = (ctx, raw) => {
|
||||||
|
const p = raw.angebotsPreis || raw.angebote?.preise?.gesamt?.ab; // TODO teilpreis
|
||||||
|
if (p?.betrag) {
|
||||||
|
return {
|
||||||
|
amount: p.betrag,
|
||||||
|
currency: p.waehrung,
|
||||||
|
hint: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseTickets = (ctx, j) => {
|
||||||
|
if (!ctx.opt.tickets) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let tickets = undefined;
|
||||||
|
let price = parsePrice(ctx, j);
|
||||||
|
let ang = j.reiseAngebote
|
||||||
|
|| j.angebote?.angebotsCluster?.flatMap(c => c.angebotsSubCluster
|
||||||
|
.flatMap(c => c.angebotsPositionen
|
||||||
|
.flatMap(p => [
|
||||||
|
p.einfacheFahrt?.standard?.reisePosition,
|
||||||
|
p.einfacheFahrt?.upsellEntgelt?.einfacheFahrt?.reisePosition,
|
||||||
|
].filter(p => p)
|
||||||
|
.map(p => {
|
||||||
|
p.reisePosition.teilpreis = Boolean(p.teilpreisInformationen?.length);
|
||||||
|
return p.reisePosition;
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (ang && ang.length > 0) { // if refreshJourney()
|
||||||
|
tickets = ang
|
||||||
|
.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' || s.premium || Boolean(s.nutzungsInformationen?.find(i => i.klasse == 'KLASSE_1')),
|
||||||
|
partialFare: s.teilpreis,
|
||||||
|
};
|
||||||
|
if (s.teilpreis) {
|
||||||
|
p.addData = 'Teilpreis / partial fare';
|
||||||
|
}
|
||||||
|
const conds = s.konditionsAnzeigen || s.konditionen;
|
||||||
|
if (conds) {
|
||||||
|
p.addDataTicketInfo = conds.map(a => a.anzeigeUeberschrift || a.bezeichnung)
|
||||||
|
.join('. ');
|
||||||
|
p.addDataTicketDetails = conds.map(a => a.textLang || a.details)
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
if (s.leuchtturmInfo || s.leuchtturmText) {
|
||||||
|
p.addDataTravelInfo = s.leuchtturmInfo?.text || s.leuchtturmText;
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
});
|
||||||
|
if (ctx.opt.generateUnreliableTicketUrls) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (price) { // if journeys()
|
||||||
|
tickets = [{
|
||||||
|
name: 'from',
|
||||||
|
priceObj: {
|
||||||
|
amount: Math.round(price.amount * 100),
|
||||||
|
currency: price.currency,
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
return tickets;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
parsePrice,
|
||||||
|
parseTickets,
|
||||||
|
};
|
|
@ -6,7 +6,7 @@ const parseTrip = (ctx, t) => { // t = raw trip
|
||||||
trip.id = trip.tripId; // TODO journeyId
|
trip.id = trip.tripId; // TODO journeyId
|
||||||
delete trip.tripId;
|
delete trip.tripId;
|
||||||
delete trip.reachable;
|
delete trip.reachable;
|
||||||
trip.cancelled = t.cancelled;
|
trip.cancelled = profile.parseCancelled(t);
|
||||||
|
|
||||||
// TODO opt.scheduledDays
|
// TODO opt.scheduledDays
|
||||||
return trip;
|
return trip;
|
||||||
|
|
|
@ -24,7 +24,7 @@ const opt = {
|
||||||
products: {},
|
products: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
tap.test('parses a regio-guide departure correctly', (t) => {
|
tap.test('parses a dbnav departure correctly', (t) => {
|
||||||
const ctx = {profile, opt, common: null, res};
|
const ctx = {profile, opt, common: null, res};
|
||||||
const departures = res.bahnhofstafelAbfahrtPositionen.map(d => profile.parseDeparture(ctx, d));
|
const departures = res.bahnhofstafelAbfahrtPositionen.map(d => profile.parseDeparture(ctx, d));
|
||||||
t.same(departures, expected);
|
t.same(departures, expected);
|
||||||
|
|
40
test/dbnav-refresh-journey.js
Normal file
40
test/dbnav-refresh-journey.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
// todo: use import assertions once they're supported by Node.js & ESLint
|
||||||
|
// https://github.com/tc39/proposal-import-assertions
|
||||||
|
import {createRequire} from 'module';
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
|
import tap from 'tap';
|
||||||
|
|
||||||
|
import {createClient} from '../index.js';
|
||||||
|
import {profile as rawProfile} from '../p/db/index.js';
|
||||||
|
const res = require('./fixtures/dbnav-refresh-journey.json');
|
||||||
|
import {dbNavJourney as expected} from './fixtures/dbnav-refresh-journey.js';
|
||||||
|
|
||||||
|
const client = createClient(rawProfile, 'public-transport/hafas-client:test');
|
||||||
|
const {profile} = client;
|
||||||
|
|
||||||
|
const opt = {
|
||||||
|
results: null,
|
||||||
|
via: null,
|
||||||
|
stopovers: false,
|
||||||
|
transfers: -1,
|
||||||
|
transferTime: 0,
|
||||||
|
accessibility: 'none',
|
||||||
|
bike: false,
|
||||||
|
tickets: true,
|
||||||
|
polylines: true,
|
||||||
|
remarks: true,
|
||||||
|
walkingSpeed: 'normal',
|
||||||
|
startWithWalking: true,
|
||||||
|
scheduledDays: false,
|
||||||
|
departure: '2020-04-10T20:33+02:00',
|
||||||
|
products: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
tap.test('parses a refresh journey correctly (DB)', (t) => {
|
||||||
|
const ctx = {profile, opt, common: null, res};
|
||||||
|
const journey = profile.parseJourney(ctx, res);
|
||||||
|
|
||||||
|
t.same(journey, expected.journey);
|
||||||
|
t.end();
|
||||||
|
});
|
|
@ -105,7 +105,6 @@ tap.test('journeys – Berlin Schwedter Str. to München Hbf', async (t) => {
|
||||||
departure: when,
|
departure: when,
|
||||||
stopovers: true,
|
stopovers: true,
|
||||||
});
|
});
|
||||||
console.log('MARK1', JSON.stringify(res));
|
|
||||||
|
|
||||||
await testJourneysStationToStation({
|
await testJourneysStationToStation({
|
||||||
test: t,
|
test: t,
|
||||||
|
|
498
test/e2e/dbnav.js
Normal file
498
test/e2e/dbnav.js
Normal file
|
@ -0,0 +1,498 @@
|
||||||
|
import tap from 'tap';
|
||||||
|
import isRoughlyEqual from 'is-roughly-equal';
|
||||||
|
|
||||||
|
import {createWhen} from './lib/util.js';
|
||||||
|
import {createClient} from '../../index.js';
|
||||||
|
import {profile as dbProfile} from '../../p/dbnav/index.js';
|
||||||
|
import {
|
||||||
|
createValidateStation,
|
||||||
|
createValidateTrip,
|
||||||
|
} from './lib/validators.js';
|
||||||
|
import {createValidateFptfWith as createValidate} from './lib/validate-fptf-with.js';
|
||||||
|
import {testJourneysStationToStation} from './lib/journeys-station-to-station.js';
|
||||||
|
import {testJourneysStationToAddress} from './lib/journeys-station-to-address.js';
|
||||||
|
import {testJourneysStationToPoi} from './lib/journeys-station-to-poi.js';
|
||||||
|
import {testEarlierLaterJourneys} from './lib/earlier-later-journeys.js';
|
||||||
|
import {testLegCycleAlternatives} from './lib/leg-cycle-alternatives.js';
|
||||||
|
import {testRefreshJourney} from './lib/refresh-journey.js';
|
||||||
|
import {journeysFailsWithNoProduct} from './lib/journeys-fails-with-no-product.js';
|
||||||
|
import {testDepartures} from './lib/departures.js';
|
||||||
|
import {testArrivals} from './lib/arrivals.js';
|
||||||
|
import {testJourneysWithDetour} from './lib/journeys-with-detour.js';
|
||||||
|
|
||||||
|
const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o);
|
||||||
|
const minute = 60 * 1000;
|
||||||
|
|
||||||
|
const T_MOCK = 1747040400 * 1000; // 2025-05-12T08:00:00+01:00
|
||||||
|
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
||||||
|
|
||||||
|
const cfg = {
|
||||||
|
when,
|
||||||
|
stationCoordsOptional: true, // TODO
|
||||||
|
products: dbProfile.products,
|
||||||
|
minLatitude: 46.673100,
|
||||||
|
maxLatitude: 55.030671,
|
||||||
|
minLongitude: 6.896517,
|
||||||
|
maxLongitude: 16.180237,
|
||||||
|
};
|
||||||
|
|
||||||
|
const validate = createValidate(cfg);
|
||||||
|
|
||||||
|
const assertValidPrice = (t, p) => {
|
||||||
|
t.ok(p);
|
||||||
|
if (p.amount !== null) {
|
||||||
|
t.equal(typeof p.amount, 'number');
|
||||||
|
t.ok(p.amount > 0);
|
||||||
|
}
|
||||||
|
if (p.hint !== null) {
|
||||||
|
t.equal(typeof p.hint, 'string');
|
||||||
|
t.ok(p.hint);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertValidTickets = (test, tickets) => {
|
||||||
|
test.ok(Array.isArray(tickets));
|
||||||
|
for (let fare of tickets) {
|
||||||
|
test.equal(typeof fare.name, 'string', 'Mandatory field "name" is missing or not a string');
|
||||||
|
test.ok(fare.name);
|
||||||
|
|
||||||
|
test.ok(isObj(fare.priceObj), 'Mandatory field "priceObj" is missing or not an object');
|
||||||
|
test.equal(typeof fare.priceObj.amount, 'number', 'Mandatory field "amount" in "priceObj" is missing or not a number');
|
||||||
|
test.ok(fare.priceObj.amount > 0);
|
||||||
|
if ('currency' in fare.priceObj) {
|
||||||
|
test.equal(typeof fare.priceObj.currency, 'string');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check optional fields
|
||||||
|
if ('addData' in fare) {
|
||||||
|
test.equal(typeof fare.addData, 'string');
|
||||||
|
}
|
||||||
|
if ('addDataTicketInfo' in fare) {
|
||||||
|
test.equal(typeof fare.addDataTicketInfo, 'string');
|
||||||
|
}
|
||||||
|
if ('addDataTicketDetails' in fare) {
|
||||||
|
test.equal(typeof fare.addDataTicketDetails, 'string');
|
||||||
|
}
|
||||||
|
if ('addDataTravelInfo' in fare) {
|
||||||
|
test.equal(typeof fare.addDataTravelInfo, 'string');
|
||||||
|
}
|
||||||
|
if ('addDataTravelDetails' in fare) {
|
||||||
|
test.equal(typeof fare.firstClass, 'boolean');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = createClient(dbProfile, 'public-transport/hafas-client:test');
|
||||||
|
|
||||||
|
const berlinHbf = '8011160';
|
||||||
|
const münchenHbf = '8000261';
|
||||||
|
const jungfernheide = '8011167';
|
||||||
|
const blnSchwedterStr = '732652';
|
||||||
|
const westhafen = '8089116';
|
||||||
|
const wedding = '8089131';
|
||||||
|
const württembergallee = '731084';
|
||||||
|
const regensburgHbf = '8000309';
|
||||||
|
const blnOstbahnhof = '8010255';
|
||||||
|
const blnTiergarten = '8089091';
|
||||||
|
const blnJannowitzbrücke = '8089019';
|
||||||
|
const potsdamHbf = '8012666';
|
||||||
|
const berlinSüdkreuz = '8011113';
|
||||||
|
const kölnHbf = '8000207';
|
||||||
|
|
||||||
|
tap.test('journeys – Berlin Schwedter Str. to München Hbf', async (t) => {
|
||||||
|
const res = await client.journeys(blnSchwedterStr, münchenHbf, {
|
||||||
|
results: 4,
|
||||||
|
departure: when,
|
||||||
|
stopovers: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await testJourneysStationToStation({
|
||||||
|
test: t,
|
||||||
|
res,
|
||||||
|
validate,
|
||||||
|
fromId: blnSchwedterStr,
|
||||||
|
toId: münchenHbf,
|
||||||
|
});
|
||||||
|
// todo: find a journey where there pricing info is always available
|
||||||
|
for (let journey of res.journeys) {
|
||||||
|
if (journey.price) {
|
||||||
|
assertValidPrice(t, journey.price);
|
||||||
|
}
|
||||||
|
if (journey.tickets) {
|
||||||
|
assertValidTickets(t, journey.tickets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('refreshJourney – valid tickets', async (t) => {
|
||||||
|
const T_MOCK = 1710831600 * 1000; // 2024-03-19T08:00:00+01:00
|
||||||
|
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
||||||
|
|
||||||
|
const journeysRes = await client.journeys(berlinHbf, münchenHbf, {
|
||||||
|
results: 4,
|
||||||
|
departure: when,
|
||||||
|
stopovers: true,
|
||||||
|
});
|
||||||
|
const refreshWithoutTicketsRes = await client.refreshJourney(journeysRes.journeys[0].refreshToken, {
|
||||||
|
tickets: false,
|
||||||
|
});
|
||||||
|
const refreshWithTicketsRes = await client.refreshJourney(journeysRes.journeys[0].refreshToken, {
|
||||||
|
tickets: true,
|
||||||
|
});
|
||||||
|
for (let res of [refreshWithoutTicketsRes, refreshWithTicketsRes]) {
|
||||||
|
if (res.journey.tickets !== undefined) {
|
||||||
|
assertValidTickets(t, res.journey.tickets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
// todo: journeys, only one product
|
||||||
|
|
||||||
|
tap.test('journeys – fails with no product', async (t) => {
|
||||||
|
await journeysFailsWithNoProduct({
|
||||||
|
test: t,
|
||||||
|
fetchJourneys: client.journeys,
|
||||||
|
fromId: blnSchwedterStr,
|
||||||
|
toId: münchenHbf,
|
||||||
|
when,
|
||||||
|
products: dbProfile.products,
|
||||||
|
});
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Berlin Schwedter Str. to Torfstraße 17', async (t) => {
|
||||||
|
const torfstr = {
|
||||||
|
type: 'location',
|
||||||
|
address: 'Torfstraße 17',
|
||||||
|
latitude: 52.5416823,
|
||||||
|
longitude: 13.3491223,
|
||||||
|
};
|
||||||
|
const res = await client.journeys(blnSchwedterStr, torfstr, {
|
||||||
|
results: 3,
|
||||||
|
departure: when,
|
||||||
|
});
|
||||||
|
|
||||||
|
await testJourneysStationToAddress({
|
||||||
|
test: t,
|
||||||
|
res,
|
||||||
|
validate,
|
||||||
|
fromId: blnSchwedterStr,
|
||||||
|
to: torfstr,
|
||||||
|
});
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Berlin Schwedter Str. to ATZE Musiktheater', async (t) => {
|
||||||
|
const atze = {
|
||||||
|
type: 'location',
|
||||||
|
id: '991598902',
|
||||||
|
poi: true,
|
||||||
|
name: 'Berlin, Atze Musiktheater für Kinder (Kultur und U',
|
||||||
|
latitude: 52.542417,
|
||||||
|
longitude: 13.350437,
|
||||||
|
};
|
||||||
|
const res = await client.journeys(blnSchwedterStr, atze, {
|
||||||
|
results: 3,
|
||||||
|
departure: when,
|
||||||
|
});
|
||||||
|
|
||||||
|
await testJourneysStationToPoi({
|
||||||
|
test: t,
|
||||||
|
res,
|
||||||
|
validate,
|
||||||
|
fromId: blnSchwedterStr,
|
||||||
|
to: atze,
|
||||||
|
});
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('journeys: via works – with detour', async (t) => {
|
||||||
|
// Going from Westhafen to Wedding via Württembergalle without detour
|
||||||
|
// is currently impossible. We check if the routing engine computes a detour.
|
||||||
|
const res = await client.journeys(westhafen, wedding, {
|
||||||
|
via: württembergallee,
|
||||||
|
results: 1,
|
||||||
|
departure: when,
|
||||||
|
stopovers: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await testJourneysWithDetour({
|
||||||
|
test: t,
|
||||||
|
res,
|
||||||
|
validate,
|
||||||
|
detourIds: [württembergallee],
|
||||||
|
});
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
// todo: walkingSpeed "Berlin - Charlottenburg, Hallerstraße" -> jungfernheide
|
||||||
|
// todo: without detour
|
||||||
|
|
||||||
|
|
||||||
|
// todo: with the DB endpoint, earlierRef/laterRef is missing queries many days in the future
|
||||||
|
tap.skip('earlier/later journeys, Jungfernheide -> München Hbf', async (t) => {
|
||||||
|
await testEarlierLaterJourneys({
|
||||||
|
test: t,
|
||||||
|
fetchJourneys: client.journeys,
|
||||||
|
validate,
|
||||||
|
fromId: jungfernheide,
|
||||||
|
toId: münchenHbf,
|
||||||
|
when,
|
||||||
|
});
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!process.env.VCR_MODE) {
|
||||||
|
tap.test('journeys – leg cycle & alternatives', async (t) => {
|
||||||
|
await testLegCycleAlternatives({
|
||||||
|
test: t,
|
||||||
|
fetchJourneys: client.journeys,
|
||||||
|
fromId: blnTiergarten,
|
||||||
|
toId: blnJannowitzbrücke,
|
||||||
|
when,
|
||||||
|
});
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tap.test('refreshJourney', async (t) => {
|
||||||
|
const T_MOCK = 1710831600 * 1000; // 2024-03-19T08:00:00+01:00
|
||||||
|
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
|
||||||
|
const validate = createValidate({...cfg, when});
|
||||||
|
|
||||||
|
await testRefreshJourney({
|
||||||
|
test: t,
|
||||||
|
fetchJourneys: client.journeys,
|
||||||
|
refreshJourney: client.refreshJourney,
|
||||||
|
validate,
|
||||||
|
fromId: jungfernheide,
|
||||||
|
toId: münchenHbf,
|
||||||
|
when,
|
||||||
|
});
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
tap.skip('journeysFromTrip – U Mehringdamm to U Naturkundemuseum, reroute to Spittelmarkt.', async (t) => {
|
||||||
|
const blnMehringdamm = '730939';
|
||||||
|
const blnStadtmitte = '732541';
|
||||||
|
const blnNaturkundemuseum = '732539';
|
||||||
|
const blnSpittelmarkt = '732543';
|
||||||
|
|
||||||
|
const isU6Leg = leg => leg.line && leg.line.name
|
||||||
|
&& leg.line.name.toUpperCase()
|
||||||
|
.replace(/\s+/g, '') === 'U6';
|
||||||
|
|
||||||
|
const sameStopOrStation = (stopA) => (stopB) => {
|
||||||
|
if (stopA.id && stopB.id && stopA.id === stopB.id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const statA = stopA.stat && stopA.stat.id || NaN;
|
||||||
|
const statB = stopB.stat && stopB.stat.id || NaN;
|
||||||
|
return statA === statB || stopA.id === statB || stopB.id === statA;
|
||||||
|
};
|
||||||
|
const departureOf = st => Number(new Date(st.departure || st.scheduledDeparture));
|
||||||
|
const arrivalOf = st => Number(new Date(st.arrival || st.scheduledArrival));
|
||||||
|
|
||||||
|
// `journeysFromTrip` only supports queries *right now*, so we can't use `when` as in all
|
||||||
|
// other tests. To make the test less brittle, we pick a connection that is served all night. 🙄
|
||||||
|
const when = new Date();
|
||||||
|
const validate = createValidate({...cfg, when});
|
||||||
|
|
||||||
|
const findTripBetween = async (stopAId, stopBId, products = {}) => {
|
||||||
|
const {journeys} = await client.journeys(stopAId, stopBId, {
|
||||||
|
departure: new Date(when - 10 * minute),
|
||||||
|
transfers: 0, products,
|
||||||
|
results: 8, stopovers: false, remarks: false,
|
||||||
|
});
|
||||||
|
for (const j of journeys) {
|
||||||
|
const l = j.legs.find(isU6Leg);
|
||||||
|
if (!l) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const t = await client.trip(l.tripId, {
|
||||||
|
stopovers: true, remarks: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pastStopovers = t.stopovers
|
||||||
|
.filter(st => departureOf(st) < Date.now()); // todo: <= ?
|
||||||
|
const hasStoppedAtA = pastStopovers
|
||||||
|
.find(sameStopOrStation({id: stopAId}));
|
||||||
|
const willStopAtB = t.stopovers
|
||||||
|
.filter(st => arrivalOf(st) > Date.now()) // todo: >= ?
|
||||||
|
.find(sameStopOrStation({id: stopBId}));
|
||||||
|
|
||||||
|
if (hasStoppedAtA && willStopAtB) {
|
||||||
|
const prevStopover = maxBy(pastStopovers, departureOf);
|
||||||
|
return {trip: t, prevStopover};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {trip: null, prevStopover: null};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find a vehicle from U Mehringdamm to U Stadtmitte (to the north) that is currently
|
||||||
|
// between these two stations.
|
||||||
|
const {trip, prevStopover} = await findTripBetween(blnMehringdamm, blnStadtmitte, {
|
||||||
|
regionalExpress: false, regional: false, suburban: false,
|
||||||
|
});
|
||||||
|
t.ok(trip, 'precondition failed: trip not found');
|
||||||
|
t.ok(prevStopover, 'precondition failed: previous stopover missing');
|
||||||
|
|
||||||
|
// todo: "Error: Suche aus dem Zug: Vor Abfahrt des Zuges"
|
||||||
|
const newJourneys = await client.journeysFromTrip(trip.id, prevStopover, blnSpittelmarkt, {
|
||||||
|
results: 3, stopovers: true, remarks: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate with fake prices.
|
||||||
|
const withFakePrice = (j) => {
|
||||||
|
const clone = Object.assign({}, j);
|
||||||
|
clone.price = {amount: 123, currency: 'EUR'};
|
||||||
|
return clone;
|
||||||
|
};
|
||||||
|
// todo: there is no such validator!
|
||||||
|
validate(t, newJourneys.map(withFakePrice), 'journeysFromTrip', 'newJourneys');
|
||||||
|
|
||||||
|
for (let i = 0; i < newJourneys.length; i++) {
|
||||||
|
const j = newJourneys[i];
|
||||||
|
const n = `newJourneys[${i}]`;
|
||||||
|
|
||||||
|
const legOnTrip = j.legs.find(l => l.tripId === trip.id);
|
||||||
|
t.ok(legOnTrip, n + ': leg with trip ID not found');
|
||||||
|
t.equal(last(legOnTrip.stopovers).stop.id, blnStadtmitte);
|
||||||
|
}
|
||||||
|
});*/
|
||||||
|
|
||||||
|
tap.test('trip details', async (t) => {
|
||||||
|
const res = await client.journeys(berlinHbf, münchenHbf, {
|
||||||
|
results: 1, departure: when,
|
||||||
|
});
|
||||||
|
|
||||||
|
const p = res.journeys[0].legs.find(l => !l.walking);
|
||||||
|
t.ok(p.tripId, 'precondition failed');
|
||||||
|
t.ok(p.line.name, 'precondition failed');
|
||||||
|
|
||||||
|
const tripRes = await client.trip(p.tripId, {when});
|
||||||
|
|
||||||
|
const validate = createValidate(cfg, {
|
||||||
|
trip: (cfg) => {
|
||||||
|
const validateTrip = createValidateTrip(cfg);
|
||||||
|
const validateTripWithFakeDirection = (val, trip, name) => {
|
||||||
|
validateTrip(val, {
|
||||||
|
...trip,
|
||||||
|
direction: trip.direction || 'foo', // todo, see #49
|
||||||
|
}, name);
|
||||||
|
};
|
||||||
|
return validateTripWithFakeDirection;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
validate(t, tripRes, 'tripResult', 'tripRes');
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('departures at Berlin Schwedter Str.', async (t) => {
|
||||||
|
const res = await client.departures(blnSchwedterStr, {
|
||||||
|
duration: 5, when,
|
||||||
|
});
|
||||||
|
|
||||||
|
await testDepartures({
|
||||||
|
test: t,
|
||||||
|
res,
|
||||||
|
validate,
|
||||||
|
id: blnSchwedterStr,
|
||||||
|
});
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('departures with station object', async (t) => {
|
||||||
|
const res = await client.departures({
|
||||||
|
type: 'station',
|
||||||
|
id: jungfernheide,
|
||||||
|
name: 'Berlin Jungfernheide',
|
||||||
|
location: {
|
||||||
|
type: 'location',
|
||||||
|
latitude: 1.23,
|
||||||
|
longitude: 2.34,
|
||||||
|
},
|
||||||
|
}, {when});
|
||||||
|
|
||||||
|
validate(t, res, 'departuresResponse', 'res');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('arrivals at Berlin Schwedter Str.', async (t) => {
|
||||||
|
const res = await client.arrivals(blnSchwedterStr, {
|
||||||
|
duration: 5, when,
|
||||||
|
});
|
||||||
|
|
||||||
|
await testArrivals({
|
||||||
|
test: t,
|
||||||
|
res,
|
||||||
|
validate,
|
||||||
|
id: blnSchwedterStr,
|
||||||
|
});
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('nearby Berlin Jungfernheide', async (t) => {
|
||||||
|
const nearby = await client.nearby({
|
||||||
|
type: 'location',
|
||||||
|
latitude: 52.530273,
|
||||||
|
longitude: 13.299433,
|
||||||
|
}, {
|
||||||
|
results: 2, distance: 400,
|
||||||
|
});
|
||||||
|
|
||||||
|
validate(t, nearby, 'locations', 'nearby');
|
||||||
|
|
||||||
|
t.equal(nearby.length, 2);
|
||||||
|
|
||||||
|
const s0 = nearby[0];
|
||||||
|
t.equal(s0.id, jungfernheide);
|
||||||
|
t.equal(s0.name, 'Berlin Jungfernheide');
|
||||||
|
t.ok(isRoughlyEqual(0.0005, s0.location.latitude, 52.530408));
|
||||||
|
t.ok(isRoughlyEqual(0.0005, s0.location.longitude, 13.299424));
|
||||||
|
t.ok(s0.distance >= 0);
|
||||||
|
t.ok(s0.distance <= 100);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('locations named Jungfernheide', async (t) => {
|
||||||
|
const locations = await client.locations('Jungfernheide', {
|
||||||
|
results: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
validate(t, locations, 'locations', 'locations');
|
||||||
|
t.ok(locations.length <= 10);
|
||||||
|
t.ok(locations.some((l) => {
|
||||||
|
return l.station && l.station.id === jungfernheide || l.id === jungfernheide;
|
||||||
|
}), 'Jungfernheide not found');
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
tap.test('stop', async (t) => {
|
||||||
|
const s = await client.stop(regensburgHbf);
|
||||||
|
|
||||||
|
validate(t, s, ['stop', 'station'], 'stop');
|
||||||
|
t.equal(s.id, regensburgHbf);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('line with additionalName', async (t) => {
|
||||||
|
const {departures} = await client.departures(potsdamHbf, {
|
||||||
|
when,
|
||||||
|
duration: 12 * 60, // 12 minutes
|
||||||
|
products: {bus: false, suburban: false, tram: false},
|
||||||
|
});
|
||||||
|
t.ok(departures.some(d => d.line && d.line.additionalName));
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
*/
|
File diff suppressed because one or more lines are too long
2
test/fixtures/db-refresh-journey.js
vendored
2
test/fixtures/db-refresh-journey.js
vendored
|
@ -84,6 +84,7 @@ const dbJourney = {
|
||||||
type: 'hint',
|
type: 'hint',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
loadFactor: 'low-to-medium',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
origin: {
|
origin: {
|
||||||
|
@ -167,6 +168,7 @@ const dbJourney = {
|
||||||
summary: 'Intercity 2',
|
summary: 'Intercity 2',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
loadFactor: 'low-to-medium',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
refreshToken: 'T$A=1@O=Berlin Hbf@X=13369549@Y=52525589@L=8011160@a=128@$A=1@O=Dortmund Hbf@X=7459294@Y=51517899@L=8000080@a=128@$202412180022$202412180521$ICE 101$$1$$$$$$§T$A=1@O=Dortmund Hbf@X=7459294@Y=51517899@L=8000080@a=128@$A=1@O=Köln Hbf@X=6958730@Y=50943029@L=8000207@a=128@$202412180536$202412180647$IC 2040$$1$$$$$$',
|
refreshToken: 'T$A=1@O=Berlin Hbf@X=13369549@Y=52525589@L=8011160@a=128@$A=1@O=Dortmund Hbf@X=7459294@Y=51517899@L=8000080@a=128@$202412180022$202412180521$ICE 101$$1$$$$$$§T$A=1@O=Dortmund Hbf@X=7459294@Y=51517899@L=8000080@a=128@$A=1@O=Köln Hbf@X=6958730@Y=50943029@L=8000207@a=128@$202412180536$202412180647$IC 2040$$1$$$$$$',
|
||||||
|
|
12
test/fixtures/dbnav-departures.js
vendored
12
test/fixtures/dbnav-departures.js
vendored
|
@ -23,7 +23,7 @@ const dbnavDepartures = [
|
||||||
type: 'line',
|
type: 'line',
|
||||||
id: 'ruf-9870',
|
id: 'ruf-9870',
|
||||||
name: 'RUF 9870',
|
name: 'RUF 9870',
|
||||||
fahrtNr: undefined,
|
fahrtNr: '9870',
|
||||||
public: true,
|
public: true,
|
||||||
productName: 'RUF',
|
productName: 'RUF',
|
||||||
mode: 'taxi',
|
mode: 'taxi',
|
||||||
|
@ -58,7 +58,7 @@ const dbnavDepartures = [
|
||||||
type: 'line',
|
type: 'line',
|
||||||
id: 'rb-82',
|
id: 'rb-82',
|
||||||
name: 'RB 82',
|
name: 'RB 82',
|
||||||
fahrtNr: undefined,
|
fahrtNr: '82',
|
||||||
public: true,
|
public: true,
|
||||||
productName: 'RB',
|
productName: 'RB',
|
||||||
mode: 'train',
|
mode: 'train',
|
||||||
|
@ -93,7 +93,7 @@ const dbnavDepartures = [
|
||||||
type: 'line',
|
type: 'line',
|
||||||
id: 'bus-807',
|
id: 'bus-807',
|
||||||
name: 'Bus 807',
|
name: 'Bus 807',
|
||||||
fahrtNr: undefined,
|
fahrtNr: '807',
|
||||||
public: true,
|
public: true,
|
||||||
productName: 'Bus',
|
productName: 'Bus',
|
||||||
mode: 'bus',
|
mode: 'bus',
|
||||||
|
@ -128,7 +128,7 @@ const dbnavDepartures = [
|
||||||
type: 'line',
|
type: 'line',
|
||||||
id: 'bus-807',
|
id: 'bus-807',
|
||||||
name: 'Bus 807',
|
name: 'Bus 807',
|
||||||
fahrtNr: undefined,
|
fahrtNr: '807',
|
||||||
public: true,
|
public: true,
|
||||||
productName: 'Bus',
|
productName: 'Bus',
|
||||||
mode: 'bus',
|
mode: 'bus',
|
||||||
|
@ -163,7 +163,7 @@ const dbnavDepartures = [
|
||||||
type: 'line',
|
type: 'line',
|
||||||
id: 'bus-807',
|
id: 'bus-807',
|
||||||
name: 'Bus 807',
|
name: 'Bus 807',
|
||||||
fahrtNr: undefined,
|
fahrtNr: '807',
|
||||||
public: true,
|
public: true,
|
||||||
productName: 'Bus',
|
productName: 'Bus',
|
||||||
mode: 'bus',
|
mode: 'bus',
|
||||||
|
@ -200,7 +200,7 @@ const dbnavDepartures = [
|
||||||
type: 'line',
|
type: 'line',
|
||||||
id: 'bus-817',
|
id: 'bus-817',
|
||||||
name: 'Bus 817',
|
name: 'Bus 817',
|
||||||
fahrtNr: undefined,
|
fahrtNr: '817',
|
||||||
public: true,
|
public: true,
|
||||||
productName: 'Bus',
|
productName: 'Bus',
|
||||||
mode: 'bus',
|
mode: 'bus',
|
||||||
|
|
290
test/fixtures/dbnav-refresh-journey.js
vendored
Normal file
290
test/fixtures/dbnav-refresh-journey.js
vendored
Normal file
|
@ -0,0 +1,290 @@
|
||||||
|
const dbNavJourney = {
|
||||||
|
journey: {
|
||||||
|
type: 'journey',
|
||||||
|
legs: [
|
||||||
|
{
|
||||||
|
origin: {
|
||||||
|
type: 'station',
|
||||||
|
id: '8000105',
|
||||||
|
name: 'Frankfurt(Main)Hbf',
|
||||||
|
location: {
|
||||||
|
type: 'location',
|
||||||
|
id: '8000105',
|
||||||
|
latitude: 50.106815,
|
||||||
|
longitude: 8.663003,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
destination: {
|
||||||
|
type: 'station',
|
||||||
|
id: '8000240',
|
||||||
|
name: 'Mainz Hbf',
|
||||||
|
location: {
|
||||||
|
type: 'location',
|
||||||
|
id: '8000240',
|
||||||
|
latitude: 50.00124,
|
||||||
|
longitude: 8.258453,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
departure: '2025-01-03T17:56:00+01:00',
|
||||||
|
plannedDeparture: '2025-01-03T17:56:00+01:00',
|
||||||
|
departureDelay: null,
|
||||||
|
arrival: '2025-01-03T18:30:00+01:00',
|
||||||
|
plannedArrival: '2025-01-03T18:30:00+01:00',
|
||||||
|
arrivalDelay: null,
|
||||||
|
tripId: '2|#VN#1#ST#1735585219#PI#1#ZI#888642#TA#1#DA#30125#1S#8000105#1T#1756#LS#8000007#LT#1921#PU#81#RT#1#CA#DPN#ZE#29266#ZB#RB 29266#PC#3#FR#8000105#FT#1756#TO#8000007#TT#1921#',
|
||||||
|
line: {
|
||||||
|
type: 'line',
|
||||||
|
id: 'rb-31-29266',
|
||||||
|
fahrtNr: '29266',
|
||||||
|
name: 'RB 31 (29266)',
|
||||||
|
public: true,
|
||||||
|
productName: 'RB',
|
||||||
|
mode: 'train',
|
||||||
|
product: 'regional',
|
||||||
|
operator: {
|
||||||
|
type: 'operator',
|
||||||
|
id: 'vlexx',
|
||||||
|
name: 'vlexx',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
direction: 'Alzey',
|
||||||
|
arrivalPlatform: '6a',
|
||||||
|
plannedArrivalPlatform: '6a',
|
||||||
|
departurePlatform: '18',
|
||||||
|
plannedDeparturePlatform: '18',
|
||||||
|
remarks: [
|
||||||
|
{
|
||||||
|
text: 'Number of bicycles conveyed limited',
|
||||||
|
type: 'hint',
|
||||||
|
code: 'bicycle-conveyance',
|
||||||
|
summary: 'bicycles conveyed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Behindertengerechtes Fahrzeug',
|
||||||
|
type: 'hint',
|
||||||
|
code: 'barrier-free-vehicle',
|
||||||
|
summary: 'barrier-free vehicle',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'power sockets for laptop',
|
||||||
|
type: 'hint',
|
||||||
|
code: 'power-sockets',
|
||||||
|
summary: 'power sockets available',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'air conditioning',
|
||||||
|
type: 'hint',
|
||||||
|
code: 'air-conditioned',
|
||||||
|
summary: 'air-conditioned vehicle',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
origin: {
|
||||||
|
type: 'station',
|
||||||
|
id: '8000240',
|
||||||
|
name: 'Mainz Hbf',
|
||||||
|
location: {
|
||||||
|
type: 'location',
|
||||||
|
id: '8000240',
|
||||||
|
latitude: 50.00124,
|
||||||
|
longitude: 8.258453,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
destination: {
|
||||||
|
type: 'station',
|
||||||
|
id: '8000240',
|
||||||
|
name: 'Mainz Hbf',
|
||||||
|
location: {
|
||||||
|
type: 'location',
|
||||||
|
id: '8000240',
|
||||||
|
latitude: 50.00124,
|
||||||
|
longitude: 8.258453,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
departure: '2025-01-03T18:30:00+01:00',
|
||||||
|
plannedDeparture: '2025-01-03T18:30:00+01:00',
|
||||||
|
departureDelay: null,
|
||||||
|
arrival: '2025-01-03T18:30:00+01:00',
|
||||||
|
plannedArrival: '2025-01-03T18:30:00+01:00',
|
||||||
|
arrivalDelay: null,
|
||||||
|
public: true,
|
||||||
|
walking: true,
|
||||||
|
distance: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
origin: {
|
||||||
|
type: 'station',
|
||||||
|
id: '8000240',
|
||||||
|
name: 'Mainz Hbf',
|
||||||
|
location: {
|
||||||
|
type: 'location',
|
||||||
|
id: '8000240',
|
||||||
|
latitude: 50.00124,
|
||||||
|
longitude: 8.258453,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
destination: {
|
||||||
|
type: 'station',
|
||||||
|
id: '8000096',
|
||||||
|
name: 'Stuttgart Hbf',
|
||||||
|
location: {
|
||||||
|
type: 'location',
|
||||||
|
id: '8000096',
|
||||||
|
latitude: 48.785053,
|
||||||
|
longitude: 9.182589,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
departure: '2025-01-03T18:42:00+01:00',
|
||||||
|
plannedDeparture: '2025-01-03T18:42:00+01:00',
|
||||||
|
departureDelay: null,
|
||||||
|
arrival: '2025-01-03T20:23:00+01:00',
|
||||||
|
plannedArrival: '2025-01-03T20:23:00+01:00',
|
||||||
|
arrivalDelay: null,
|
||||||
|
tripId: '2|#VN#1#ST#1735585219#PI#1#ZI#193906#TA#0#DA#30125#1S#8010085#1T#845#LS#8000141#LT#2130#PU#81#RT#1#CA#IC#ZE#2048#ZB#IC 2048#PC#1#FR#8010085#FT#845#TO#8000141#TT#2130#',
|
||||||
|
line: {
|
||||||
|
type: 'line',
|
||||||
|
id: 'ic-2048',
|
||||||
|
fahrtNr: '2048',
|
||||||
|
name: 'IC 2048',
|
||||||
|
public: true,
|
||||||
|
productName: 'IC',
|
||||||
|
mode: 'train',
|
||||||
|
product: 'national',
|
||||||
|
operator: {
|
||||||
|
type: 'operator',
|
||||||
|
id: 'db-fernverkehr-ag',
|
||||||
|
name: 'DB Fernverkehr AG',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
direction: 'Tübingen Hbf',
|
||||||
|
arrivalPlatform: '13',
|
||||||
|
plannedArrivalPlatform: '13',
|
||||||
|
departurePlatform: '5a/b',
|
||||||
|
plannedDeparturePlatform: '5a/b',
|
||||||
|
remarks: [
|
||||||
|
{
|
||||||
|
text: 'Komfort Check-in possible (visit bahn.de/kci for more information)',
|
||||||
|
type: 'hint',
|
||||||
|
code: 'komfort-checkin',
|
||||||
|
summary: 'Komfort-Checkin available',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Bicycles conveyed - subject to reservation',
|
||||||
|
type: 'hint',
|
||||||
|
code: 'bicycle-conveyance-reservation',
|
||||||
|
summary: 'bicycles conveyed, subject to reservation',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Number of bicycles conveyed limited',
|
||||||
|
type: 'hint',
|
||||||
|
code: 'bicycle-conveyance',
|
||||||
|
summary: 'bicycles conveyed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Food and beverages served at seat',
|
||||||
|
type: 'hint',
|
||||||
|
code: 'snacks',
|
||||||
|
summary: 'snacks available for purchase at the seat',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'RZ',
|
||||||
|
summary: 'Einstieg mit Rollstuhl stufenfrei',
|
||||||
|
text: 'Einstieg mit Rollstuhl stufenfrei',
|
||||||
|
type: 'hint',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Intercity 2: visit www.bahn.de/ic2 for more information',
|
||||||
|
type: 'hint',
|
||||||
|
code: 'intercity-2',
|
||||||
|
summary: 'Intercity 2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
loadFactor: 'high',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
refreshToken: 'T$A=1@O=Frankfurt(Main)Hbf@X=8663785@Y=50107149@L=8000105@a=128@$A=1@O=Mainz Hbf@X=8258723@Y=50001113@L=8000240@a=128@$202501031756$202501031830$RB 29266$$1$$$$$$§T$A=1@O=Mainz Hbf@X=8258723@Y=50001113@L=8000240@a=128@$A=1@O=Stuttgart Hbf@X=9181636@Y=48784081@L=8000096@a=128@$202501031842$202501032023$IC 2048$$1$$$$$$',
|
||||||
|
remarks: [],
|
||||||
|
price: {
|
||||||
|
amount: 43.99,
|
||||||
|
currency: 'EUR',
|
||||||
|
hint: null,
|
||||||
|
},
|
||||||
|
tickets: [
|
||||||
|
{
|
||||||
|
name: 'Super Sparpreis',
|
||||||
|
priceObj: {
|
||||||
|
amount: 4399,
|
||||||
|
currency: 'EUR',
|
||||||
|
},
|
||||||
|
firstClass: false,
|
||||||
|
partialFare: false,
|
||||||
|
addDataTicketInfo: 'Train-specific travel. No cancellations. No City-Ticket',
|
||||||
|
addDataTicketDetails: 'You can use all trains indicated on your ticket. Your ticket constitutes a continuous contract of carriage in each direction (through ticket). Should you make a passenger rights claim, the ticket will be considered in its entirety. Special rules apply to tickets including City-Ticket, see there. Your ticket cannot be cancelled. Your ticket does not include a City-Ticket. The City-Ticket is issued together with your Super Sparpreis or Sparpreis ticket, depending on the journey you have booked.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Super Sparpreis',
|
||||||
|
priceObj: {
|
||||||
|
amount: 5299,
|
||||||
|
currency: 'EUR',
|
||||||
|
},
|
||||||
|
firstClass: true,
|
||||||
|
partialFare: false,
|
||||||
|
addDataTicketInfo: 'Train-specific travel. No cancellations. No City-Ticket. No access to the DB Lounge',
|
||||||
|
addDataTicketDetails: 'You can use all trains indicated on your ticket. Your ticket constitutes a continuous contract of carriage in each direction (through ticket). Should you make a passenger rights claim, the ticket will be considered in its entirety. Special rules apply to tickets including City-Ticket, see there. Your ticket cannot be cancelled. Your ticket does not include a City-Ticket. The City-Ticket is issued together with your Super Sparpreis or Sparpreis ticket, depending on the journey you have booked. Your ticket does not entitle you to use the DB Lounge.',
|
||||||
|
addDataTravelInfo: 'Travel 1st class',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Sparpreis',
|
||||||
|
priceObj: {
|
||||||
|
amount: 5099,
|
||||||
|
currency: 'EUR',
|
||||||
|
},
|
||||||
|
firstClass: false,
|
||||||
|
partialFare: false,
|
||||||
|
addDataTicketInfo: 'Train-specific travel. Cancellation subject to a fee before first day of validity. No City-Ticket',
|
||||||
|
addDataTicketDetails: 'You can use all trains indicated on your ticket. Your ticket constitutes a continuous contract of carriage in each direction (through ticket). Should you make a passenger rights claim, the ticket will be considered in its entirety. Special rules apply to tickets including City-Ticket, see there. You can cancel your ticket up to and including 02.01.2025 for a fee of EUR 10,00. You will receive a voucher for the remaining amount. No cancellation thereafter. Your ticket does not include a City-Ticket. The City-Ticket is issued together with your Super Sparpreis or Sparpreis ticket, depending on the journey you have booked.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Sparpreis',
|
||||||
|
priceObj: {
|
||||||
|
amount: 6199,
|
||||||
|
currency: 'EUR',
|
||||||
|
},
|
||||||
|
firstClass: true,
|
||||||
|
partialFare: false,
|
||||||
|
addDataTicketInfo: 'Train-specific travel. Cancellation subject to a fee before first day of validity. No City-Ticket. No access to the DB Lounge',
|
||||||
|
addDataTicketDetails: 'You can use all trains indicated on your ticket. Your ticket constitutes a continuous contract of carriage in each direction (through ticket). Should you make a passenger rights claim, the ticket will be considered in its entirety. Special rules apply to tickets including City-Ticket, see there. You can cancel your ticket up to and including 02.01.2025 for a fee of EUR 10,00. You will receive a voucher for the remaining amount. No cancellation thereafter. Your ticket does not include a City-Ticket. The City-Ticket is issued together with your Super Sparpreis or Sparpreis ticket, depending on the journey you have booked. Your ticket does not entitle you to use the DB Lounge.',
|
||||||
|
addDataTravelInfo: 'Travel 1st class',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Flexpreis',
|
||||||
|
priceObj: {
|
||||||
|
amount: 5550,
|
||||||
|
currency: 'EUR',
|
||||||
|
},
|
||||||
|
firstClass: false,
|
||||||
|
partialFare: false,
|
||||||
|
addDataTicketInfo: 'Unrestricted choice of trains. Cancellation. City-Ticket',
|
||||||
|
addDataTicketDetails: 'Your IC/EC ticket lets you use any Intercity or Eurocity train as well as regional and local trains. Your ticket constitutes a continuous contract of carriage in each direction (through ticket). Should you make a passenger rights claim, the ticket will be considered in its entirety. Special rules apply to tickets including City-Ticket; see there. You can cancel your ticket free of charge up to and including 26.12.2024. After that, cancellation is available for a fee of EUR 10,00 until 02.01.2025 and from 03.01.2025 for a fee of EUR 30,00. Your ticket includes a City-Ticket for Frankfurt(Main), Stadtgebiet Frankfurt ohne Flughafen (Tarifgebiet 50 ohne Flughafen) and Stuttgart, Stadtgebiet Stuttgart (Tarifzone 1, ehemals Zone 10 und 20). The City-Ticket is valid in conjunction with your long-distance ticket only when you use it for connecting services in local or regional rail passenger transport (e.g. S-Bahn, RE and RB trains) as part of the through ticket. The City-Ticket is issued together with your Super Sparpreis or Sparpreis ticket, depending on the journey you have booked.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Flexpreis',
|
||||||
|
priceObj: {
|
||||||
|
amount: 10270,
|
||||||
|
currency: 'EUR',
|
||||||
|
},
|
||||||
|
firstClass: true,
|
||||||
|
partialFare: false,
|
||||||
|
addDataTicketInfo: 'Unrestricted choice of trains. Cancellation. City-Ticket. Access to the DB Lounge. Seat included',
|
||||||
|
addDataTicketDetails: 'Your IC/EC ticket lets you use any Intercity or Eurocity train as well as regional and local trains. Your ticket constitutes a continuous contract of carriage in each direction (through ticket). Should you make a passenger rights claim, the ticket will be considered in its entirety. Special rules apply to tickets including City-Ticket; see there. You can cancel your ticket free of charge up to and including 26.12.2024. After that, cancellation is available for a fee of EUR 10,00 until 02.01.2025 and from 03.01.2025 for a fee of EUR 30,00. Your ticket includes a City-Ticket for Frankfurt(Main), Stadtgebiet Frankfurt ohne Flughafen (Tarifgebiet 50 ohne Flughafen) and Stuttgart, Stadtgebiet Stuttgart (Tarifzone 1, ehemals Zone 10 und 20). The City-Ticket is valid in conjunction with your long-distance ticket only when you use it for connecting services in local or regional rail passenger transport (e.g. S-Bahn, RE and RB trains) as part of the through ticket. The City-Ticket is issued together with your Super Sparpreis or Sparpreis ticket, depending on the journey you have booked. Your ticket entitles you to use the DB Lounge. Your ticket includes a free seat reservation.',
|
||||||
|
addDataTravelInfo: 'Travel 1st class',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
realtimeDataUpdatedAt: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
dbNavJourney,
|
||||||
|
};
|
1
test/fixtures/dbnav-refresh-journey.json
vendored
Normal file
1
test/fixtures/dbnav-refresh-journey.json
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -40,7 +40,7 @@ tap.test('parses ICE leg correctly', (t) => {
|
||||||
const expected = {
|
const expected = {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
id: 'ice-229',
|
id: 'ice-229',
|
||||||
fahrtNr: 229,
|
fahrtNr: '229',
|
||||||
name: 'ICE 229',
|
name: 'ICE 229',
|
||||||
public: true,
|
public: true,
|
||||||
product: 'nationalExpress',
|
product: 'nationalExpress',
|
||||||
|
@ -71,7 +71,7 @@ tap.test('parses Bus trip correctly', (t) => {
|
||||||
const expected = {
|
const expected = {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
id: '',
|
id: '',
|
||||||
fahrtNr: undefined,
|
fahrtNr: '',
|
||||||
name: 'Bus 807',
|
name: 'Bus 807',
|
||||||
public: true,
|
public: true,
|
||||||
product: undefined,
|
product: undefined,
|
||||||
|
@ -142,7 +142,7 @@ tap.test('parses ris entry correctly', (t) => {
|
||||||
const expected = {
|
const expected = {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
id: 'rb-51-15538',
|
id: 'rb-51-15538',
|
||||||
fahrtNr: 15538,
|
fahrtNr: '15538',
|
||||||
name: 'RB 51 (15538)',
|
name: 'RB 51 (15538)',
|
||||||
public: true,
|
public: true,
|
||||||
product: 'nationalExpress',
|
product: 'nationalExpress',
|
||||||
|
@ -162,7 +162,7 @@ tap.test('parses dbnav ruf correctly', (t) => {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
id: 'ruf-9870',
|
id: 'ruf-9870',
|
||||||
name: 'RUF 9870',
|
name: 'RUF 9870',
|
||||||
fahrtNr: undefined,
|
fahrtNr: '9870',
|
||||||
public: true,
|
public: true,
|
||||||
product: 'taxi',
|
product: 'taxi',
|
||||||
productName: 'RUF',
|
productName: 'RUF',
|
||||||
|
|
Loading…
Add table
Reference in a new issue