dbnav journeys, trips, fixes

This commit is contained in:
Traines 2025-01-03 10:57:24 +00:00
parent 3d998de41c
commit 87a705e966
29 changed files with 3528 additions and 129 deletions

View file

@ -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
}; };
}; };

View file

@ -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,

View file

@ -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 {

View file

@ -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,
};

View file

@ -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"
} }

View file

@ -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
View 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
View 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,
};

View file

@ -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;
}; };

View file

@ -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;
} }

View file

@ -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;
} }

View file

@ -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;
}; };

View file

@ -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) => {

View file

@ -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,

View file

@ -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],
}, },
})); }));

View file

@ -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
View 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,
};

View file

@ -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;

View file

@ -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);

View 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();
});

View file

@ -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
View 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

View file

@ -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$$$$$$',

View file

@ -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
View 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,
};

File diff suppressed because one or more lines are too long

View file

@ -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',