Compare commits

..

45 commits
v6.3.4 ... main

Author SHA1 Message Date
Traines
b59d7b3084 fix docs
[skip ci]
2025-04-27 17:13:10 +00:00
Traines
db4c03054a cspell...
Some checks failed
test / lint-and-spellcheck (push) Has been cancelled
test / unit-tests (18.x) (push) Has been cancelled
test / unit-tests (20.x) (push) Has been cancelled
test / unit-tests (22.x) (push) Has been cancelled
test / integration-tests (18.x) (push) Has been cancelled
test / integration-tests (20.x) (push) Has been cancelled
test / integration-tests (22.x) (push) Has been cancelled
test / e2e-tests (18.x) (push) Has been cancelled
2025-04-11 21:05:18 +00:00
Traines
eac21d188b 6.8.1 2025-04-11 20:59:29 +00:00
Traines
ad09f8b1be dticket support 2025-04-11 20:59:06 +00:00
Traines
c4d0a55d41 docs
Some checks failed
test / lint-and-spellcheck (push) Has been cancelled
test / unit-tests (18.x) (push) Has been cancelled
test / unit-tests (20.x) (push) Has been cancelled
test / unit-tests (22.x) (push) Has been cancelled
test / integration-tests (18.x) (push) Has been cancelled
test / integration-tests (20.x) (push) Has been cancelled
test / integration-tests (22.x) (push) Has been cancelled
test / e2e-tests (18.x) (push) Has been cancelled
2025-03-22 00:56:43 +00:00
Traines
29aab87cdf add dbbahnhof 2025-03-22 00:39:43 +00:00
Traines
883eb8c8de refactor 2025-03-21 21:13:20 +00:00
Traines
b20cf1060a add dbris, moreStops param 2025-03-21 20:49:38 +00:00
Traines
b887c674d4 fix param parsing for refreshJourney
Some checks failed
test / lint-and-spellcheck (push) Has been cancelled
test / unit-tests (18.x) (push) Has been cancelled
test / unit-tests (20.x) (push) Has been cancelled
test / unit-tests (22.x) (push) Has been cancelled
test / integration-tests (18.x) (push) Has been cancelled
test / integration-tests (20.x) (push) Has been cancelled
test / integration-tests (22.x) (push) Has been cancelled
test / e2e-tests (18.x) (push) Has been cancelled
2025-03-20 10:58:32 +00:00
Traines
b3e0e764e2 update db-hafas-stations
Some checks failed
test / lint-and-spellcheck (push) Has been cancelled
test / unit-tests (18.x) (push) Has been cancelled
test / unit-tests (20.x) (push) Has been cancelled
test / unit-tests (22.x) (push) Has been cancelled
test / integration-tests (18.x) (push) Has been cancelled
test / integration-tests (20.x) (push) Has been cancelled
test / integration-tests (22.x) (push) Has been cancelled
test / e2e-tests (18.x) (push) Has been cancelled
2025-03-19 00:39:01 +00:00
Traines
2ea47f7792 disable some more live e2e tests 2025-03-19 00:38:48 +00:00
Traines
6c2081c14e fix spelling and tests...
Some checks failed
test / lint-and-spellcheck (push) Has been cancelled
test / unit-tests (18.x) (push) Has been cancelled
test / unit-tests (20.x) (push) Has been cancelled
test / unit-tests (22.x) (push) Has been cancelled
test / integration-tests (18.x) (push) Has been cancelled
test / integration-tests (20.x) (push) Has been cancelled
test / integration-tests (22.x) (push) Has been cancelled
test / e2e-tests (18.x) (push) Has been cancelled
2025-03-15 23:25:48 +00:00
Traines
f741a13670 docs 2025-03-15 23:15:40 +00:00
Traines
bcaad526c7 disable blocked endpoints tests for now 2025-03-15 22:54:12 +00:00
Traines
162b946bac support for bestprice, notOnlyFastRoutes, serviceDays, fix param default handling 2025-03-15 22:44:08 +00:00
Traines
14b80dbf33 update deps
[skip ci]
2025-03-14 17:28:14 +00:00
Traines
1927f98906 fix station enrichment 2025-03-14 17:19:48 +00:00
Traines
0ef3935a35 update db-hafas-stations, bump version 2025-03-14 16:57:38 +00:00
Traines
b04a671b50 improve duration explanation 2025-03-14 16:57:02 +00:00
Traines
9975a6c9ac more reliable setup of proxy agent
Some checks failed
test / lint-and-spellcheck (push) Has been cancelled
test / unit-tests (18.x) (push) Has been cancelled
test / unit-tests (20.x) (push) Has been cancelled
test / unit-tests (22.x) (push) Has been cancelled
test / integration-tests (18.x) (push) Has been cancelled
test / integration-tests (20.x) (push) Has been cancelled
test / integration-tests (22.x) (push) Has been cancelled
test / e2e-tests (18.x) (push) Has been cancelled
2025-03-08 23:12:36 +00:00
Traines
960371e2ec v6.6.1
Some checks failed
test / lint-and-spellcheck (push) Has been cancelled
test / unit-tests (18.x) (push) Has been cancelled
test / unit-tests (20.x) (push) Has been cancelled
test / unit-tests (22.x) (push) Has been cancelled
test / integration-tests (18.x) (push) Has been cancelled
test / integration-tests (20.x) (push) Has been cancelled
test / integration-tests (22.x) (push) Has been cancelled
test / e2e-tests (18.x) (push) Has been cancelled
2025-03-06 20:22:05 +00:00
Traines
88acdd1620 enrichStations fix 2025-03-06 20:18:00 +00:00
dabund24
25cbb288ca
Fix dbnav time zone bug (#25)
* fix dbnav time zone bug

* test fix of dbnav time zone bug

* exclude `test/parse/dbnav-journey.js` from spell check
2025-03-06 21:13:08 +01:00
Kristjan ESPERANTO
a6e84be2df Add spell check to CI
Some checks are pending
test / lint-and-spellcheck (push) Waiting to run
test / unit-tests (18.x) (push) Waiting to run
test / unit-tests (20.x) (push) Waiting to run
test / unit-tests (22.x) (push) Waiting to run
test / integration-tests (18.x) (push) Waiting to run
test / integration-tests (20.x) (push) Waiting to run
test / integration-tests (22.x) (push) Waiting to run
test / e2e-tests (18.x) (push) Blocked by required conditions
2025-03-06 16:13:56 +01:00
Kristjan ESPERANTO
de63bf0a37 Fix typos found with cspell 2025-03-06 16:13:56 +01:00
Kristjan ESPERANTO
040a8f44e4 Add cspell 2025-03-06 16:13:56 +01:00
Traines
6b67a77823 v6.6.0
Some checks failed
test / lint (push) Has been cancelled
test / unit-tests (18.x) (push) Has been cancelled
test / unit-tests (20.x) (push) Has been cancelled
test / unit-tests (22.x) (push) Has been cancelled
test / integration-tests (18.x) (push) Has been cancelled
test / integration-tests (20.x) (push) Has been cancelled
test / integration-tests (22.x) (push) Has been cancelled
test / e2e-tests (18.x) (push) Has been cancelled
2025-03-02 13:39:35 +00:00
Kristjan ESPERANTO
debb45a929
Update dependencies (#21) 2025-03-02 14:32:29 +01:00
Kristjan ESPERANTO
53b385a865
Replace lodash by modern JavaScript functions (#20)
Some checks are pending
test / lint (push) Waiting to run
test / unit-tests (18.x) (push) Waiting to run
test / unit-tests (20.x) (push) Waiting to run
test / unit-tests (22.x) (push) Waiting to run
test / integration-tests (18.x) (push) Waiting to run
test / integration-tests (20.x) (push) Waiting to run
test / integration-tests (22.x) (push) Waiting to run
test / e2e-tests (18.x) (push) Blocked by required conditions
* Replace lodash by built-in functions

* .flatMap -> .flat
2025-03-01 17:30:58 +01:00
Traines
185870db3d cleanup
Some checks failed
test / lint (push) Has been cancelled
test / unit-tests (18.x) (push) Has been cancelled
test / unit-tests (20.x) (push) Has been cancelled
test / unit-tests (22.x) (push) Has been cancelled
test / integration-tests (18.x) (push) Has been cancelled
test / integration-tests (20.x) (push) Has been cancelled
test / integration-tests (22.x) (push) Has been cancelled
test / e2e-tests (18.x) (push) Has been cancelled
2025-02-25 12:56:29 +00:00
Traines
16829f839c add back HTTP_PROXY support 2025-02-25 12:52:50 +00:00
Traines
9fe4972d2b fix includeRelatedStations 2025-02-25 12:23:26 +00:00
McToel
1aeb246622
Browser compatibility (#17)
* Removed Proxy and local address code

* replaced node crypto with web crypto

* Replaced require with static imports

* removed commented out imports

* import db-hafas-stations on demand

* trying to handle undefined envs

* Less optimistic variable handling

* cleanup

* Small browser docs addition

* Linting

* No async in new Promise

* Bumped eslint to v9 and ecmaScript to 2025

* removed duplicated eslint config

* Bumped minimal node version to node 18

* Added node 24

* using math.random instead of webcrypto and reintroduced randomizeUserAgent

* Oh no node 24 is actually not released yet

* removed temp debug file
2025-02-25 13:21:26 +01:00
Kristjan ESPERANTO
6d1d0c626f Upgrade ESLint and fix linting issues
Some checks failed
test / lint (push) Has been cancelled
test / unit-tests (16.x) (push) Has been cancelled
test / unit-tests (18.x) (push) Has been cancelled
test / unit-tests (20.x) (push) Has been cancelled
test / unit-tests (22.x) (push) Has been cancelled
test / integration-tests (16.x) (push) Has been cancelled
test / integration-tests (18.x) (push) Has been cancelled
test / integration-tests (20.x) (push) Has been cancelled
test / integration-tests (22.x) (push) Has been cancelled
test / e2e-tests (18.x) (push) Has been cancelled
2025-02-17 21:41:20 +01:00
Kristjan ESPERANTO
afa99b0742
Review test workflow (#16)
Some checks are pending
test / lint (push) Waiting to run
test / unit-tests (16.x) (push) Waiting to run
test / unit-tests (18.x) (push) Waiting to run
test / unit-tests (20.x) (push) Waiting to run
test / unit-tests (22.x) (push) Waiting to run
test / integration-tests (16.x) (push) Waiting to run
test / integration-tests (18.x) (push) Waiting to run
test / integration-tests (20.x) (push) Waiting to run
test / integration-tests (22.x) (push) Waiting to run
test / e2e-tests (18.x) (push) Blocked by required conditions
* Run linting only once

* Update actions/cache to v4

* Run e2e-tests with the same matrix as the others

* Change cache keys to avoid cache collisions

* Run e2e-tests only once
Since they go against the live DB APIs #16
2025-02-17 19:09:14 +01:00
Kristjan ESPERANTO
229dbac93e
Update copyright information (#15)
Some checks failed
test / unit-tests (16.x) (push) Has been cancelled
test / unit-tests (18.x) (push) Has been cancelled
test / unit-tests (20.x) (push) Has been cancelled
test / unit-tests (22.x) (push) Has been cancelled
test / integration-tests (16.x) (push) Has been cancelled
test / integration-tests (18.x) (push) Has been cancelled
test / integration-tests (20.x) (push) Has been cancelled
test / integration-tests (22.x) (push) Has been cancelled
test / e2e-tests (16.x) (push) Has been cancelled
2025-02-14 22:33:50 +01:00
Traines
7a1e513fa2 6.5.0
Some checks are pending
test / unit-tests (16.x) (push) Waiting to run
test / unit-tests (18.x) (push) Waiting to run
test / unit-tests (20.x) (push) Waiting to run
test / unit-tests (22.x) (push) Waiting to run
test / integration-tests (16.x) (push) Waiting to run
test / integration-tests (18.x) (push) Waiting to run
test / integration-tests (20.x) (push) Waiting to run
test / integration-tests (22.x) (push) Waiting to run
test / e2e-tests (16.x) (push) Blocked by required conditions
2025-02-13 23:34:39 +00:00
Traines
f1302b0a7b docs 2025-02-13 23:33:45 +00:00
Traines
177a3cab3f migrate and update loyaltyCard parsing from db-rest 2025-02-13 23:32:01 +00:00
Traines
71d1a4f1a9 refactor enrichStations, only load on first request 2025-02-13 22:16:22 +00:00
Traines
6ff406ea79 artificially filter for includeRelatedStations and direction 2025-02-13 21:53:49 +00:00
Traines
2a23e1ad9b make dbweb selectable in docker img 2025-02-13 19:41:46 +00:00
Traines
9314e59053 6.4.0
Some checks failed
test / unit-tests (16.x) (push) Has been cancelled
test / unit-tests (18.x) (push) Has been cancelled
test / unit-tests (20.x) (push) Has been cancelled
test / unit-tests (22.x) (push) Has been cancelled
test / integration-tests (16.x) (push) Has been cancelled
test / integration-tests (18.x) (push) Has been cancelled
test / integration-tests (20.x) (push) Has been cancelled
test / integration-tests (22.x) (push) Has been cancelled
test / e2e-tests (16.x) (push) Has been cancelled
2025-02-11 21:20:33 +00:00
Traines
69c098744a update docs 2025-02-11 21:16:23 +00:00
dabund24
c671e995cb
bahn.de boards (#12)
* parse bahn.de boards

* add optional chaining in line.js

* unit tests for bahn.de boards

* fix product check in line.js for bahn.de boards

* add integration tests for bahn.de boards

* allow letting hafas decide the amount of vias

* split dbweb and dbregioguide profiles; add db profile

* commit location-filter.js (forgot that in the last commit)

* simplify how db profile works

* remove `ezGleis` from coalesce for scheduled platform

* un-break parsing of remarks

* determine fahrtNr by removing all non-digits

* employ enrichStations for board stop property

* prevent timeouts in dbweb e2e test from calling `end()` twice

* use promises in dbweb e2e tests when waiting for enrichStations to work

* replace vias option with stopovers option for dbweb profile; enrich stations when only name is known

* change dbweb-departures test covering enrichStation feature for stop and stopovers

* remove check for not existing option

* move verkehrsmittel.name in front of verkehrsmittel.langText when parsing name in line.js
2025-02-09 00:46:21 +01:00
102 changed files with 12850 additions and 2543 deletions

View file

@ -1,54 +0,0 @@
{
"env": {
"es2021": true,
"node": true
},
"extends": ["eslint:recommended", "plugin:@stylistic/all-extends"],
"ignorePatterns": ["node_modules", "*example.js"],
"parserOptions": {
"ecmaVersion": 2021,
"sourceType": "module"
},
"rules": {
"curly": "error",
"no-implicit-coercion": "error",
"no-unused-vars": [
"error",
{
"vars": "all",
"args": "none",
"ignoreRestSiblings": false
}
],
"@stylistic/array-bracket-newline": ["error", "consistent"],
"@stylistic/array-element-newline": ["error", "consistent"],
"@stylistic/arrow-parens": "off",
"@stylistic/comma-dangle": ["error", "always-multiline"],
"@stylistic/dot-location": ["error", "property"],
"@stylistic/function-call-argument-newline": ["error", "consistent"],
"@stylistic/function-paren-newline": "off",
"@stylistic/indent": ["error", "tab"],
"@stylistic/indent-binary-ops": ["error", "tab"],
"@stylistic/max-len": "off",
"@stylistic/multiline-ternary": ["error", "always-multiline"],
"@stylistic/newline-per-chained-call": ["error", { "ignoreChainWithDepth": 1 }],
"@stylistic/no-mixed-operators": "off",
"@stylistic/no-tabs": "off",
"@stylistic/object-property-newline": "off",
"@stylistic/one-var-declaration-per-line": "off",
"@stylistic/operator-linebreak": ["error", "before"],
"@stylistic/padded-blocks": "off",
"@stylistic/quote-props": ["error", "consistent-as-needed"],
"@stylistic/quotes": ["error", "single"]
},
"overrides": [
{
"files": [
"test/**"
],
"rules": {
"no-unused-vars": "off"
}
}
]
}

View file

@ -6,12 +6,29 @@ env:
npm_config_cache: /tmp/npm-cache
jobs:
lint-and-spellcheck:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: "Checkout code"
uses: actions/checkout@v4
- name: "Use Node.js"
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
- run: npm install
- name: Run lint check
run: npm run lint
- name: Run spell check
run: npm run test-spelling
unit-tests:
runs-on: ubuntu-latest
strategy:
matrix:
node-version:
- 16.x
- 18.x
- 20.x
- 22.x
@ -25,13 +42,12 @@ jobs:
- id: cache-npm
name: restore npm cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
key: npm-cache-${{ github.ref_name }}
key: npm-cache-${{ github.ref_name }}-${{ matrix.node-version }}-unit-tests
path: ${{ env.npm_config_cache }}
- run: npm install
- run: npm run lint
- run: npm run test-unit
integration-tests:
@ -39,7 +55,6 @@ jobs:
strategy:
matrix:
node-version:
- 16.x
- 18.x
- 20.x
- 22.x
@ -53,9 +68,9 @@ jobs:
- id: cache-npm
name: restore npm cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
key: npm-cache-${{ github.ref_name }}
key: npm-cache-${{ github.ref_name }}-${{ matrix.node-version }}-integration-tests
path: ${{ env.npm_config_cache }}
- run: npm install
@ -66,7 +81,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
node-version: [18.x]
steps:
- name: checkout
uses: actions/checkout@v4
@ -77,9 +92,9 @@ jobs:
- id: cache-npm
name: restore npm cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
key: npm-cache-${{ github.ref_name }}
key: npm-cache-${{ github.ref_name }}-${{ matrix.node-version }}-e2e-tests
path: ${{ env.npm_config_cache }}
- run: npm install

View file

@ -1,4 +1,7 @@
Copyright (c) 2024, Jannis R
# ISC License
- Copyright © 2024 Jannis R
- Copyright © 2025 traines-source
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

40
api.js
View file

@ -1,32 +1,12 @@
import {createClient} from './index.js';
import {profile as dbProfile} from './p/db/index.js';
import {profile as dbnavProfile} from './p/dbnav/index.js';
import {profile as dbwebProfile} from './p/dbweb/index.js';
import {profile as dbrisProfile} from './p/dbris/index.js';
import {profile as dbbahnhofProfile} from './p/dbbahnhof/index.js';
import {profile as dbregioguideProfile} from './p/dbregioguide/index.js';
import {mapRouteParsers} from './lib/api-parsers.js';
import {createHafasRestApi as createApi} from 'hafas-rest-api';
import {loyaltyCardParser} from 'db-rest/lib/loyalty-cards.js';
import {parseBoolean, parseInteger} from 'hafas-rest-api/lib/parse.js';
// TODO product support for nearby etc?
const mapRouteParsers = (route, parsers) => {
if (!route.includes('journey')) {
return parsers;
}
return {
...parsers,
loyaltyCard: loyaltyCardParser,
firstClass: {
description: 'Search for first-class options?',
type: 'boolean',
default: 'false',
parse: parseBoolean,
},
age: {
description: 'Age of traveller',
type: 'integer',
defaultStr: '*adult*',
parse: parseInteger,
},
};
};
const config = {
hostname: process.env.HOSTNAME || 'localhost',
@ -45,10 +25,18 @@ const config = {
mapRouteParsers,
};
const profiles = {
db: dbProfile,
dbnav: dbnavProfile,
dbweb: dbwebProfile,
dbris: dbrisProfile,
dbbahnhof: dbbahnhofProfile,
dbregioguide: dbregioguideProfile,
};
const start = async () => {
const vendo = createClient(
process.env.DB_PROFILE == 'db' ? dbProfile : dbnavProfile,
profiles[process.env.DB_PROFILE] || dbnavProfile,
process.env.USER_AGENT || 'link-to-your-project-or-email',
config,
);

644
cspell.config.json Normal file
View file

@ -0,0 +1,644 @@
{
"version": "0.2",
"language": "en",
"words": [
"Abfahrt",
"abfahrten",
"abfahrts",
"abfrage",
"abgangs",
"Abgelaufen",
"Abonnement",
"Abschnitte",
"abschnitts",
"Adelsheim",
"Adenauerplatz",
"agilis",
"Ahorn",
"Ahrensfelde",
"Aktualisierung",
"Alexanderplatz",
"alterseingabe",
"Altstadt",
"Alzey",
"andere",
"Anfang",
"anfrage",
"Anfrage",
"anfragezeit",
"Angebot",
"angebote",
"angebots",
"angebotsbeziehung",
"angebotseinholung",
"ANGEBOTSINFORMATION",
"Angebotsoption",
"angefragten",
"Angeltürn",
"ankuenfte",
"ankunft",
"ankunfts",
"Anmeldung",
"ANRUFPFLICHTIG",
"ANRUFPFLICHTIGEVERKEHRE",
"Anteil",
"anzahl",
"anzeige",
"Anzeigen",
"APPLEPAY",
"ARGUMENTE",
"arrs",
"Arverio",
"Aschaffenburg",
"attribut",
"Atze",
"Auerbach",
"AUSFALL",
"ausgegeben",
"auslastung",
"auslastungs",
"Auslastungsinformation",
"auslastungsmeldungen",
"auslastungstexte",
"Ausreserviert",
"außergewöhnlich",
"Ausserhalb",
"außerhalb",
"Ausstattung",
"Auswahl",
"autonome",
"bahnbonus",
"BAHNCARD",
"Bahnhof",
"bahnhofs",
"Bahnhofsinfo",
"bahnhofstafel",
"bahnhofstafeln",
"BARRIEREFREI",
"Bayerischer",
"BEFÖRDERER",
"begrenzt",
"behaviour",
"Behindertengerechte",
"Behindertengerechtes",
"Benoit",
"Beratzhausen",
"bereits",
"besonderer",
"Besucherpark",
"betrag",
"Beusselstraße",
"bezeichnung",
"Bietigheim",
"Bismarckstr",
"Bissingen",
"bitmasks",
"Blaschkoallee",
"Blissestr",
"BNWNZF",
"Böhme",
"BONVOYO",
"Bordbistro",
"Bordrestaurant",
"Boxberg",
"Breckerfeld",
"Britz",
"brokentrip",
"brutto",
"Buch",
"Buchbar",
"buchbarkeit",
"BUCHEN",
"BUCHUNG",
"buchungs",
"Bundesbahnen",
"Bundesplatz",
"BUSSE",
"bzgl",
"capacitorjs",
"Chaussee",
"checkin",
"childrens",
"CITYTICKET",
"clie",
"cncl",
"codeshares",
"conds",
"consumability",
"Consumability",
"Creglingen",
"crosssell",
"Ctrf",
"customisation",
"customisations",
"Damm",
"Daten",
"Dauer",
"dauerhaft",
"dbnav",
"dbregioguide",
"dbris",
"dbweb",
"Deldicque",
"DELFI",
"derhuerst",
"Deutz",
"dhid",
"differenzpreis",
"Dinkelsbühl",
"distanz",
"Dombühl",
"echtzeit",
"Eggmühl",
"ehemals",
"Eicholzheim",
"einchecken",
"eine",
"einfache",
"Einrichtung",
"Einstieg",
"einstiegs",
"Einstiegshilfe",
"Einstiegstyp",
"Eisenacher",
"emis",
"empfehlen",
"Ende",
"entfällt",
"Entgelt",
"ereignis",
"Erforderlich",
"erforderlicher",
"Ergoldsbach",
"Erlenbach",
"ERMAESSIGUNG",
"ermaessigungen",
"erster",
"ERWACHSENER",
"erwarten",
"erwartet",
"Eubigheim",
"Eurocity",
"eventuell",
"externe",
"Fahrkarte",
"fahrplan",
"Fahrplanperiode",
"Fahrpreis",
"Fahrradbeförderung",
"FAHRRADMITNAHME",
"Fahrt",
"FAHRZEUG",
"Fahrzeuggebundene",
"FAMILIENKIND",
"Fehler",
"Fehrbelliner",
"Fernbf",
"Fernverkehr",
"Flexpreis",
"Flix",
"Fltr",
"Flughafen",
"FPTF",
"Freising",
"Friedrichshall",
"Friedrichstr",
"frueher",
"Frwd",
"FUSSWEG",
"Fußweg",
"FVFFLPI",
"FVFSPPI",
"FVFSSPI",
"FVKBACI",
"GARE",
"Garten",
"Gastronomie",
"Gattung",
"gekauft",
"Geltungzeitpunkt",
"GENERALABONNEMENT",
"geolocation",
"geopositions",
"Geringe",
"gesamt",
"Gesundbrunnen",
"gleis",
"Gneisenaustr",
"Goerdelersteg",
"grafisch",
"Greifswalder",
"Grenzallee",
"Grünanlagen",
"gruppen",
"Gütersloh",
"GUTSCHEIN",
"haben",
"hafas",
"Hagelstadt",
"Halbtax",
"HALBTAXABO",
"Halemweg",
"Halensee",
"Hallerstraße",
"halte",
"Haltestellen",
"Hansering",
"Hansestadt",
"Haselhorst",
"Hauptbahnhof",
"Heidelberger",
"Hennef",
"Hermannplatz",
"Hermannstraße",
"hessen",
"Heuchelhof",
"HINFAHRT",
"Hinweis",
"Hinweise",
"HOCH",
"HOCHGESCHWINDIGKEITSZUEGE",
"Hohe",
"Hohenstadt",
"Hohenzollerndamm",
"Hüngheim",
"IBNR",
"Ihren",
"Ihrer",
"Informationen",
"INKLUSIVE",
"Innsbrucker",
"Instanz",
"INTERCITYUNDEUROCITYZUEGE",
"INTERREGIOUNDSCHNELLZUEGE",
"inventarsystem",
"irregulaere",
"Jakob",
"Jannis",
"Jannowitzbrücke",
"jetzt",
"Johannisthaler",
"journeystop",
"JUGENDLICHER",
"Jungfernheide",
"Kanal",
"KATALOG",
"kategorie",
"Kategorien",
"kategorisierung",
"kein",
"KEINE",
"Kennung",
"Kennzeichen",
"Kirche",
"klasse",
"KLASSENLOS",
"KLEINKIND",
"Kleistpark",
"Klima",
"KLIMATICKET",
"Köfering",
"Köln",
"kombinations",
"komfort",
"konditionen",
"konditions",
"Konstanzer",
"kontext",
"Kontingente",
"KRCC",
"KREDITKARTE",
"Kristjan",
"Kurz",
"kurztext",
"Landsberger",
"Landshut",
"langtext",
"LASTSCHRIFT",
"Lauda",
"letzte",
"letztes",
"leuchtturm",
"Lichtenberg",
"liegt",
"linien",
"Lipschitzallee",
"Loesungs",
"Ludwigsburg",
"luxon",
"materialisierungs",
"Maxnet",
"MBAAA",
"Mehringdamm",
"Meidling",
"Meldungen",
"Merchingen",
"Messe",
"Mierendorffplatz",
"Millis",
"Minden",
"mitteltext",
"mittlere",
"Mobilitätseingeschränkte",
"Mobilitätsservice",
"Möckernbrücke",
"Möckmühl",
"modul",
"moeckmuehl",
"Moeglich",
"möglich",
"Montabaur",
"Moosburg",
"Mosbach",
"Movas",
"München",
"Musiktheater",
"mwst",
"mxtxm",
"Nachgelagert",
"Nachricht",
"Nahreisezug",
"NAHVERKEHRSONSTIGEZUEGE",
"Naturkundemuseum",
"Neckarsulm",
"netto",
"Neufahrn",
"Neukölln",
"Neumarkt",
"NICHT",
"Niederbay",
"NIEDRIG",
"noch",
"noopener",
"Nord",
"noreferrer",
"Notiz",
"Notizen",
"NULLPREIS",
"nummer",
"nutzungs",
"Obereubigheim",
"Oberpf",
"Oberschefflenz",
"Obertraubling",
"Oberwittstadt",
"Ohne",
"orte",
"Ostbahnhof",
"Osterburken",
"Österreichische",
"Ostkreuz",
"Parchimer",
"Parsberg",
"passend",
"Passlist",
"Paulsternstr",
"Pauschalpreis",
"Pergamonkeller",
"Pergamonweg",
"Perleberg",
"PFLICHT",
"PLAETZE",
"planungs",
"platf",
"Platz",
"platzbedarfe",
"platzprofil",
"pollyjs",
"polyline",
"polylines",
"Positionen",
"Preis",
"preise",
"preisunterdrueckung",
"Prenzlauer",
"prio",
"priorisierte",
"prioritaet",
"produkt",
"produkte",
"produktgattungen",
"Profil",
"PRUEFEN",
"PUBLICTRANSPORT",
"Punkte",
"rabatt",
"Radzio",
"RAILPLUS",
"randomised",
"Ravenstein",
"Referenz",
"referenzen",
"referenziertes",
"Regio",
"Regionalbf",
"regulaere",
"regulaerer",
"reise",
"REISEANGEBOT",
"reiseloesung",
"reisende",
"reisenden",
"REISESTELLENKARTE",
"reisetag",
"Reisezentrum",
"REMENTR",
"REMRESR",
"Reservierbar",
"Reservieren",
"Reservierung",
"reservierungen",
"reservierungs",
"RESERVIERUNGSANGEBOT",
"RESERVIERUNGSENTGELT",
"reservierungspflicht",
"Rhein",
"richtung",
"roehrt",
"Rohrdamm",
"Roigheim",
"Rollstuhl",
"rollstuhlgerechte",
"Römisches",
"Rosenthal",
"Rothenburg",
"Rudow",
"Rueck",
"Rüsselsheim",
"Saale",
"Sammelfaehig",
"satz",
"SBAHN",
"SBAHNEN",
"Scharnweberstr",
"SCHIFF",
"SCHIFFE",
"Schlachthof",
"schnelle",
"Schöneberg",
"Schönefeld",
"Schönhauser",
"Schwedter",
"Seckach",
"Sennfeld",
"servicekategorie",
"SHCARD",
"sich",
"SICT",
"Sieg",
"Siegburg",
"Siemensdamm",
"Siglingen",
"sitzplatz",
"Sitzplatzreservierung",
"slugg",
"Sonnenallee",
"Sören",
"spaeter",
"Sparpreis",
"spezifische",
"Spichernstr",
"Spittelmarkt",
"Stadtgebiet",
"Stadtmitte",
"Stadtpark",
"Steinach",
"Storkower",
"STRASSENBAHN",
"stufe",
"stufenfrei",
"suchbegriff",
"Suche",
"Südkreuz",
"Südstern",
"Sutter",
"tage",
"täglich",
"tapjs",
"tarif",
"Tarifbereiche",
"Tarifgebiet",
"Tarifzone",
"Tauber",
"teilpreis",
"teilstrecken",
"Tempelhof",
"Texte",
"Tiergarten",
"Toel",
"Torfstr",
"Torfstraße",
"Traines",
"traveller",
"Travellers",
"travelling",
"Treptower",
"Tsua",
"tvlr",
"UBAHN",
"ueber",
"uebergreifende",
"ueberschrift",
"Uiffingen",
"Umbuchungen",
"Umstiege",
"umstiegs",
"Umstiegsdauer",
"Umstiegszeit",
"unsere",
"unter",
"Unterwittstadt",
"upsell",
"ursprungs",
"ushrp",
"uuidv",
"Vaihingen",
"vendo",
"verbindung",
"verbindungen",
"verbindungs",
"verbindungsabschnitt",
"verbindungssuche",
"Verfuegbar",
"verfügbar",
"verkehrmittel",
"verkehrsmittel",
"Verkehrstage",
"VERKNUEPFT",
"Verlauf",
"vertraglicher",
"vlexx",
"Voordeelurenabo",
"Vorhanden",
"VORTEILE",
"VORTEILSCARD",
"Vorverkaufszeitraum",
"waehrung",
"wagenreihung",
"wählen",
"Wannsee",
"Wegener",
"Weichselstr",
"wenden",
"wenn",
"Westbahn",
"Westend",
"Westf",
"westhafen",
"Westkreuz",
"wichtig",
"Wilmersdorfer",
"wird",
"Witzleben",
"wochentage",
"Worringen",
"wunsch",
"Wunschplatz",
"Württemberg",
"württembergallee",
"Würzburg",
"Wutzkyallee",
"Yorckstr",
"Yureka",
"Zahlungsarten",
"Zeitpunkt",
"Zeitraum",
"zgck",
"ziel",
"Zimmern",
"Zitadelle",
"Zoologischer",
"Zuege",
"zugart",
"zugattrib",
"zugattribute",
"Zuges",
"zugfahrt",
"zuglaeufe",
"Zuglauf",
"zugnummer",
"zulaessige",
"Zusammenfassung",
"Züttlingen",
"Zwickauer",
"zwischenhalte",
"bestprice",
"intervalle",
"Intervalle",
"tagesbest",
"dbbahnhof",
"Deutschlandticket",
"fahrverguenstigungen",
"cancelation"
],
"ignorePaths": [
"docs/dumps/**",
"test/e2e/fixtures/**",
"test/fixtures/**",
"test/parse/remarks.js",
"test/parse/dbnav-journey.js"
],
"dictionaries": [
"node"
]
}

View file

@ -12,7 +12,7 @@ Also see the [root readme](https://github.com/public-transport/db-vendo-client)
- [`locations(query, [opt])`](locations.md) find stations, POIs and addresses
- [`stop(id, [opt])`](stop.md) get details about a stop/station
- [`nearby(location, [opt])`](nearby.md) show stations & POIs around
- `radar(north, west, south, east, [opt])` not supporteda
- `radar(north, west, south, east, [opt])` not supported
- `reachableFrom(address, [opt])` not supported
- `remarks([opt])` not supported
- `lines(query, [opt])` not supported

View file

@ -24,7 +24,7 @@ With `opt`, you can override the default options, which look like this:
```js
{
when: new Date(),
direction: null, // not supported
direction: null, // only supported in `dbweb` and with `enrichStations=true` (experimental)
line: null, // not supported
duration: 10, // show departures for the next n minutes
results: null, // max. number of results; `null` means "whatever HAFAS wants"
@ -32,23 +32,24 @@ With `opt`, you can override the default options, which look like this:
entrances: true, // not supported
linesOfStops: false, // not supported
remarks: true, // parse & expose hints & warnings?
stopovers: false, // fetch & parse previous/next stopovers?
stopovers: false, // fetch & parse previous/next stopovers?, only supported with `dbweb` profile
// departures at related stations
// e.g. those that belong together on the metro map.
includeRelatedStations: true, // only true supported
includeRelatedStations: true,
moreStops: null // also include departures/arrivals for array of up to nine additional station evaNumbers (not supported with dbnav and dbweb)
language: 'en' // language to get results in
}
```
The maximum supported duration is 720 for `db` and 60 for `dbnav` profile.
The maximum supported duration depends on the profile (see main readme), e.g. 720 for `db` and 60 for `dbnav`. In order to use the `dbris` profile, you need to pass the env vars `DB_API_KEY` and `DB_CLIENT_ID`.
If you pass an object `opt.products`, its fields will partially override the default products defined in the profile.
## Response
*Note:* As stated in the [*Friendly Public Transport Format* v2 draft spec](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md), the `when` field includes the current delay. The `delay` field, if present, expresses how much the former differs from the schedule.
You may pass a departure's `tripId` into [`trip(id, lineName, [opt])`](trip.md) to get details on the whole trip. For the `dbnav` profile HAFAS trip ids will be returned, for the `db` profile, RIS trip ids will be returned, then the `trip()` endpoint supports both id types.
You may pass a departure's `tripId` into [`trip(id, lineName, [opt])`](trip.md) to get details on the whole trip. For the `dbnav`/`dbweb` profile HAFAS trip ids will be returned, for the `db` profile, RIS trip ids will be returned, then the `trip()` endpoint supports both id types.
For `db` profile, cancelled trips will not be contained in the response!
For `db` profile, cancelled trips will not be contained in the response! For the `db` and `dbnav` profile, only the most important remarks will be contained in the boards.
```js
import {createClient} from 'db-vendo-client'

View file

@ -28,7 +28,7 @@
longitude: 3.21
}
// an address, which is an FTPF `location` object
// an address, which is an FPTF `location` object
{
type: 'location',
address: 'foo street 1',
@ -75,7 +75,9 @@ With `opt`, you can override the default options, which look like this:
subStops: true, // not supported
entrances: true, // not supported
remarks: true, // parse & expose hints & warnings?
scheduledDays: false, // not yet supported
scheduledDays: false, // returns a field `serviceDays` (instead of `scheduledDays` in hafas-client!) with a different, human-readable structure
notOnlyFastRoutes: false, // if true, also show journeys that are mathematically non-optimal
bestprice: false, // search for lowest prices across the entire day, returns list of journeys sorted by price
firstClass: false, // first or second class for tickets
loyaltyCard: null, // BahnCards etc., see below
language: 'en', // language to get results in

View file

@ -10,7 +10,7 @@ With `opt`, you can override the default options, which look like this:
{
results: 8, // maximum number of results
distance: null, // maximum walking distance in meters
poi: false, // return points of interest?
poi: false, // not supported
stops: true, // return stops/stations?
subStops: true, // not supported
entrances: true, // not supported

View file

@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: db-vendo-client
description: Schema for db-vendo-client/hafas-rest-api's Friendy Public Transport Format REST API
description: Schema for db-vendo-client/hafas-rest-api's Friendly Public Transport Format REST API
contact:
url: https://github.com/public-transport/db-vendo-client
version: '6'
@ -275,7 +275,7 @@ paths:
format: date-time
- name: earlierThan
in: query
description: Compute journeys "before" an `ealierRef`.
description: Compute journeys "before" an `earlierRef`.
schema:
type: string
- name: laterThan
@ -365,7 +365,19 @@ paths:
default: true
- name: scheduledDays
in: query
description: Parse & return dates each journey is valid on?
description: Parse & return dates the journey is valid on?, returns a field `serviceDays` (instead of `scheduledDays` in hafas-client!) with a different, human-readable structure
schema:
type: boolean
default: false
- name: notOnlyFastRoutes
in: query
description: if true, also show journeys that are mathematically non-optimal
schema:
type: boolean
default: false
- name: bestprice
in: query
description: search for lowest prices across the entire day
schema:
type: boolean
default: false
@ -385,13 +397,14 @@ paths:
- bahncard-2nd-25
- bahncard-1st-50
- bahncard-2nd-50
- bahncard-1st-100
- bahncard-2nd-100
- vorteilscard
- halbtaxabo-railplus
- halbtaxabo
- voordeelurenabo-railplus
- voordeelurenabo
- shcard
- generalabonnement
- generalabonnement-1st
- generalabonnement-2nd
- nl-40
- at-klimaticket
- name: firstClass
in: query
description: Search for first-class options?
@ -600,7 +613,7 @@ paths:
default: true
- name: linesOfStops
in: query
description: Parse & return lines of each stop/station?
description: not supported
schema:
type: boolean
default: false
@ -682,7 +695,19 @@ paths:
default: true
- name: scheduledDays
in: query
description: Parse & return dates the journey is valid on?
description: Parse & return dates the journey is valid on?, returns a field `serviceDays` (instead of `scheduledDays` in hafas-client!) with a different, human-readable structure
schema:
type: boolean
default: false
- name: notOnlyFastRoutes
in: query
description: if true, also show journeys that are mathematically non-optimal
schema:
type: boolean
default: false
- name: bestprice
in: query
description: search for lowest prices across the entire day
schema:
type: boolean
default: false
@ -1862,11 +1887,11 @@ components:
default: false
type: boolean
subStops:
description: parse & expose sub-stops of stations?
description: not supported
default: false
type: boolean
entrances:
description: parse & expose entrances of stops/stations?
description: not supported
default: true
type: boolean
remarks:
@ -1970,11 +1995,11 @@ components:
default: false
type: boolean
subStops:
description: parse & expose sub-stops of stations?
description: not supported
default: true
type: boolean
entrances:
description: parse & expose entrances of stops/stations?
description: not supported
default: true
type: boolean
remarks:
@ -2009,15 +2034,15 @@ components:
default: true
type: boolean
subStops:
description: parse & expose sub-stops of stations?
description: not supported
default: false
type: boolean
entrances:
description: parse & expose entrances of stops/stations?
description: not supported
default: true
type: boolean
linesOfStops:
description: parse & expose lines at each stop/station?
description: not supported
default: false
type: boolean
language:
@ -2036,11 +2061,11 @@ components:
default: false
type: boolean
subStops:
description: parse & expose sub-stops of stations?
description: not supported
default: true
type: boolean
entrances:
description: parse & expose entrances of stops/stations?
description: not supported
default: true
type: boolean
remarks:
@ -2063,11 +2088,11 @@ components:
default: false
type: boolean
subStops:
description: parse & expose sub-stops of stations?
description: not supported
default: true
type: boolean
entrances:
description: parse & expose entrances of stops/stations?
description: not supported
default: true
type: boolean
remarks:
@ -2087,7 +2112,7 @@ components:
type: string
format: date-time
direction:
description: only show departures heading to this station
description: only show departures heading to this station, only supported for `dbweb` profile
default: undefined
type: string
line:
@ -2103,11 +2128,11 @@ components:
default: 10
type: number
subStops:
description: parse & expose sub-stops of stations?
description: not supported
default: true
type: boolean
entrances:
description: parse & expose entrances of stops/stations?
description: not supported
default: true
type: boolean
linesOfStops:
@ -2124,7 +2149,7 @@ components:
type: boolean
includeRelatedStations:
description: departures at related stations
default: false
default: true
type: boolean
products:
$ref: '#/components/schemas/Products'
@ -2181,7 +2206,7 @@ components:
default: undefined
type: number
poi:
description: return points of interest?
description: not supported
default: false
type: boolean
stops:
@ -2193,15 +2218,15 @@ components:
description: products
default: undefined
subStops:
description: parse & expose sub-stops of stations?
description: not supported
default: true
type: boolean
entrances:
description: parse & expose entrances of stops/stations?
description: not supported
default: true
type: boolean
linesOfStops:
description: parse & expose lines at each stop/station?
description: not supported
default: false
type: boolean
language:
@ -2229,11 +2254,11 @@ components:
description: products
default: undefined
subStops:
description: parse & expose sub-stops of stations?
description: not supported
default: true
type: boolean
entrances:
description: parse & expose entrances of stops/stations?
description: not supported
default: true
type: boolean
polylines:
@ -2271,11 +2296,11 @@ components:
default: 20
type: number
subStops:
description: parse & expose sub-stops of stations?
description: not supported
default: true
type: boolean
entrances:
description: parse & expose entrances of stops/stations?
description: not supported
default: true
type: boolean
polylines:

View file

@ -134,7 +134,7 @@ Unexpected errors e.g. due to bugs in `db-vendo-client` itself aside, it
Each `HafasError` error has the following properties:
- `isHafasError` Always `true`. Allows you to differente HAFAS-related errors from e.g. network errors.
- `isHafasError` Always `true`. Allows you to distinguish HAFAS-related errors from e.g. network errors.
- `code` A string representing the error type for all other error classes, e.g. `INVALID_REQUEST` for `HafasInvalidRequestError`. `null` for plain `HafasError`s.
- `isCausedByServer` Boolean, telling you if the HAFAS endpoint says that it couldn't process your request because *it* is unavailable/broken.
- `hafasCode` A HAFAS-specific error code, if the HAFAS endpoint returned one; e.g. `H890` when no journeys could be found. `null` otherwise.

View file

@ -1,6 +1,6 @@
# `stop(id, [opt])`
This endpoint is only available with `dbnav` profile.
This endpoint is not available with `dbweb` profile.
`id` must be in one of these formats:

62
eslint.config.js Normal file
View file

@ -0,0 +1,62 @@
import eslintPluginJs from '@eslint/js';
import eslintPluginStylistic from '@stylistic/eslint-plugin';
import globals from 'globals';
const config = [
eslintPluginJs.configs.recommended,
eslintPluginStylistic.configs['all-flat'],
{
files: ['**/*.js'],
languageOptions: {
ecmaVersion: 'latest',
globals: {
...globals.node,
},
sourceType: 'module',
},
rules: {
'@stylistic/array-bracket-newline': ['error', 'consistent'],
'@stylistic/array-element-newline': ['error', 'consistent'],
'@stylistic/arrow-parens': 'off',
'@stylistic/comma-dangle': ['error', 'always-multiline'],
'@stylistic/dot-location': ['error', 'property'],
'@stylistic/function-call-argument-newline': ['error', 'consistent'],
'@stylistic/function-paren-newline': 'off',
'@stylistic/indent': ['error', 'tab'],
'@stylistic/indent-binary-ops': ['error', 'tab'],
'@stylistic/max-len': 'off',
'@stylistic/multiline-comment-style': 'off',
'@stylistic/multiline-ternary': ['error', 'always-multiline'],
'@stylistic/newline-per-chained-call': ['error', {ignoreChainWithDepth: 1}],
'@stylistic/no-extra-parens': 'off',
'@stylistic/no-mixed-operators': 'off',
'@stylistic/no-tabs': 'off',
'@stylistic/object-property-newline': 'off',
'@stylistic/one-var-declaration-per-line': 'off',
'@stylistic/operator-linebreak': ['error', 'before'],
'@stylistic/padded-blocks': 'off',
'@stylistic/quote-props': ['error', 'consistent-as-needed'],
'@stylistic/quotes': ['error', 'single'],
'curly': 'error',
'no-implicit-coercion': 'error',
'no-unused-vars': [
'error',
{
vars: 'all',
args: 'none',
ignoreRestSiblings: false,
},
],
},
},
{
files: ['test/**', '**/example.js'],
rules: {
'no-unused-vars': 'off',
'@stylistic/semi': 'off',
},
},
];
export default config;

View file

@ -6,9 +6,10 @@ const c = {
VOORDEELURENABO: Symbol('Voordeelurenabo'),
SHCARD: Symbol('SH-Card'),
GENERALABONNEMENT: Symbol('General-Abonnement'),
NL_40: Symbol('NL-40%'),
AT_KLIMATICKET: Symbol('AT-KlimaTicket'),
};
// see https://gist.github.com/juliuste/202bb04f450a79f8fa12a2ec3abcd72d
const formatLoyaltyCard = (data) => {
if (!data) {
return {
@ -19,7 +20,7 @@ const formatLoyaltyCard = (data) => {
const cls = data.class === 1 ? 'KLASSE_1' : 'KLASSE_2';
if (data.type.toString() === c.BAHNCARD.toString()) {
return {
art: 'BAHNCARD' + data.discount,
art: 'BAHNCARD' + (data.business ? 'BUSINESS' : '') + data.discount,
klasse: cls,
};
}
@ -35,13 +36,24 @@ const formatLoyaltyCard = (data) => {
klasse: 'KLASSENLOS',
};
}
// TODO Rest
if (data.type.toString() === c.GENERALABONNEMENT.toString()) {
return {
art: 'CH-GENERAL-ABONNEMENT',
klasse: cls,
};
}
if (data.type.toString() === c.NL_40.toString()) {
return {
art: 'NL-40_OHNE_RAILPLUS',
klasse: 'KLASSENLOS',
};
}
if (data.type.toString() === c.AT_KLIMATICKET.toString()) {
return {
art: 'KLIMATICKET_OE',
klasse: 'KLASSENLOS',
};
}
return {
art: 'KEINE_ERMAESSIGUNG',
klasse: 'KLASSENLOS',

View file

@ -1,4 +1,4 @@
import isObj from 'lodash/isObject.js';
const isObj = element => element !== null && 'object' === typeof element && !Array.isArray(element);
const hasProp = (o, k) => Object.prototype.hasOwnProperty.call(o, k);

View file

@ -1,49 +0,0 @@
const formatStationBoardReq = (ctx, station, type) => {
const {profile, opt} = ctx;
return {
endpoint: profile.boardEndpoint,
path: (type == 'departures' ? 'departure' : 'arrival') + '/' + station,
query: {
// TODO direction, fields below
modeOfTransport: profile.formatProductsFilter(ctx, opt.products || {}, 'ris'),
timeStart: profile.formatTime(profile, opt.when, true),
timeEnd: profile.formatTime(profile, opt.when.getTime() + opt.duration * 60 * 1000, true),
expandTimeFrame: 'TIME_END', // TODO impact?
},
method: 'get',
};
};
/*
TODO separate RIS::Boards profile?
const formatRisStationBoardReq = (ctx, station, type) => {
const {profile, opt} = ctx;
return {
endpoint: profile.boardEndpoint,
path: type + '/' + station,
query: {
// TODO direction
filterTransports: profile.formatProductsFilter(ctx, opt.products || {}, 'ris'),
timeStart: profile.formatTime(profile, opt.when, true),
timeEnd: profile.formatTime(profile, opt.when.getTime() + opt.duration * 60 * 1000, true),
maxViaStops: opt.stopovers ? undefined : 0,
includeStationGroup: opt.includeRelatedStations,
maxTransportsPerType: opt.results === Infinity ? undefined : opt.results,
includeMessagesDisruptions: opt.remarks,
sortBy: 'TIME_SCHEDULE',
},
method: 'get',
headers: {
'Db-Client-Id': process.env.DB_CLIENT_ID,
'Db-Api-Key': process.env.DB_API_KEY,
'Accept': 'application/vnd.de.db.ris+json',
},
};
};
*/
export {
formatStationBoardReq,
};

View file

@ -1,10 +1,10 @@
import isObj from 'lodash/isObject.js';
import distance from 'gps-distance';
import readStations from 'db-hafas-stations';
import {defaultProfile} from './lib/default-profile.js';
import {validateProfile} from './lib/validate-profile.js';
const isObj = element => element !== null && 'object' === typeof element && !Array.isArray(element);
// background info: https://github.com/public-transport/hafas-client/issues/286
const FORBIDDEN_USER_AGENTS = [
'my-awesome-program', // previously used in readme.md, p/*/readme.md & docs/*.md
@ -27,32 +27,36 @@ const validateLocation = (loc, name = 'location') => {
}
};
const loadEnrichedStationData = (profile) => new Promise((resolve, reject) => {
const loadEnrichedStationData = async (profile) => {
const dbHafasStations = await import('db-hafas-stations');
const items = {};
readStations.full()
.on('data', (station) => {
for await (const station of dbHafasStations.readFullStations()) {
items[station.id] = station;
})
.once('end', () => {
items[station.name] = station;
}
if (profile.DEBUG) {
console.log('Loaded station index.');
}
resolve(items);
})
.once('error', (err) => {
reject(err);
});
});
return items;
};
const applyEnrichedStationData = async (ctx, shouldLoadEnrichedStationData) => {
const {profile, common} = ctx;
if (shouldLoadEnrichedStationData && !common.locations) {
const locations = await loadEnrichedStationData(profile);
common.locations = locations;
}
};
const createClient = (profile, userAgent, opt = {}) => {
profile = Object.assign({}, defaultProfile, profile);
validateProfile(profile);
const common = {};
if (opt.enrichStations !== false) {
loadEnrichedStationData(profile)
.then(locations => {
common.locations = locations;
});
let shouldLoadEnrichedStationData = false;
if (typeof opt.enrichStations === 'function') {
profile.enrichStation = opt.enrichStations;
} else if (opt.enrichStations !== false) {
shouldLoadEnrichedStationData = true;
}
if ('string' !== typeof userAgent) {
@ -63,6 +67,7 @@ const createClient = (profile, userAgent, opt = {}) => {
}
const _stationBoard = async (station, type, resultsField, parse, opt = {}) => {
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
if (isObj(station) && station.id) {
station = station.id;
} else if ('string' !== typeof station) {
@ -73,10 +78,10 @@ const createClient = (profile, userAgent, opt = {}) => {
throw new TypeError('type must be a non-empty string.');
}
if (!profile.departuresGetPasslist && 'stopovers' in opt) {
if (!profile.departuresGetPasslist && opt.stopovers) {
throw new Error('opt.stopovers is not supported by this endpoint');
}
if (!profile.departuresStbFltrEquiv && 'includeRelatedStations' in opt) { // TODO artificially filter?
if (!profile.departuresStbFltrEquiv && 'includeRelatedStations' in opt) {
throw new Error('opt.includeRelatedStations is not supported by this endpoint');
}
@ -94,6 +99,7 @@ const createClient = (profile, userAgent, opt = {}) => {
// departures at related stations
// e.g. those that belong together on the metro map.
includeRelatedStations: true,
moreStops: null, // also include departures/arrivals for array of up to nine additional station evaNumbers (not supported with dbnav and dbweb)
}, opt);
opt.when = new Date(opt.when || Date.now());
if (Number.isNaN(Number(opt.when))) {
@ -105,9 +111,15 @@ const createClient = (profile, userAgent, opt = {}) => {
const {res} = await profile.request({profile, opt}, userAgent, req);
const ctx = {profile, opt, common, res};
const results = (res[resultsField] || res.items || res.bahnhofstafelAbfahrtPositionen || res.bahnhofstafelAnkunftPositionen)
let results = (res[resultsField] || res.items || res.bahnhofstafelAbfahrtPositionen || res.bahnhofstafelAnkunftPositionen || res.entries.flat())
.map(res => parse(ctx, res)); // TODO sort?, slice
if (!opt.includeRelatedStations) {
results = results.filter(r => !r.stop?.id || r.stop.id == station);
}
if (opt.direction) {
results = results.filter(r => !r.nextStopovers || r.nextStopovers.find(s => s.stop?.id == opt.direction || s.stop?.name == opt.direction));
}
return {
[resultsField]: results,
realtimeDataUpdatedAt: null, // TODO
@ -122,6 +134,7 @@ const createClient = (profile, userAgent, opt = {}) => {
};
const journeys = async (from, to, opt = {}) => {
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
if ('earlierThan' in opt && 'laterThan' in opt) {
throw new TypeError('opt.earlierThan and opt.laterThan are mutually exclusive.');
}
@ -166,6 +179,10 @@ const createClient = (profile, userAgent, opt = {}) => {
entrances: true, // parse & expose entrances of stops/stations?
remarks: true, // parse & expose hints & warnings?
scheduledDays: false, // parse & expose dates each journey is valid on?
notOnlyFastRoutes: false, // if true, also show routes that are mathematically non-optimal
bestprice: false, // search for lowest prices across the entire day
deutschlandTicketDiscount: false,
deutschlandTicketConnectionsOnly: false,
}, opt);
if (opt.when !== undefined) {
@ -191,9 +208,15 @@ const createClient = (profile, userAgent, opt = {}) => {
const req = profile.formatJourneysReq({profile, opt}, from, to, when, outFrwd, journeysRef);
const {res} = await profile.request({profile, opt}, userAgent, req);
const ctx = {profile, opt, common, res};
const verbindungen = Number.isInteger(opt.results) ? res.verbindungen.slice(0, opt.results) : res.verbindungen;
if (opt.bestprice) {
res.verbindungen = (res.intervalle || res.tagesbestPreisIntervalle).flatMap(i => i.verbindungen.map(v => ({...v, ...v.verbindung})));
}
const verbindungen = Number.isInteger(opt.results) && opt.results != 3 ? res.verbindungen.slice(0, opt.results) : res.verbindungen; // TODO remove default from hafas-rest-api
const journeys = verbindungen
.map(j => profile.parseJourney(ctx, j));
if (opt.bestprice) {
journeys.sort((a, b) => a.price?.amount - b.price?.amount);
}
return {
earlierRef: res.verbindungReference?.earlier || res.frueherContext || null,
@ -204,6 +227,8 @@ const createClient = (profile, userAgent, opt = {}) => {
};
const refreshJourney = async (refreshToken, opt = {}) => {
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
if ('string' !== typeof refreshToken || !refreshToken) {
throw new TypeError('refreshToken must be a non-empty string.');
}
@ -216,6 +241,8 @@ const createClient = (profile, userAgent, opt = {}) => {
entrances: true, // parse & expose entrances of stops/stations?
remarks: true, // parse & expose hints & warnings?
scheduledDays: false, // parse & expose dates the journey is valid on?
deutschlandTicketDiscount: false,
deutschlandTicketConnectionsOnly: false,
}, opt);
const req = profile.formatRefreshJourneyReq({profile, opt}, refreshToken);
@ -230,6 +257,8 @@ const createClient = (profile, userAgent, opt = {}) => {
};
const locations = async (query, opt = {}) => {
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
if (!isNonEmptyString(query)) {
throw new TypeError('query must be a non-empty string.');
}
@ -256,6 +285,8 @@ const createClient = (profile, userAgent, opt = {}) => {
};
const stop = async (stop, opt = {}) => {
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
if (isObj(stop) && stop.id) {
stop = stop.id;
} else if ('string' !== typeof stop) {
@ -277,6 +308,8 @@ const createClient = (profile, userAgent, opt = {}) => {
};
const nearby = async (location, opt = {}) => {
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
validateLocation(location, 'location');
opt = Object.assign({
@ -307,6 +340,8 @@ const createClient = (profile, userAgent, opt = {}) => {
};
const trip = async (id, opt = {}) => {
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
if (!isNonEmptyString(id)) {
throw new TypeError('id must be a non-empty string.');
}
@ -334,6 +369,8 @@ const createClient = (profile, userAgent, opt = {}) => {
// todo [breaking]: rename to trips()?
const tripsByName = async (_lineNameOrFahrtNr = '*', _opt = {}) => {
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
throw new Error('not implemented');
};
@ -360,4 +397,5 @@ const createClient = (profile, userAgent, opt = {}) => {
export {
createClient,
loadEnrichedStationData,
};

109
lib/api-parsers.js Normal file
View file

@ -0,0 +1,109 @@
import {data as cards} from '../format/loyalty-cards.js';
import {parseBoolean, parseInteger, parseArrayOfStrings} from 'hafas-rest-api/lib/parse.js';
const typesByName = new Map([
['bahncard-1st-25', {type: cards.BAHNCARD, discount: 25, class: 1}],
['bahncard-2nd-25', {type: cards.BAHNCARD, discount: 25, class: 2}],
['bahncard-1st-50', {type: cards.BAHNCARD, discount: 50, class: 1}],
['bahncard-2nd-50', {type: cards.BAHNCARD, discount: 50, class: 2}],
['bahncard-1st-100', {type: cards.BAHNCARD, discount: 100, class: 1}],
['bahncard-2nd-100', {type: cards.BAHNCARD, discount: 100, class: 2}],
['vorteilscard', {type: cards.VORTEILSCARD}],
['halbtaxabo-railplus', {type: cards.HALBTAXABO}],
['halbtaxabo', {type: cards.HALBTAXABO}],
['voordeelurenabo-railplus', {type: cards.VOORDEELURENABO}],
['voordeelurenabo', {type: cards.VOORDEELURENABO}],
['shcard', {type: cards.SHCARD}],
['generalabonnement-1st', {type: cards.GENERALABONNEMENT, class: 1}],
['generalabonnement-2nd', {type: cards.GENERALABONNEMENT, class: 2}],
['generalabonnement', {type: cards.GENERALABONNEMENT}],
['nl-40', {type: cards.NL_40}],
['at-klimaticket', {type: cards.AT_KLIMATICKET}],
]);
const types = Array.from(typesByName.keys());
const parseLoyaltyCard = (key, val) => {
if (typesByName.has(val)) {
return typesByName.get(val);
}
if (!val) {
return null;
}
throw new Error(key + ' must be one of ' + types.join(', '));
};
const parseArrayOr = (parseEntry) => {
return (key, val) => {
if (Array.isArray(val)) {
return val.map(e => parseEntry(key, e));
}
return parseEntry(key, val);
};
};
const mapRouteParsers = (route, parsers) => {
if (route.includes('journey')) {
return {
...parsers,
firstClass: {
description: 'Search for first-class options?',
type: 'boolean',
default: false,
parse: parseBoolean,
},
loyaltyCard: {
description: 'Type of loyalty card in use.',
type: 'string',
enum: types,
defaultStr: '*none*',
parse: parseArrayOr(parseLoyaltyCard),
},
age: {
description: 'Age of traveller',
type: 'integer',
defaultStr: '*adult*',
parse: parseArrayOr(parseInteger),
},
notOnlyFastRoutes: {
description: 'If true, also show routes that are mathematically non-optimal',
type: 'boolean',
default: false,
parse: parseBoolean,
},
bestprice: {
description: 'Search for lowest prices across the entire day',
type: 'boolean',
default: false,
parse: parseBoolean,
},
deutschlandTicketDiscount: {
description: 'Calculate ticket prices assuming Deutschlandticket is present',
type: 'boolean',
default: false,
parse: parseBoolean,
},
deutschlandTicketConnectionsOnly: {
description: 'Only return journeys that can be used with the Deutschlandticket',
type: 'boolean',
default: false,
parse: parseBoolean,
},
};
}
if (route.includes('departures') || route.includes('arrivals')) {
return {
...parsers,
moreStops: {
description: 'Also include departures/arrivals for up to nine comma-separated station evaNumbers (not supported with dbnav and dbweb)',
type: 'string',
default: '',
parse: parseArrayOfStrings,
},
};
}
return parsers;
};
export {
mapRouteParsers,
};

View file

@ -2,10 +2,6 @@ import {request} from '../lib/request.js';
import {products} from '../lib/products.js';
import {ageGroup, ageGroupFromAge, ageGroupLabel} from './age-group.js';
import {formatStationBoardReq} from '../format/station-board-req.js';
import {formatTripReq} from '../format/trip-req.js';
import {formatNearbyReq} from '../format/nearby-req.js';
import {parseDateTime} from '../parse/date-time.js';
import {parsePlatform} from '../parse/platform.js';
import {parseProducts} from '../parse/products.js';
@ -16,7 +12,7 @@ import {parseTrip} from '../parse/trip.js';
import {parseJourneyLeg} from '../parse/journey-leg.js';
import {parseJourney} from '../parse/journey.js';
import {parseLine} from '../parse/line.js';
import {parseLocation} from '../parse/location.js';
import {parseLocation, enrichStation} from '../parse/location.js';
import {parsePolyline} from '../parse/polyline.js';
import {parseOperator} from '../parse/operator.js';
import {parseRemarks, parseCancelled} from '../parse/remarks.js';
@ -37,7 +33,7 @@ import {formatTravellers} from '../format/travellers.js';
import {formatLoyaltyCard} from '../format/loyalty-cards.js';
import {formatTransfers} from '../format/transfers.js';
const DEBUG = (/(^|,)hafas-client(,|$)/).test(process.env.DEBUG || '');
const DEBUG = (/(^|,)hafas-client(,|$)/).test(typeof process !== 'undefined' ? process.env.DEBUG || '' : '');
const logRequest = DEBUG
? (_, req, reqId) => console.error(String(req.body))
: () => { };
@ -61,13 +57,13 @@ const defaultProfile = {
logRequest,
logResponse,
formatStationBoardReq,
formatLocationsReq: notImplemented,
formatStopReq: notImplemented,
formatTripReq,
formatNearbyReq,
formatJourneysReq: notImplemented,
formatRefreshJourneyReq: notImplemented,
formatTripReq: notImplemented,
formatNearbyReq: notImplemented,
formatLocationsReq: notImplemented,
formatStopReq: notImplemented,
formatStationBoardReq: notImplemented,
transformJourneysQuery: id,
parseDateTime,
@ -82,6 +78,7 @@ const defaultProfile = {
parseLine,
parseStationName: id,
parseLocation,
enrichStation,
parsePolyline,
parseOperator,
parseRemarks,
@ -111,7 +108,7 @@ const defaultProfile = {
journeysOutFrwd: true,
departuresGetPasslist: false,
departuresStbFltrEquiv: false,
departuresStbFltrEquiv: true,
trip: true,
radar: false,
refreshJourney: true,

View file

@ -111,7 +111,7 @@ const byErrorCode = Object.assign(Object.create(null), {
},
PROBLEMS: {
Error: HafasServerError,
message: 'an unknown problem occured during search',
message: 'an unknown problem occurred during search',
props: {
shouldRetry: true,
},
@ -212,7 +212,7 @@ const byErrorCode = Object.assign(Object.create(null), {
},
H9230: {
Error: HafasServerError,
message: 'journeys search: an internal error occured',
message: 'journeys search: an internal error occurred',
props: {
shouldRetry: true,
},

View file

@ -1,49 +1,28 @@
import ProxyAgent from 'https-proxy-agent';
import {isIP} from 'net';
import {Agent as HttpsAgent} from 'https';
import roundRobin from '@derhuerst/round-robin-scheduler';
import {randomBytes} from 'crypto';
import {stringify} from 'qs';
import {Request, fetch} from 'cross-fetch';
import {parse as parseContentType} from 'content-type';
import {HafasError} from './errors.js';
const proxyAddress = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || null;
const localAddresses = process.env.LOCAL_ADDRESS || null;
const proxyAddress = typeof process !== 'undefined' && (process.env.HTTPS_PROXY || process.env.HTTP_PROXY) || null;
if (proxyAddress && localAddresses) {
console.error('Both env vars HTTPS_PROXY/HTTP_PROXY and LOCAL_ADDRESS are not supported.');
process.exit(1);
}
let getAgent = () => undefined;
const plainAgent = new HttpsAgent({
keepAlive: true,
});
let getAgent = () => plainAgent;
if (proxyAddress) {
const agent = new ProxyAgent(proxyAddress, {
const setupProxy = async () => {
if (proxyAddress && !getAgent()) {
const a = await import('https-proxy-agent');
const agent = new a.default.HttpsProxyAgent(proxyAddress, {
keepAlive: true,
keepAliveMsecs: 10 * 1000, // 10s
});
getAgent = () => agent;
} else if (localAddresses) {
const agents = process.env.LOCAL_ADDRESS.split(',')
.map((addr) => {
const family = isIP(addr);
if (family === 0) {
throw new Error('invalid local address:' + addr);
}
return new HttpsAgent({
localAddress: addr, family,
keepAlive: true,
});
});
const pool = roundRobin(agents);
getAgent = () => pool.get();
}
};
const id = randomBytes(3)
const randomBytesHexString = length => [...Array(length)].map(() => Math.floor(Math.random() * 16)
.toString(16))
.join('');
const id = randomBytesHexString(6)
.toString('hex');
const randomizeUserAgent = (userAgent) => {
let ua = userAgent;
@ -88,21 +67,23 @@ const checkIfResponseIsOk = (_) => {
};
};
if (body.fehlerNachricht) { // TODO better handling
if (body.fehlerNachricht || body.errors) { // TODO better handling
const {Error: HafasError, message, props} = getError(body);
throw new HafasError(message, body.err, {...errProps, ...props});
throw new HafasError(message, body.err || body.errors, {...errProps, ...props});
}
};
const request = async (ctx, userAgent, reqData) => {
const {profile, opt} = ctx;
await setupProxy();
const endpoint = reqData.endpoint;
delete reqData.endpoint;
const rawReqBody = profile.transformReqBody(ctx, reqData.body);
const req = profile.transformReq(ctx, {
const reqOptions = profile.transformReq(ctx, {
agent: getAgent(),
keepalive: true,
method: reqData.method,
// todo: CORS? referrer policy?
body: JSON.stringify(rawReqBody),
@ -114,7 +95,6 @@ const request = async (ctx, userAgent, reqData) => {
'user-agent': profile.randomizeUserAgent
? randomizeUserAgent(userAgent)
: userAgent,
'connection': 'keep-alive', // prevent excessive re-connecting
...reqData.headers,
},
redirect: 'follow',
@ -122,15 +102,14 @@ const request = async (ctx, userAgent, reqData) => {
});
let url = endpoint + (reqData.path || '');
if (req.query) {
url += '?' + stringify(req.query, {arrayFormat: 'brackets', encodeValuesOnly: true});
if (reqOptions.query) {
url += '?' + stringify(reqOptions.query, {arrayFormat: 'brackets', encodeValuesOnly: true});
}
const reqId = randomBytes(3)
.toString('hex');
const fetchReq = new Request(url, req);
const reqId = randomBytesHexString(6);
const fetchReq = new Request(url, reqOptions);
profile.logRequest(ctx, fetchReq, reqId);
const res = await fetch(url, req);
const res = await fetch(url, reqOptions);
const errProps = {
// todo [breaking]: assign as non-enumerable property
@ -142,6 +121,7 @@ const request = async (ctx, userAgent, reqData) => {
if (!res.ok) {
// todo [breaking]: make this a FetchError or a HafasClientError?
console.log(JSON.stringify(res), await res.text());
const err = new Error(res.statusText);
Object.assign(err, errProps);
throw err;
@ -150,7 +130,7 @@ const request = async (ctx, userAgent, reqData) => {
let cType = res.headers.get('content-type');
if (cType) {
const {type} = parseContentType(cType);
if (type !== req.headers['Accept']) {
if (type !== reqOptions.headers['Accept']) {
throw new HafasError('invalid/unsupported response content-type: ' + cType, null, errProps);
}
}

View file

@ -1,26 +1,67 @@
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
const baseProfile = require('./base.json');
import dbnavBase from '../dbnav/base.json' with { type: 'json' };
import dbregioguideBase from '../dbregioguide/base.json' with { type: 'json' };
import {products} from '../../lib/products.js';
import {formatJourneysReq, formatRefreshJourneyReq} from './journeys-req.js';
// journeys()
import {formatJourneysReq} from '../dbnav/journeys-req.js';
const {journeysEndpoint} = dbnavBase;
// refreshJourneys()
import {formatRefreshJourneyReq} from '../dbnav/journeys-req.js';
const {refreshJourneysEndpointTickets, refreshJourneysEndpointPolyline} = dbnavBase;
// locations()
import {formatLocationsReq} from '../dbnav/locations-req.js';
import {formatLocationFilter} from '../dbnav/location-filter.js';
const {locationsEndpoint} = dbnavBase;
// stop()
import {formatStopReq} from '../dbnav/stop-req.js';
import {parseStop} from '../dbnav/parse-stop.js';
const {stopEndpoint} = dbnavBase;
// nearby()
import {formatNearbyReq} from '../dbnav/nearby-req.js';
const {nearbyEndpoint} = dbnavBase;
// trip()
import {formatTripReq} from './trip-req.js';
import {formatLocationFilter} from './location-filter.js';
import {formatLocationsReq} from './locations-req.js';
const tripEndpoint_dbnav = dbnavBase.tripEndpoint;
const tripEndpoint_dbregioguide = dbregioguideBase.tripEndpoint;
// arrivals(), departures()
import {formatStationBoardReq} from '../dbregioguide/station-board-req.js';
const {boardEndpoint} = dbregioguideBase;
const profile = {
...baseProfile,
locale: 'de-DE',
timezone: 'Europe/Berlin',
products,
formatJourneysReq,
journeysEndpoint,
formatRefreshJourneyReq,
refreshJourneysEndpointTickets, refreshJourneysEndpointPolyline,
formatLocationsReq, formatLocationFilter,
locationsEndpoint,
formatStopReq, parseStop,
stopEndpoint,
formatNearbyReq,
nearbyEndpoint,
formatTripReq,
formatLocationsReq,
formatLocationFilter,
tripEndpoint_dbnav, tripEndpoint_dbregioguide,
formatStationBoardReq,
boardEndpoint,
};
export {
profile,
};

View file

@ -1,15 +1,16 @@
import {formatTripReq as hafasFormatTripReq} from '../../format/trip-req.js';
import {formatTripReq as hafasFormatTripReq} from '../dbnav/trip-req.js';
import {formatTripReq as risTripReq} from '../dbregioguide/trip-req.js';
const formatTripReq = ({profile, opt}, id) => {
const _profile = {...profile};
if (id.includes('#')) {
return hafasFormatTripReq({profile, opt}, id);
_profile['tripEndpoint'] = profile.tripEndpoint_dbnav;
return hafasFormatTripReq({profile: _profile, opt}, id);
}
return {
endpoint: profile.regioGuideTripEndpoint,
path: id,
method: 'get',
};
_profile['tripEndpoint'] = profile.tripEndpoint_dbregioguide;
return risTripReq({profile: _profile, opt}, id);
};
export {

4
p/dbbahnhof/base.json Normal file
View file

@ -0,0 +1,4 @@
{
"boardEndpoint": "https://www.bahnhof.de/api/boards/",
"defaultLanguage": "en"
}

31
p/dbbahnhof/index.js Normal file
View file

@ -0,0 +1,31 @@
import baseProfile from './base.json' with { type: 'json' };
import {products} from '../../lib/products.js';
import {formatStationBoardReq} from './station-board-req.js';
const profile = {
...baseProfile,
locale: 'de-DE',
timezone: 'Europe/Berlin',
products,
formatStationBoardReq,
journeysOutFrwd: false,
departuresGetPasslist: true,
departuresStbFltrEquiv: true,
trip: false,
radar: false,
refreshJourney: false,
journeysFromTrip: false,
refreshJourneyUseOutReconL: false,
tripsByName: false,
remarks: false,
remarksGetPolyline: false,
reachableFrom: false,
lines: false,
};
export {
profile,
};

View file

@ -0,0 +1,30 @@
import {stringify} from 'qs';
const formatStationBoardReq = (ctx, station, type) => {
const {profile, opt} = ctx;
if (opt.departure || opt.arrival) {
throw new Error('opt.departure/opt.arrival is not supported for profile dbbahnhof, can only query for current time.');
}
const evaNumbers = [station];
if (opt.moreStops) {
evaNumbers.push(...opt.moreStops);
}
const query = {
filterTransports: profile.formatProductsFilter(ctx, opt.products || {}, 'ris_alt'),
evaNumbers: evaNumbers,
duration: opt.duration,
sortBy: 'TIME_SCHEDULE',
locale: opt.language,
};
return {
endpoint: profile.boardEndpoint,
path: type + '?' + stringify(query, {arrayFormat: 'repeat', encodeValuesOnly: true}),
method: 'get',
};
};
export {
formatStationBoardReq,
};

View file

@ -1,5 +1,6 @@
{
"journeysEndpoint": "https://app.vendo.noncd.db.de/mob/angebote/fahrplan",
"bestpriceEndpoint": "https://app.vendo.noncd.db.de/mob/angebote/tagesbestpreis",
"refreshJourneysEndpointTickets": "https://app.vendo.noncd.db.de/mob/angebote/recon",
"refreshJourneysEndpointPolyline": "https://app.vendo.noncd.db.de/mob/trip/recon",
"locationsEndpoint": "https://app.vendo.noncd.db.de/mob/location/search",

View file

@ -1,7 +1,4 @@
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
const baseProfile = require('./base.json');
import baseProfile from './base.json' with { type: 'json' };
import {products} from '../../lib/products.js';
import {formatJourneysReq, formatRefreshJourneyReq} from './journeys-req.js';
import {formatTripReq} from './trip-req.js';
@ -11,6 +8,7 @@ import {formatStopReq} from './stop-req.js';
import {formatNearbyReq} from './nearby-req.js';
import {formatStationBoardReq} from './station-board-req.js';
import {parseStop} from './parse-stop.js';
import {parseJourney} from './parse-journey.js';
const profile = {
...baseProfile,
@ -27,6 +25,7 @@ const profile = {
formatStationBoardReq,
formatLocationFilter,
parseJourney,
parseStop,
};

View file

@ -9,6 +9,10 @@ const formatBaseJourneysReq = (ctx) => {
einstiegsTypList: [
'STANDARD',
],
fahrverguenstigungen: {
deutschlandTicketVorhanden: ctx.opt.deutschlandTicketDiscount,
nurDeutschlandTicketVerbindungen: ctx.opt.deutschlandTicketConnectionsOnly,
},
klasse: travellers.klasse,
reisendenProfil: {
reisende: travellers.reisende.map(t => {
@ -55,8 +59,11 @@ const formatJourneysReq = (ctx, from, to, when, outFrwd, journeysRef) => {
if (journeysRef) {
query.reiseHin.wunsch.context = journeysRef;
}
if (opt.notOnlyFastRoutes) {
query.reiseHin.wunsch.economic = true;
}
return {
endpoint: ctx.profile.journeysEndpoint,
endpoint: opt.bestprice ? profile.bestpriceEndpoint : profile.journeysEndpoint,
body: query,
headers: getHeaders('application/x.db.vendo.mob.verbindungssuche.v8+json'),
method: 'post',

34
p/dbnav/parse-journey.js Normal file
View file

@ -0,0 +1,34 @@
import {parseJourney as parseJourneyDefault} from '../../parse/journey.js';
const parseJourney = (ctx, jj) => {
const legs = (jj.verbindung || jj).verbindungsAbschnitte;
if (legs.length > 0) {
legs[0] = preprocessJourneyLeg(legs[0]);
}
if (legs.length > 1) {
legs[legs.length - 1] = preprocessJourneyLeg(legs.at(-1));
}
return parseJourneyDefault(ctx, jj);
};
const preprocessJourneyLeg = (pt) => { // fixes https://github.com/public-transport/db-vendo-client/issues/24
if (pt.typ === 'FUSSWEG' || pt.typ === 'TRANSFER') {
pt.ezAbgangsDatum = correctRealtimeTimeZone(pt.abgangsDatum, pt.ezAbgangsDatum);
pt.ezAnkunftsDatum = correctRealtimeTimeZone(pt.ankunftsDatum, pt.ezAnkunftsDatum);
}
return pt;
};
const correctRealtimeTimeZone = (planned, realtime) => {
if (planned && realtime) {
const timeZoneOffsetRegex = /([+-]\d\d:\d\d|Z)$/;
const timeZoneOffsetPlanned = timeZoneOffsetRegex.exec(planned)[0];
return realtime.replace(timeZoneOffsetRegex, timeZoneOffsetPlanned);
}
return realtime;
};
export {parseJourney};

5
p/dbregioguide/base.json Normal file
View file

@ -0,0 +1,5 @@
{
"tripEndpoint": "https://regio-guide.de/@prd/zupo-travel-information/api/public/ri/journey/",
"boardEndpoint": "https://regio-guide.de/@prd/zupo-travel-information/api/public/ri/board/",
"defaultLanguage": "en"
}

32
p/dbregioguide/index.js Normal file
View file

@ -0,0 +1,32 @@
import baseProfile from './base.json' with { type: 'json' };
import {products} from '../../lib/products.js';
import {formatTripReq} from './trip-req.js';
import {formatStationBoardReq} from './station-board-req.js';
const profile = {
...baseProfile,
locale: 'de-DE',
timezone: 'Europe/Berlin',
products,
formatTripReq,
formatStationBoardReq,
journeysOutFrwd: false,
departuresGetPasslist: false,
departuresStbFltrEquiv: true,
trip: false,
radar: false,
refreshJourney: false,
journeysFromTrip: false,
refreshJourneyUseOutReconL: false,
tripsByName: false,
remarks: false,
remarksGetPolyline: false,
reachableFrom: false,
lines: false,
};
export {
profile,
};

View file

@ -0,0 +1,24 @@
const formatStationBoardReq = (ctx, station, type) => {
const {profile, opt} = ctx;
if (opt.moreStops) {
station += ',' + opt.moreStops.join(',');
}
return {
endpoint: profile.boardEndpoint,
path: (type == 'departures' ? 'departure' : 'arrival') + '/' + station,
query: {
// TODO direction, fields below
modeOfTransport: profile.formatProductsFilter(ctx, opt.products || {}, 'ris'),
timeStart: profile.formatTime(profile, opt.when, true),
timeEnd: profile.formatTime(profile, opt.when.getTime() + opt.duration * 60 * 1000, true),
expandTimeFrame: 'TIME_END', // TODO impact?
},
method: 'get',
};
};
export {
formatStationBoardReq,
};

View file

@ -0,0 +1,11 @@
const formatTripReq = ({profile, opt}, id) => {
return {
endpoint: profile.tripEndpoint,
path: id,
method: 'get',
};
};
export {
formatTripReq,
};

4
p/dbris/base.json Normal file
View file

@ -0,0 +1,4 @@
{
"boardEndpoint": "https://apis.deutschebahn.com/db/apis/ris-boards/v1/public/",
"defaultLanguage": "en"
}

31
p/dbris/index.js Normal file
View file

@ -0,0 +1,31 @@
import baseProfile from './base.json' with { type: 'json' };
import {products} from '../../lib/products.js';
import {formatStationBoardReq} from './station-board-req.js';
const profile = {
...baseProfile,
locale: 'de-DE',
timezone: 'Europe/Berlin',
products,
formatStationBoardReq,
journeysOutFrwd: false,
departuresGetPasslist: true,
departuresStbFltrEquiv: true,
trip: false,
radar: false,
refreshJourney: false,
journeysFromTrip: false,
refreshJourneyUseOutReconL: false,
tripsByName: false,
remarks: false,
remarksGetPolyline: false,
reachableFrom: false,
lines: false,
};
export {
profile,
};

View file

@ -0,0 +1,34 @@
const formatStationBoardReq = (ctx, station, type) => {
const {profile, opt} = ctx;
const query = {
filterTransports: profile.formatProductsFilter(ctx, opt.products || {}, 'ris_alt'),
timeStart: profile.formatTime(profile, opt.when, true),
timeEnd: profile.formatTime(profile, opt.when.getTime() + opt.duration * 60 * 1000, true),
includeStationGroup: opt.includeRelatedStations,
maxTransportsPerType: opt.results === Infinity ? undefined : opt.results,
includeMessagesDisruptions: opt.remarks,
sortBy: 'TIME_SCHEDULE',
};
if (!opt.stopovers) {
query.maxViaStops = 0;
}
if (opt.moreStops) {
station += ',' + opt.moreStops.join(',');
}
return {
endpoint: profile.boardEndpoint,
path: type + '/' + station,
query: query,
method: 'get',
headers: {
'Db-Client-Id': process.env.DB_CLIENT_ID,
'Db-Api-Key': process.env.DB_API_KEY,
'Accept': 'application/vnd.de.db.ris+json',
},
};
};
export {
formatStationBoardReq,
};

View file

@ -1,11 +1,11 @@
{
"journeysEndpoint": "https://int.bahn.de/web/api/angebote/fahrplan",
"bestpriceEndpoint": "https://int.bahn.de/web/api/angebote/tagesbestpreis",
"refreshJourneysEndpointTickets": "https://int.bahn.de/web/api/angebote/recon",
"refreshJourneysEndpointPolyline": "https://int.bahn.de/web/api/reiseloesung/verbindung",
"locationsEndpoint": "https://int.bahn.de/web/api/reiseloesung/orte",
"nearbyEndpoint": "https://int.bahn.de/web/api/reiseloesung/orte/nearby",
"tripEndpoint": "https://int.bahn.de/web/api/reiseloesung/fahrt",
"regioGuideTripEndpoint": "https://regio-guide.de/@prd/zupo-travel-information/api/public/ri/journey/",
"boardEndpoint": "https://regio-guide.de/@prd/zupo-travel-information/api/public/ri/board/",
"boardEndpoint": "https://int.bahn.de/web/api/reiseloesung/",
"defaultLanguage": "en"
}

View file

@ -12,7 +12,7 @@ const regensburgHbf = '8000309'
let data = await client.locations('Berlin Jungfernheide')
// let data = await client.locations('Atze Musiktheater', {
// poi: true,
// addressses: false,
// addresses: false,
// fuzzy: false,
// })
// let data = await client.nearby({

30
p/dbweb/index.js Normal file
View file

@ -0,0 +1,30 @@
import baseProfile from './base.json' with { type: 'json' };
import {products} from '../../lib/products.js';
import {formatJourneysReq, formatRefreshJourneyReq} from './journeys-req.js';
import {formatLocationFilter} from './location-filter.js';
import {formatLocationsReq} from './locations-req.js';
import {formatStationBoardReq} from './station-board-req.js';
import {formatTripReq} from './trip-req.js';
import {formatNearbyReq} from './nearby-req.js';
const profile = {
...baseProfile,
locale: 'de-DE',
timezone: 'Europe/Berlin',
products,
formatJourneysReq,
formatRefreshJourneyReq,
formatTripReq,
formatNearbyReq,
formatLocationsReq,
formatStationBoardReq,
formatLocationFilter,
departuresGetPasslist: true,
};
export {
profile,
};

View file

@ -10,10 +10,10 @@ const formatJourneysReq = (ctx, from, to, when, outFrwd, journeysRef) => {
let query = {
maxUmstiege: transfers,
minUmstiegszeit: opt.transferTime,
deutschlandTicketVorhanden: false,
nurDeutschlandTicketVerbindungen: false,
deutschlandTicketVorhanden: opt.deutschlandTicketDiscount,
nurDeutschlandTicketVerbindungen: opt.deutschlandTicketConnectionsOnly,
reservierungsKontingenteVorhanden: false,
schnelleVerbindungen: true,
schnelleVerbindungen: !opt.notOnlyFastRoutes,
sitzplatzOnly: false,
abfahrtsHalt: from.lid,
zwischenhalte: opt.via
@ -35,14 +35,14 @@ const formatJourneysReq = (ctx, from, to, when, outFrwd, journeysRef) => {
if (opt.results !== null) {
// TODO query.numF = opt.results;
}
query = Object.assign(query, ctx.profile.formatTravellers(ctx));
query = Object.assign(query, profile.formatTravellers(ctx));
return {
endpoint: ctx.profile.journeysEndpoint,
endpoint: opt.bestprice ? profile.bestpriceEndpoint : profile.journeysEndpoint,
body: query,
method: 'post',
};
};
// TODO poly conditional other endpoint
const formatRefreshJourneyReq = (ctx, refreshToken) => {
const {profile, opt} = ctx;
if (opt.tickets) {

View file

@ -0,0 +1,20 @@
const formatStationBoardReq = (ctx, station, type) => {
const {profile, opt} = ctx;
return {
endpoint: profile.boardEndpoint,
path: type === 'departures' ? 'abfahrten' : 'ankuenfte',
query: {
ortExtId: station,
zeit: profile.formatTimeOfDay(profile, opt.when),
datum: profile.formatDate(profile, opt.when),
mitVias: opt.stopovers || Boolean(opt.direction) || undefined,
verkehrsmittel: profile.formatProductsFilter(ctx, opt.products || {}),
},
method: 'GET',
};
};
export {
formatStationBoardReq,
};

3933
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{
"name": "db-vendo-client",
"description": "Client for bahn.de public transport APIs.",
"version": "6.3.4",
"version": "6.8.2",
"type": "module",
"main": "index.js",
"files": [
@ -30,7 +30,9 @@
"roehrt",
"Sören Wegener (https://soerface.de/)",
"Paul Sutter <paul.sutter@moia.io>",
"1Maxnet1"
"1Maxnet1",
"McToel <info@bahnvorhersage.de>",
"Daniel Bund <dev@dabund24.de> (https://github.com/dabund24)"
],
"homepage": "https://github.com/public-transport/db-vendo-client",
"repository": {
@ -51,46 +53,48 @@
"api",
"http"
],
"packageManager": "npm@9.2.0",
"packageManager": "npm@10.9.0",
"engines": {
"node": ">=16"
"node": ">=18"
},
"dependencies": {
"@derhuerst/round-robin-scheduler": "^1.0.4",
"content-type": "^1.0.4",
"cross-fetch": "^4.0.0",
"db-hafas-stations": "^1.0.0",
"content-type": "^1.0.5",
"cross-fetch": "^4.1.0",
"db-hafas-stations": "2.0.0",
"gps-distance": "0.0.4",
"https-proxy-agent": "^7.0.0",
"lodash": "^4.17.5",
"luxon": "^3.1.1",
"qs": "^6.6.0",
"slugg": "^1.2.0",
"uuid": "^11.0.5"
"https-proxy-agent": "^7.0.6",
"luxon": "^3.5.0",
"qs": "^6.14.0",
"slugg": "^1.2.1",
"uuid": "^11.1.0"
},
"devDependencies": {
"@pollyjs/adapter-node-http": "^6.0.5",
"@pollyjs/core": "^6.0.5",
"@pollyjs/persister-fs": "^6.0.5",
"@stylistic/eslint-plugin": "^1.5.1",
"@eslint/js": "^9.21.0",
"@pollyjs/adapter-node-http": "^6.0.6",
"@pollyjs/core": "^6.0.6",
"@pollyjs/persister-fs": "^6.0.6",
"@stylistic/eslint-plugin": "^4.1.0",
"cspell": "^8.17.5",
"db-rest": "github:derhuerst/db-rest",
"eslint": "^8.56.0",
"eslint": "^9.21.0",
"globals": "^16.0.0",
"hafas-rest-api": "^5.1.3",
"is-coordinates": "^2.0.2",
"is-roughly-equal": "^0.1.0",
"p-retry": "^6.0.0",
"p-throttle": "^5.0.0",
"tap": "^19.2.5",
"p-retry": "^6.2.1",
"p-throttle": "^7.0.0",
"tap": "^20.0.3",
"validate-fptf": "^3.0.0"
},
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"lint": "eslint",
"lint:fix": "eslint --fix",
"test-unit": "tap test/lib/*.js test/*.js test/format/*.js test/parse/*.js",
"test-integration": "VCR_MODE=playback tap test/e2e/*.js",
"test-integration:record": "VCR_MODE=record tap -t60 -j1 test/e2e/*.js",
"test-e2e": "VCR_OFF=true tap -t60 -j16 test/e2e/*.js",
"test": "npm run test-unit && npm run test-integration",
"test-spelling": "cspell .",
"test": "npm run test-unit && npm run test-integration && npm run test-spelling",
"prepublishOnly": "npm run lint && npm test",
"api": "node api.js"
},

View file

@ -10,17 +10,17 @@ const createParseArrOrDep = (prefix) => {
const {profile, opt} = ctx;
const cancelled = profile.parseCancelled(d);
const res = {
tripId: d.journeyID || d.train?.journeyId || d.zuglaufId,
stop: profile.parseLocation(ctx, d.station || d.abfrageOrt),
tripId: d.journeyID || d.journeyId || d.train?.journeyId || d.zuglaufId,
stop: profile.parseLocation(ctx, d.station || d.abfrageOrt || d.stopPlace || {bahnhofsId: d.bahnhofsId}),
...profile.parseWhen(
ctx,
null,
d.timeSchedule || d.time || d.abgangsDatum || d.ankunftsDatum,
d.timeType != 'SCHEDULE' ? d.timePredicted || d.time || d.ezAbgangsDatum || d.ezAnkunftsDatum : null,
d.timeSchedule || d.time || d.zeit || d.abgangsDatum || d.ankunftsDatum,
d.timeType != 'SCHEDULE' ? d.timePredicted || d.timeDelayed || d.time || d.ezZeit || d.ezAbgangsDatum || d.ezAnkunftsDatum : null,
cancelled),
...profile.parsePlatform(ctx, d.platformSchedule || d.platform || d.gleis, d.platformPredicted || d.platform || d.ezGleis, cancelled),
// prognosisType: TODO
direction: d.transport?.direction?.stopPlaces?.length > 0 && profile.parseStationName(ctx, d.transport?.direction?.stopPlaces[0].name) || profile.parseStationName(ctx, d.destination?.name || d.richtung) || null,
direction: d.transport?.direction?.stopPlaces?.length > 0 && profile.parseStationName(ctx, d.transport?.direction?.stopPlaces[0].name) || profile.parseStationName(ctx, d.destination?.name || d.richtung || d.terminus) || null,
provenance: profile.parseStationName(ctx, d.transport?.origin?.name || d.origin?.name || d.abgangsOrt?.name) || null,
line: profile.parseLine(ctx, d) || null,
remarks: [],
@ -39,7 +39,25 @@ const createParseArrOrDep = (prefix) => {
if (opt.remarks) {
res.remarks = profile.parseRemarks(ctx, d);
}
// TODO opt.stopovers
if (opt.stopovers || opt.direction) {
let stopovers = undefined;
if (Array.isArray(d.ueber)) {
stopovers = d.ueber
.map(viaName => profile.parseStopover(ctx, {name: viaName}, null));
} else if (Array.isArray(d.transport?.via) || Array.isArray(d.viaStops)) {
stopovers = (d.transport?.via || d.viaStops)
.map(via => profile.parseStopover(ctx, via, null));
}
if (stopovers) {
if (prefix === ARRIVAL) {
res.previousStopovers = stopovers;
} else if (prefix === DEPARTURE) {
res.nextStopovers = stopovers;
}
}
}
return res;
};

View file

@ -69,7 +69,15 @@ const parseJourney = (ctx, jj) => { // j = raw journey
// TODO
if (opt.scheduledDays && j.serviceDays) {
// todo [breaking]: rename to scheduledDates
// res.scheduledDays = profile.parseScheduledDays(ctx, j.serviceDays);
// TODO parse scheduledDays as before
res.serviceDays = j.serviceDays.map(d => ({
irregular: d.irregular,
lastDateInPeriod: d.lastDateInPeriod || d.letztesDatumInZeitraum,
planningPeriodBegin: d.planningPeriodBegin || d.planungsZeitraumAnfang,
planningPeriodEnd: d.planningPeriodEnd || d.planungsZeitraumEnde,
regular: d.regular,
weekdays: d.weekdays || d.wochentage,
}));
}
res.price = profile.parsePrice(ctx, jj);

View file

@ -2,21 +2,21 @@ import slugg from 'slugg';
const parseLine = (ctx, p) => {
const profile = ctx.profile;
const fahrtNr = p.verkehrsmittel?.nummer || p.transport?.number || p.train?.no || p.no || ((p.risZuglaufId || '') + '_').split('_')[1] || p.verkehrsmittelNummer || ((p.mitteltext || '') + ' ').split(' ')[1] || ((p.zugName || '') + ' ').split(' ')[1];
const fahrtNr = p.verkehrsmittel?.nummer || p.transport?.number || p.train?.no || p.no || ((p.risZuglaufId || '') + '_').split('_')[1] || p.verkehrsmittelNummer || (p.verkehrmittel?.langText || p.verkehrsmittel?.langText || p.mitteltext || p.zugName || p.lineName || '').replace(/\D/g, '');
const res = {
type: 'line',
id: slugg(p.verkehrsmittel?.langText || p.transport?.journeyDescription || p.risZuglaufId || p.train && p.train.category + ' ' + p.train.lineName + ' ' + p.train.no || p.no && p.name + ' ' + p.no || p.langtext || p.mitteltext || p.zugName), // TODO terrible
id: slugg(p.verkehrsmittel?.langText || p.verkehrmittel?.langText || p.transport?.journeyDescription || p.risZuglaufId || p.train && p.train.category + ' ' + p.train.lineName + ' ' + p.train.no || p.no && p.name + ' ' + p.no || p.langtext || p.mitteltext || p.zugName || p.lineName), // TODO terrible
fahrtNr: String(fahrtNr),
name: p.verkehrsmittel?.langText || p.verkehrsmittel?.name || p.zugName || p.transport && p.transport.category + ' ' + p.transport.line || p.train && p.train.category + ' ' + p.train.lineName || p.name || p.mitteltext || p.langtext,
name: p.verkehrsmittel?.name || p.verkehrsmittel?.langText || p.verkehrmittel?.name || p.verkehrmittel?.langText || p.zugName || p.transport && p.transport.category + ' ' + p.transport.line || p.train && p.train.category + ' ' + p.train.lineName || p.name || p.mitteltext || p.langtext || p.lineName,
public: true,
};
const adminCode = p.administrationId || p.administration?.id || p.administration?.administrationID;
const adminCode = p.administrationID || p.administrationId || p.administration?.id || p.administration?.administrationID;
if (adminCode) {
res.adminCode = adminCode;
}
res.productName = p.verkehrsmittel?.kurzText || p.transport?.category || p.train?.category || p.category || p.kurztext;
const foundProduct = profile.products.find(pp => pp.vendo == p.verkehrsmittel?.produktGattung || pp.ris == p.transport?.type || pp.ris == p.train?.type || pp.ris == p.type || pp.ris_alt == p.train?.type || pp.ris_alt == p.type || pp.dbnav_short == p.produktGattung);
res.productName = p.verkehrsmittel?.kurzText || p.verkehrmittel?.kurzText || p.transport?.category || p.train?.category || p.category || p.kurztext || p.lineName?.replace(/\d/g, '');
const foundProduct = profile.products.find(pp => pp.vendo == p.verkehrsmittel?.produktGattung || pp.vendo == p.verkehrmittel?.produktGattung || pp.ris == p.transport?.type || pp.ris == p.train?.type || pp.ris == p.type || pp.ris_alt == p.train?.type || pp.ris_alt == p.type || pp.dbnav_short == p.produktGattung);
res.mode = foundProduct?.mode;
res.product = foundProduct?.id;

View file

@ -9,7 +9,7 @@ const parseLoadFactor = (opt, auslastung) => {
if (!auslastung) {
return null;
}
const cls = opt.firstClass
const cls = opt.firstClass === true
? 'KLASSE_1'
: 'KLASSE_2';
const load = auslastung.find(a => a.klasse === cls)?.stufe;

View file

@ -7,16 +7,16 @@ const ADDRESS = 'ADR';
const leadingZeros = /^0+/;
const parseLocation = (ctx, l) => {
const {profile, common} = ctx;
const {profile} = ctx;
if (!l) {
return null;
}
const lid = parse(l.id || l.locationId, {delimiter: '@'});
const res = {
let res = {
type: 'location',
id: (l.extId || l.evaNr || lid.L || l.evaNumber || l.evaNo || '').replace(leadingZeros, '') || null,
id: (l.extId || l.evaNr || lid.L || l.evaNumber || l.evaNo || l.bahnhofsId || '').replace(leadingZeros, '') || null,
};
const name = l.name || lid.O;
@ -28,13 +28,15 @@ const parseLocation = (ctx, l) => {
res.longitude = lid.X / 1000000;
}
// addresses and pois might also have fake evaNr sometimes!
if (l.type === STATION || l.extId || l.evaNumber || l.evaNo || 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' || l.bahnhofsId) {
let stop = {
type: 'station',
id: res.id,
name: name,
};
if (name) {
stop.name = name;
}
if ('number' === typeof res.latitude) {
stop.location = res; // todo: remove `.id`
}
@ -44,13 +46,7 @@ const parseLocation = (ctx, l) => {
stop.products = profile.parseProducts(ctx, l.products);
}
if (common && common.locations && common.locations[stop.id]) {
delete stop.type;
stop = {
...common.locations[stop.id],
...stop,
};
}
stop = profile.enrichStation(ctx, stop);
// TODO isMeta
// TODO entrances, lines
@ -58,6 +54,8 @@ const parseLocation = (ctx, l) => {
}
res.name = name;
res = profile.enrichStation(ctx, res);
if (l.type === ADDRESS || lid.A == '2') {
res.address = name;
}
@ -68,6 +66,31 @@ const parseLocation = (ctx, l) => {
return res;
};
const enrichStation = (ctx, stop, locations) => {
const {common} = ctx;
const locs = locations || common?.locations;
const rich = locs && (locs[stop.id] || locs[stop.name]);
if (rich) {
delete stop.type;
delete stop.id;
stop = {
...rich,
...stop,
};
delete stop.lines;
delete stop.facilities;
delete stop.reisezentrumOpeningHours;
if (stop.station) {
stop.station = {...stop.station};
delete stop.station.lines;
delete stop.station.facilities;
delete stop.station.reisezentrumOpeningHours;
}
}
return stop;
};
export {
parseLocation,
enrichStation,
};

View file

@ -1,11 +1,11 @@
import maxBy from 'lodash/maxBy.js';
const parsePolyline = (ctx, p) => { // p = raw polylineGroup
const desc = p.polylineDescriptions || p.polylineDesc;
if (desc.length < 1) {
return null;
}
const points = maxBy(desc, d => d.coordinates.length).coordinates; // TODO initial and final poly?
const points = desc.reduce((max, d) => (d.coordinates.length > max.coordinates.length ? d : max),
).coordinates; // TODO: initial and final poly?
if (points.length === 0) {
return null;
}

View file

@ -1,8 +1,6 @@
import flatMap from 'lodash/flatMap.js';
const parseRemarks = (ctx, ref) => {
// TODO ereignisZusammenfassung, priorisierteMeldungen?
return flatMap([
return [
ref.disruptions || [],
ref.risNotizen || [],
ref.echtzeitNotizen && ref.echtzeitNotizen.map(e => {
@ -12,12 +10,18 @@ const parseRemarks = (ctx, ref) => {
ref.himNotizen || [],
ref.hims || [],
ref.serviceNotiz && [ref.serviceNotiz] || [],
ref.messages || [],
ref.messages?.common || [],
ref.messages?.delay || [],
ref.messages?.cancelation || [],
ref.messages?.destination || [],
ref.messages?.via || [],
Array.isArray(ref.messages) ? ref.messages : [],
ref.meldungen || [],
ref.meldungenAsObject || [],
ref.attributNotizen || [],
ref.attributes || [],
ref.verkehrsmittel?.zugattribute || [],
])
].flat()
.map(remark => {
if (remark.kategorie || remark.priority) {
const res = ctx.profile.parseHintByCode(remark);
@ -29,12 +33,12 @@ const parseRemarks = (ctx, ref) => {
if (remark.prioritaet || remark.prio || remark.type) {
type = 'status';
}
if (!remark.priority && !remark.kategorie && remark.key || remark.disruptionID
if (!remark.priority && !remark.kategorie && remark.key || remark.disruptionID || remark.important
|| remark.prioritaet && remark.prioritaet == 'HOCH' || remark.prio && remark.prio == 'HOCH' || remark.priority && remark.priority < 100) {
type = 'warning';
}
let res = {
code: remark.code || remark.key || remark.id,
code: remark.code || remark.key || remark.id || remark.type,
summary: remark.nachrichtKurz || remark.value || remark.ueberschrift || remark.text || remark.shortText
|| Object.values(remark.descriptions || {})
.shift()?.textShort,
@ -202,11 +206,16 @@ const parseRemarks = (ctx, ref) => {
*/
const parseCancelled = (ref) => {
return ref.canceled || ref.cancelled || ref.journeyCancelled || (ref.risNotizen || ref.echtzeitNotizen) && Boolean((ref.risNotizen || ref.echtzeitNotizen).find(r => r.key == 'text.realtime.stop.cancelled'
return ref.canceled
|| ref.cancelled
|| ref.journeyCancelled
|| (ref.risNotizen || ref.echtzeitNotizen || ref.meldungen) && Boolean(
(ref.risNotizen || ref.echtzeitNotizen || ref.meldungen).find(r => r.key == 'text.realtime.stop.cancelled'
|| r.type == 'HALT_AUSFALL'
|| r.text == 'Halt entfällt'
|| r.text == 'Stop cancelled',
));
),
);
};
export {

View file

@ -1,11 +1,14 @@
const PARTIAL_FARE_HINT = 'Teilpreis / partial fare';
const parsePrice = (ctx, raw) => {
const p = raw.angebotsPreis || raw.angebote?.preise?.gesamt?.ab; // TODO teilpreis
const p = raw.angebotsPreis || raw.angebote?.preise?.gesamt?.ab || raw.abPreis;
if (p?.betrag) {
const partialFare = raw.hasTeilpreis ?? raw.angebote?.preise?.istTeilpreis ?? raw.teilpreis;
return {
amount: p.betrag,
currency: p.waehrung,
hint: null,
hint: partialFare ? PARTIAL_FARE_HINT : null,
partialFare: partialFare,
};
}
return undefined;
@ -45,7 +48,7 @@ const parseTickets = (ctx, j) => {
partialFare: s.teilpreis,
};
if (s.teilpreis) {
p.addData = 'Teilpreis / partial fare';
p.addData = PARTIAL_FARE_HINT;
}
const conds = s.konditionsAnzeigen || s.konditionen;
if (conds) {

View file

@ -2,39 +2,46 @@
**A client for the new "vendo"/"movas" Deutsche Bahn APIs, a drop-in replacement for [hafas-client](https://github.com/public-transport/hafas-client/).**
[![npm version](https://img.shields.io/npm/v/db-vendo-client.svg)](https://www.npmjs.com/package/db-vendo-client)
![ISC-licensed](https://img.shields.io/github/license/public-transport/db-vendo-client.svg)
[![support Jannis via GitHub Sponsors](https://img.shields.io/badge/support%20Jannis-donate-fa7664.svg)](https://github.com/sponsors/derhuerst)
This is a very early version. What works:
The following [FPTF](https://github.com/public-transport/friendly-public-transport-format)/[hafas-client](https://github.com/public-transport/hafas-client/) endpoints are supported (depending on the chosen profile, see below):
* `journeys()`, `refreshJourney()` including tickets
* `journeys()`, `refreshJourney()` including tickets and bestprice option
* `locations()`, `nearby()`,
* `departures()`, `arrivals()` boards
* `trip()`
What doesn't work (yet, see TODO's scattered around the code):
What doesn't work:
* `journeys()` details like scheduledDays, stop/station groups, some line details ...
* `journeys()` details like stop/station groups, some line details ...
* loadFactor and other details in boards
* certain stop details like products for `locations()` and geopositions and remarks for boards this can be remedied by turning on `enrichStations` in the config, enriching location info with [db-hafas-stations](https://github.com/derhuerst/db-hafas-stations).
* certain stop details like products for `locations()` and geopositions for boards this can be remedied with `enrichStations` in the config (turned on by default), enriching stop info with [db-hafas-stations](https://github.com/derhuerst/db-hafas-stations).
* some query options/filters (e.g. routingMode for journeys, direction for boards)
* all other endpoints (`tripsByName()`, `radar()`, `journeysFromTrip()`, `reachableFrom()`, `remarks()`, `lines()`, `station()`)
Depending on the configured profile, db-vendo-client will use multiple different DB APIs that offer varying functionality, so choose wisely:
| | `db` Profile | `dbnav` Profile |
| ------------- | ------------- | ------------- |
| no API key required | ✅ | ✅ |
| max duration boards | 12h | 1h |
| remarks | not for boards | only limited remarks for boards (still no `remarks()` endpoint) |
| cancelled trips | contained with cancelled flag in journeys, not contained in boards | contained with cancelled flag |
| tickets | only for `refreshJourney()`, mutually exclusive with polylines | only for `refreshJourney()`, mutually exclusive with polylines |
| polylines | only for `refreshJourney()` (mutually exclusive with tickets) and for `trip()` (only for HAFAS trip ids) | only for `refreshJourney()/trip()`, mutually exclusive with tickets |
| trip ids used | HAFAS trip ids for journeys, RIS trip ids for boards (static on train splits?) | HAFAS trip ids |
| line.id/fahrtNr used | unreliable/route id for journeys/`trip()`, actual fahrtNr for boards | actual fahrtNr for journeys, unreliable/route id for boards and `trip()` |
| adminCode/operator | adminCode only for boards | only for journeys |
| `stop()` | ❌ | ✅ |
| assumed backend API stability | less stable | more stable |
| Profile | `db` | `dbnav` | `dbweb` | `dbbahnhof` | `dbris` |
| ------------- | ------------- | ------------- | ------------- | ------------- | ------------- |
| no API key required | ✅ | ✅ | ✅ | ✅ | ❌ |
| all above endpoints supported | ✅ | ✅ | except `stop()` | only boards | only boards |
| duration for boards | up to 12h | always 1h | always 1h | up to 6h, only from current time | up to 12h |
| remarks | not for boards | for boards only most important remarks | all remarks on boards and journeys | most remarks | all remarks |
| cancelled trips | contained with cancelled flag in journeys, not contained in boards | contained with cancelled flag | contained with cancelled flag | contained with cancelled flag | contained with cancelled flag |
| tickets | only for `refreshJourney()`, mutually exclusive with polylines | only for `refreshJourney()`, mutually exclusive with polylines | only for `refreshJourney()`, mutually exclusive with polylines | ❌ | ❌ |
| polylines | only for `refreshJourney()` (mutually exclusive with tickets) and for `trip()` (only for HAFAS trip ids) | only for `refreshJourney()/trip()`, mutually exclusive with tickets | only for `refreshJourney()/trip()`, mutually exclusive with tickets | ❌ | ❌ |
| trip ids used | HAFAS trip ids for journeys, RIS trip ids for boards (static on train splits?) | HAFAS trip ids | HAFAS trip ids | RIS trip ids | RIS trip ids |
| line.id/fahrtNr used | actual fahrtNr | actual fahrtNr for journeys, unreliable/route id for boards and `trip()` | unreliable/route id | unreliable/route id | ✅ |
| adminCode/operator | ✅ | only for journeys | only operator | only adminCode | ✅ |
| stopovers | not in boards | not in boards | ✅ | some | ✅ |
| assumed backend API stability | less stable | more stable | less stable | less stable | more stable |
| quotas | 60 requests per minute for journeys, unknown for boards (IPv4) | 60 requests per minute (IPv4) | aggressive blocking (IPv4/IPv6) | ? | depends on API key |
> [!IMPORTANT]
> If you think that for your project, quotas may become an issue, [consider alternative ways to obtain the data you need.](https://github.com/derhuerst/db-rest/blob/6/docs/readme.md#why-not-to-use-this-api).
Feel free to report anything that you stumble upon via Issues or create a PR :)
@ -44,7 +51,7 @@ Also consult the **[documentation](docs/readme.md)**.
After DB has switched to the new "vendo"/"movas" platform for bahn.de and DB Navigator, the old [HAFAS](https://de.wikipedia.org/wiki/HAFAS) API (see [hafas-client](https://github.com/public-transport/hafas-client/)) seems now to have been shut off. This project aims to enable easy switching to the new APIs. However, not all information will be available from the new APIs.
Actually, db-vendo-client is a wrapper around multiple different APIs, currently the bahn.de API for route planning and the regio-guide RIS API for boards for the `db` profile and the DB Navigator API for the `dbnav` profile. See some [notes about the various new APIs at DB](docs/db-apis.md).
Actually, db-vendo-client is a wrapper around multiple different APIs, currently the bahn.de API for `dbweb`, the DB Navigator API for the `dbnav` profile, and a combination of the DB Navigator API and the regio-guide RIS API for the `db` profile. See some [notes about the various new APIs at DB](docs/db-apis.md).
Strictly speaking, permission is necessary to use this library with the DB APIs.
@ -74,6 +81,12 @@ There are [community-maintained TypeScript typings available as `@types/hafas-cl
> [!IMPORTANT]
> Depending on your use case, it is very important that you employ caching, either with a simple [HTTP proxy cache](https://github.com/traines-source/time-space-train-planner/blob/master/deployments/nginx-cache.conf) in front of the REST API or by using [cached-hafas-client](https://github.com/public-transport/cached-hafas-client) (where, of course, you can just drop in a `db-vendo-client` instead of a `hafas-client` instance). Also see [db-rest](https://github.com/derhuerst/db-rest), which does this and some more plumbing.
## Browser usage
`db-vendo-client` is mostly browser compatible, however none of the endpoints enables CORS, so it is impossible to use `db-vendo-client` in normal browser environments. It was tested with vite + capacitorjs and should also work with cordova or react native and similar projects.
**Limitations:** Does not work with `enrichStations` option enabled or with the `dbris` profile.
## Related Projects
- [hafas-client](https://github.com/public-transport/hafas-client/) including further related projects

View file

@ -1,33 +0,0 @@
// 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/db-departures-regio-guide.json');
import {dbDepartures as expected} from './fixtures/db-departures-regio-guide.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});
const {profile} = client;
const opt = {
direction: null,
duration: 10,
linesOfStops: true,
remarks: true,
stopovers: true,
includeRelatedStations: true,
when: '2019-08-19T20:30:00+02:00',
products: {},
};
tap.test('parses a regio-guide departure correctly', (t) => {
const ctx = {profile, opt, common: null, res};
const departures = res.items.map(d => profile.parseDeparture(ctx, d));
t.same(departures, expected);
t.end();
});

View file

@ -0,0 +1,29 @@
import tap from 'tap';
import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbbahnhof/index.js';
import res from './fixtures/dbbahnhof-departures.json' with { type: 'json' };
import {dbDepartures as expected} from './fixtures/dbbahnhof-departures.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});
const {profile} = client;
const opt = {
direction: null,
duration: 10,
linesOfStops: true,
remarks: true,
stopovers: false,
includeRelatedStations: true,
when: '2019-08-19T20:30:00+02:00',
products: {},
};
tap.test('parses a bahnhof.de departure correctly', (t) => {
const ctx = {profile, opt, common: null, res};
const arrivals = res.entries.flat()
.map(d => profile.parseArrival(ctx, d));
t.same(arrivals, expected);
t.end();
});

View file

@ -1,13 +1,8 @@
// 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/dbnav/index.js';
const res = require('./fixtures/dbnav-departures.json');
import res from './fixtures/dbnav-departures.json' with { type: 'json' };
import {dbnavDepartures as expected} from './fixtures/dbnav-departures.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -1,13 +1,8 @@
// 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/dbnav/index.js';
const res = require('./fixtures/dbnav-refresh-journey.json');
import res from './fixtures/dbnav-refresh-journey.json' with { type: 'json' };
import {dbNavJourney as expected} from './fixtures/dbnav-refresh-journey.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -1,13 +1,8 @@
// 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/dbnav/index.js';
const res = require('./fixtures/dbnav-stop.json');
import res from './fixtures/dbnav-stop.json' with { type: 'json' };
import {dbnavDepartures as expected} from './fixtures/dbnav-stop.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -1,13 +1,8 @@
// 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/dbnav/index.js';
const res = require('./fixtures/dbnav-trip.json');
import res from './fixtures/dbnav-trip.json' with { type: 'json' };
import {dbTrip as expected} from './fixtures/dbnav-trip.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -1,14 +1,9 @@
// 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/db-trip-regio-guide.json');
import {dbTrip as expected} from './fixtures/db-trip-regio-guide.js';
import {profile as rawProfile} from '../p/dbregioguide/index.js';
import res from './fixtures/dbregioguide-trip.json' with { type: 'json' };
import {dbTrip as expected} from './fixtures/dbregioguide-trip.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});
const {profile} = client;

View file

@ -1,13 +1,8 @@
// 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/dbris-arrivals.json');
import {profile as rawProfile} from '../p/dbris/index.js';
import res from './fixtures/dbris-arrivals.json' with { type: 'json' };
import {dbArrivals as expected} from './fixtures/dbris-arrivals.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});
@ -18,7 +13,7 @@ const opt = {
duration: 10,
linesOfStops: true,
remarks: true,
stopovers: true,
stopovers: false,
includeRelatedStations: true,
when: '2019-08-19T20:30:00+02:00',
products: {},

28
test/dbris-departures.js Normal file
View file

@ -0,0 +1,28 @@
import tap from 'tap';
import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbris/index.js';
import res from './fixtures/dbris-departures.json' with { type: 'json' };
import {dbDepartures as expected} from './fixtures/dbris-departures.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});
const {profile} = client;
const opt = {
direction: null,
duration: 10,
linesOfStops: true,
remarks: true,
stopovers: false,
includeRelatedStations: true,
when: '2019-08-19T20:30:00+02:00',
products: {},
};
tap.test('parses a RIS::Boards departure correctly', (t) => {
const ctx = {profile, opt, common: null, res};
const arrivals = res.departures.map(d => profile.parseArrival(ctx, d));
t.same(arrivals, expected);
t.end();
});

87
test/dbweb-departures.js Normal file
View file

@ -0,0 +1,87 @@
import tap from 'tap';
import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbweb/index.js';
import res from './fixtures/dbweb-departures.json' with { type: 'json' };
import {dbwebDepartures as expected} from './fixtures/dbweb-departures.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});
const {profile} = client;
const opt = {
direction: null,
duration: null,
linesOfStops: true,
remarks: true,
stopovers: true,
includeRelatedStations: true,
when: '2025-02-08T15:37:00',
products: {},
};
const osterburken = {
type: 'station',
id: '8000295',
name: 'Osterburken',
location: {
type: 'location',
id: '8000295',
latitude: 49.42992,
longitude: 9.422996,
},
products: {
nationalExpress: false,
national: false,
regionalExp: false,
regional: true,
suburban: true,
bus: true,
ferry: false,
subway: false,
tram: false,
taxi: true,
},
weight: 5.6,
};
const moeckmuehl = {
type: 'station',
id: '8004050',
name: 'Möckmühl',
location: {
type: 'location',
id: '8004050',
latitude: 49.321187,
longitude: 9.357977,
},
products: {
nationalExpress: false,
national: false,
regionalExp: false,
regional: true,
suburban: false,
bus: true,
ferry: false,
subway: false,
tram: false,
taxi: false,
},
distance: 2114,
weight: 6.45,
};
const common = {
locations: {
Osterburken: osterburken,
8000295: osterburken,
Möckmühl: moeckmuehl,
},
};
tap.test('parses a dbweb departure correctly', (t) => {
const ctx = {profile, opt, common, res};
const departures = res.entries.map(d => profile.parseDeparture(ctx, d));
t.same(departures, expected);
t.end();
});

View file

@ -1,14 +1,9 @@
// 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/db-journey.json');
import {dbJourney as expected} from './fixtures/db-journey.js';
import {profile as rawProfile} from '../p/dbweb/index.js';
import res from './fixtures/dbweb-journey.json' with { type: 'json' };
import {dbwebJourney as expected} from './fixtures/dbweb-journey.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});
const {profile} = client;
@ -31,7 +26,7 @@ const opt = {
products: {},
};
tap.test('parses a journey correctly (DB)', (t) => { // TODO DEVI leg
tap.test('parses a dbweb journey correctly', (t) => { // TODO DEVI leg
const ctx = {profile, opt, common: null, res};
const journey = profile.parseJourney(ctx, res.verbindungen[0]);

View file

@ -1,14 +1,9 @@
// 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/db-refresh-journey.json');
import {dbJourney as expected} from './fixtures/db-refresh-journey.js';
import {profile as rawProfile} from '../p/dbweb/index.js';
import res from './fixtures/dbweb-refresh-journey.json' with { type: 'json' };
import {dbJourney as expected} from './fixtures/dbweb-refresh-journey.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});
const {profile} = client;
@ -31,7 +26,7 @@ const opt = {
products: {},
};
tap.test('parses a refresh journey correctly (DB)', (t) => {
tap.test('parses a refresh journey correctly (dbweb)', (t) => {
const ctx = {profile, opt, common: null, res};
const journey = profile.parseJourney(ctx, res.verbindungen[0]);

View file

@ -1,14 +1,9 @@
// 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/db-trip.json');
import {dbTrip as expected} from './fixtures/db-trip.js';
import {profile as rawProfile} from '../p/dbweb/index.js';
import res from './fixtures/dbweb-trip.json' with { type: 'json' };
import {dbwebTrip as expected} from './fixtures/dbweb-trip.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});
const {profile} = client;

View file

@ -99,7 +99,8 @@ const potsdamHbf = '8012666';
const berlinSüdkreuz = '8011113';
const kölnHbf = '8000207';
tap.test('journeys  Berlin Schwedter Str. to München Hbf', async (t) => {
if (!process.env.VCR_OFF) {
tap.test('journeys  Berlin Schwedter Str. to München Hbf', async (t) => {
const res = await client.journeys(blnSchwedterStr, münchenHbf, {
results: 4,
departure: when,
@ -123,10 +124,10 @@ tap.test('journeys  Berlin Schwedter Str. to München Hbf', async (t) => {
}
}
t.end();
});
});
tap.test('refreshJourney valid tickets', async (t) => {
const T_MOCK = 1710831600 * 1000; // 2024-03-19T08:00:00+01:00
tap.test('refreshJourney valid tickets', async (t) => {
const T_MOCK = 1758279600 * 1000;
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
const journeysRes = await client.journeys(berlinHbf, münchenHbf, {
@ -147,11 +148,11 @@ tap.test('refreshJourney valid tickets', async (t) => {
}
t.end();
});
});
// todo: journeys, only one product
// todo: journeys, only one product
tap.test('journeys fails with no product', async (t) => {
tap.test('journeys fails with no product', async (t) => {
await journeysFailsWithNoProduct({
test: t,
fetchJourneys: client.journeys,
@ -161,9 +162,9 @@ tap.test('journeys fails with no product', async (t) => {
products: dbProfile.products,
});
t.end();
});
});
tap.test('Berlin Schwedter Str. to Torfstraße 17', async (t) => {
tap.test('Berlin Schwedter Str. to Torfstraße 17', async (t) => {
const torfstr = {
type: 'location',
address: 'Torfstraße 17',
@ -183,9 +184,9 @@ tap.test('Berlin Schwedter Str. to Torfstraße 17', async (t) => {
to: torfstr,
});
t.end();
});
});
tap.test('Berlin Schwedter Str. to ATZE Musiktheater', async (t) => {
tap.test('Berlin Schwedter Str. to ATZE Musiktheater', async (t) => {
const atze = {
type: 'location',
id: '991598902',
@ -207,10 +208,10 @@ tap.test('Berlin Schwedter Str. to ATZE Musiktheater', async (t) => {
to: atze,
});
t.end();
});
});
tap.test('journeys: via works with detour', async (t) => {
// Going from Westhafen to Wedding via Württembergalle without detour
tap.test('journeys: via works with detour', async (t) => {
// Going from Westhafen to Wedding via Württembergallee without detour
// is currently impossible. We check if the routing engine computes a detour.
const res = await client.journeys(westhafen, wedding, {
via: württembergallee,
@ -226,11 +227,11 @@ tap.test('journeys: via works with detour', async (t) => {
detourIds: [württembergallee],
});
t.end();
});
// todo: walkingSpeed "Berlin - Charlottenburg, Hallerstraße" -> jungfernheide
// todo: without detour
});
// 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) => {
@ -260,7 +261,7 @@ if (!process.env.VCR_MODE) {
}
tap.test('refreshJourney', async (t) => {
const T_MOCK = 1710831600 * 1000; // 2024-03-19T08:00:00+01:00
const T_MOCK = 1763542800 * 1000;
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
const validate = createValidate({...cfg, when});
@ -476,7 +477,6 @@ tap.test('locations named Jungfernheide', async (t) => {
t.end();
});
/*
tap.test('stop', async (t) => {
const s = await client.stop(regensburgHbf);
@ -486,6 +486,7 @@ tap.test('stop', async (t) => {
t.end();
});
/*
tap.test('line with additionalName', async (t) => {
const {departures} = await client.departures(potsdamHbf, {
when,

146
test/e2e/dbbahnhof.js Normal file
View file

@ -0,0 +1,146 @@
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/dbregioguide/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', {enrichStations: false});
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('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();
});

View file

@ -126,7 +126,7 @@ tap.test('journeys  Berlin Schwedter Str. to München Hbf', async (t) => {
});
tap.test('refreshJourney valid tickets', async (t) => {
const T_MOCK = 1710831600 * 1000; // 2024-03-19T08:00:00+01:00
const T_MOCK = 1758279600 * 1000;
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
const journeysRes = await client.journeys(berlinHbf, münchenHbf, {
@ -151,7 +151,8 @@ tap.test('refreshJourney valid tickets', async (t) => {
// todo: journeys, only one product
tap.test('journeys fails with no product', async (t) => {
if (!process.env.VCR_OFF) {
tap.test('journeys fails with no product', async (t) => {
await journeysFailsWithNoProduct({
test: t,
fetchJourneys: client.journeys,
@ -161,9 +162,9 @@ tap.test('journeys fails with no product', async (t) => {
products: dbProfile.products,
});
t.end();
});
});
tap.test('Berlin Schwedter Str. to Torfstraße 17', async (t) => {
tap.test('Berlin Schwedter Str. to Torfstraße 17', async (t) => {
const torfstr = {
type: 'location',
address: 'Torfstraße 17',
@ -183,9 +184,9 @@ tap.test('Berlin Schwedter Str. to Torfstraße 17', async (t) => {
to: torfstr,
});
t.end();
});
});
tap.test('Berlin Schwedter Str. to ATZE Musiktheater', async (t) => {
tap.test('Berlin Schwedter Str. to ATZE Musiktheater', async (t) => {
const atze = {
type: 'location',
id: '991598902',
@ -207,10 +208,10 @@ tap.test('Berlin Schwedter Str. to ATZE Musiktheater', async (t) => {
to: atze,
});
t.end();
});
});
tap.test('journeys: via works with detour', async (t) => {
// Going from Westhafen to Wedding via Württembergalle without detour
tap.test('journeys: via works with detour', async (t) => {
// Going from Westhafen to Wedding via Württembergallee without detour
// is currently impossible. We check if the routing engine computes a detour.
const res = await client.journeys(westhafen, wedding, {
via: württembergallee,
@ -226,14 +227,14 @@ tap.test('journeys: via works with detour', async (t) => {
detourIds: [württembergallee],
});
t.end();
});
});
// todo: walkingSpeed "Berlin - Charlottenburg, Hallerstraße" -> jungfernheide
// todo: without detour
// 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) => {
// 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,
@ -244,9 +245,9 @@ tap.skip('earlier/later journeys, Jungfernheide -> München Hbf', async (t) => {
});
t.end();
});
});
if (!process.env.VCR_MODE) {
if (!process.env.VCR_MODE) {
tap.test('journeys leg cycle & alternatives', async (t) => {
await testLegCycleAlternatives({
test: t,
@ -257,10 +258,10 @@ if (!process.env.VCR_MODE) {
});
t.end();
});
}
}
tap.test('refreshJourney', async (t) => {
const T_MOCK = 1710831600 * 1000; // 2024-03-19T08:00:00+01:00
tap.test('refreshJourney', async (t) => {
const T_MOCK = 1763542800 * 1000;
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
const validate = createValidate({...cfg, when});
@ -274,7 +275,8 @@ tap.test('refreshJourney', async (t) => {
when,
});
t.end();
});
});
}
/*
tap.skip('journeysFromTrip U Mehringdamm to U Naturkundemuseum, reroute to Spittelmarkt.', async (t) => {

499
test/e2e/dbregioguide.js Normal file
View file

@ -0,0 +1,499 @@
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/dbregioguide/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', {enrichStations: false});
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ürttembergallee 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();
});
*/

501
test/e2e/dbweb.js Normal file
View file

@ -0,0 +1,501 @@
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/dbweb/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', {enrichStations: true});
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';
if (!process.env.VCR_OFF) {
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ürttembergallee 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

@ -1,4 +1,4 @@
// Polly's HTTP adapter uses nock [1] underneath, with currenctly monkey-patches the built-in `node:http` module. For this to work, it must be imported quite early, before many other parts of hafas-client.
// Polly's HTTP adapter uses nock [1] underneath, with currently monkey-patches the built-in `node:http` module. For this to work, it must be imported quite early, before many other parts of hafas-client.
// Importing the adapter itself has no side effects (or rather: immediately undoes nock's monkey-patching, to re-patch when it is actually getting used). We activate it (by passing it into Polly's core) below.
// remotely related: https://github.com/nock/nock/issues/2461
import NodeHttpAdapter from '@pollyjs/adapter-node-http';

492
test/fixtures/dbbahnhof-departures.js vendored Normal file
View file

@ -0,0 +1,492 @@
const dbDepartures = [
{
tripId: '20250322-f6cd6d71-510f-378c-b825-fbb9380f5b03',
stop: {
type: 'station',
id: '8000105',
name: 'Frankfurt(Main)Hbf',
},
when: '2025-03-22T00:42:00+01:00',
plannedWhen: '2025-03-22T00:42:00+01:00',
delay: 0,
platform: '11',
plannedPlatform: '11',
direction: 'Fulda',
provenance: null,
line: {
type: 'line',
id: 'rb51',
fahrtNr: '51',
name: 'RB51',
public: true,
adminCode: '8005KG',
productName: 'RB',
mode: 'train',
product: 'regional',
operator: null,
},
remarks: [
{
code: 'bicycle-transport',
summary: 'Limited bicycle transport possible',
text: 'Limited bicycle transport possible',
type: 'status',
},
],
origin: null,
destination: {
type: 'station',
id: '8000115',
name: 'Fulda',
},
},
{
tripId: '20250321-69f912f7-2b47-3ffd-b00f-708e8c568f6b',
stop: {
type: 'station',
id: '8098105',
name: 'Frankfurt Hbf (tief)',
},
when: '2025-03-22T00:43:00+01:00',
plannedWhen: '2025-03-22T00:42:00+01:00',
delay: 60,
platform: '103',
plannedPlatform: '103',
direction: 'Wiesbaden Hbf',
provenance: null,
line: {
type: 'line',
id: 's1',
fahrtNr: '1',
name: 'S1',
public: true,
adminCode: '800528',
productName: 'S',
mode: 'train',
product: 'suburban',
operator: null,
},
remarks: [
{
code: 'bicycle-transport',
summary: 'Limited bicycle transport possible',
text: 'Limited bicycle transport possible',
type: 'status',
},
],
origin: null,
destination: {
type: 'station',
id: '8000250',
name: 'Wiesbaden Hbf',
},
},
{
tripId: '20250322-fcf0de07-fc6e-3aeb-9757-3b08e6760c3f',
stop: {
type: 'station',
id: '8098105',
name: 'Frankfurt Hbf (tief)',
},
when: '2025-03-22T00:44:00+01:00',
plannedWhen: '2025-03-22T00:44:00+01:00',
delay: null,
platform: null,
plannedPlatform: null,
direction: 'Bad Soden(Taunus)',
provenance: null,
line: {
type: 'line',
id: 'sev',
fahrtNr: '',
name: 'SEV',
public: true,
adminCode: 'B4',
productName: 'SEV',
mode: 'train',
product: 'regional',
operator: null,
},
remarks: [
{
code: 'replacement-service',
summary: 'Replacement bus for {{1}}',
text: 'Replacement bus for {{1}}',
type: 'status',
},
],
origin: null,
destination: {
type: 'station',
id: '8000752',
name: 'Bad Soden(Taunus)',
},
},
{
tripId: '20250321-bff43415-303b-36ad-ae4e-8d9605fc3f1e',
stop: {
type: 'station',
id: '8098105',
name: 'Frankfurt Hbf (tief)',
},
when: null,
plannedWhen: '2025-03-22T00:44:00+01:00',
prognosedWhen: null,
delay: null,
platform: null,
plannedPlatform: '102',
prognosedPlatform: '102',
direction: 'Offenbach(Main)Ost',
provenance: null,
line: {
type: 'line',
id: 's8',
fahrtNr: '8',
name: 'S8',
public: true,
adminCode: '800528',
productName: 'S',
mode: 'train',
product: 'suburban',
operator: null,
},
remarks: [
{
code: 'unplanned-info',
summary: 'Defective signal box',
text: 'Defective signal box',
type: 'status',
},
{
code: 'canceled-trip',
summary: 'Trip cancelled',
text: 'Trip cancelled',
type: 'warning',
},
],
origin: null,
destination: {
type: 'station',
id: '8004645',
name: 'Offenbach(Main)Ost',
},
cancelled: true,
},
{
tripId: '20250322-1423f343-0076-3f29-b1e8-004dc1f27068',
stop: {
type: 'station',
id: '8098105',
name: 'Frankfurt Hbf (tief)',
},
when: '2025-03-22T00:47:00+01:00',
plannedWhen: '2025-03-22T00:47:00+01:00',
delay: 0,
platform: '101',
plannedPlatform: '101',
direction: 'Darmstadt Hbf',
provenance: null,
line: {
type: 'line',
id: 's6',
fahrtNr: '6',
name: 'S6',
public: true,
adminCode: '800528',
productName: 'S',
mode: 'train',
product: 'suburban',
operator: null,
},
remarks: [
{
code: 'bicycle-transport',
summary: 'Limited bicycle transport possible',
text: 'Limited bicycle transport possible',
type: 'status',
},
],
origin: null,
destination: {
type: 'station',
id: '8000068',
name: 'Darmstadt Hbf',
},
},
{
tripId: '20250322-f2c61208-75b1-3c19-a580-172988addf2c',
stop: {
type: 'station',
id: '8098105',
name: 'Frankfurt Hbf (tief)',
},
when: null,
plannedWhen: '2025-03-22T00:47:00+01:00',
prognosedWhen: null,
delay: null,
platform: null,
plannedPlatform: '103',
prognosedPlatform: '103',
direction: 'Wiesbaden Hbf',
provenance: null,
line: {
type: 'line',
id: 's8',
fahrtNr: '8',
name: 'S8',
public: true,
adminCode: '800528',
productName: 'S',
mode: 'train',
product: 'suburban',
operator: null,
},
remarks: [
{
code: 'unplanned-info',
summary: 'Defective signal box',
text: 'Defective signal box',
type: 'status',
},
{
code: 'canceled-trip',
summary: 'Trip cancelled',
text: 'Trip cancelled',
type: 'warning',
},
],
origin: null,
destination: {
type: 'station',
id: '8000250',
name: 'Wiesbaden Hbf',
},
cancelled: true,
},
{
tripId: '20250322-841901b7-7420-3890-8691-05c8cbbacdce',
stop: {
type: 'station',
id: '8098105',
name: 'Frankfurt Hbf (tief)',
},
when: '2025-03-22T01:05:00+01:00',
plannedWhen: '2025-03-22T00:49:00+01:00',
delay: 960,
platform: '102',
plannedPlatform: '102',
direction: 'Rödermark-Ober Roden',
provenance: null,
line: {
type: 'line',
id: 's1',
fahrtNr: '1',
name: 'S1',
public: true,
adminCode: '800528',
productName: 'S',
mode: 'train',
product: 'suburban',
operator: null,
},
remarks: [
{
code: 'unplanned-info',
summary: 'Repair on the turnout',
text: 'Repair on the turnout',
type: 'status',
},
{
code: 'bicycle-transport',
summary: 'Limited bicycle transport possible',
text: 'Limited bicycle transport possible',
type: 'status',
},
],
origin: null,
destination: {
type: 'station',
id: '8000285',
name: 'Rödermark-Ober Roden',
},
},
{
tripId: '20250322-08673537-4773-3e90-afa3-ab467119a609',
stop: {
type: 'station',
id: '100010',
name: 'Hauptbahnhof, Frankfurt a.M.',
},
when: '2025-03-22T00:50:00+01:00',
plannedWhen: '2025-03-22T00:50:00+01:00',
delay: 0,
platform: null,
plannedPlatform: null,
direction: 'Bockenheimer Warte, Frankfurt a.M.',
provenance: null,
line: {
type: 'line',
id: 'u4',
fahrtNr: '4',
name: 'U4',
public: true,
adminCode: 'rmv255',
productName: 'U',
mode: 'train',
product: 'subway',
operator: null,
},
remarks: [],
origin: null,
destination: {
type: 'station',
id: '101201',
name: 'Bockenheimer Warte, Frankfurt a.M.',
},
},
{
tripId: '20250322-29a07ddd-969e-3660-ae2e-4d6c6af4a866',
stop: {
type: 'station',
id: '8000105',
name: 'Frankfurt(Main)Hbf',
},
when: null,
plannedWhen: '2025-03-22T00:50:00+01:00',
prognosedWhen: null,
delay: null,
platform: null,
plannedPlatform: '2',
prognosedPlatform: '2',
direction: 'Riedstadt-Goddelau',
provenance: null,
line: {
type: 'line',
id: 's7',
fahrtNr: '7',
name: 'S7',
public: true,
adminCode: '800528',
productName: 'S',
mode: 'train',
product: 'suburban',
operator: null,
},
remarks: [
{
code: 'unplanned-info',
summary: 'Short-term unavailability of employees',
text: 'Short-term unavailability of employees',
type: 'status',
},
{
code: 'canceled-trip',
summary: 'Trip cancelled',
text: 'Trip cancelled',
type: 'warning',
},
],
origin: null,
destination: {
type: 'station',
id: '8000126',
name: 'Riedstadt-Goddelau',
},
cancelled: true,
},
{
tripId: '20250322-01575b72-00ad-36da-bc88-49410d87321a',
stop: {
type: 'station',
id: '8098105',
name: 'Frankfurt Hbf (tief)',
},
when: '2025-03-22T00:52:00+01:00',
plannedWhen: '2025-03-22T00:52:00+01:00',
delay: 0,
platform: '103',
plannedPlatform: '103',
direction: 'Niedernhausen(Taunus)',
provenance: null,
line: {
type: 'line',
id: 's2',
fahrtNr: '2',
name: 'S2',
public: true,
adminCode: '800528',
productName: 'S',
mode: 'train',
product: 'suburban',
operator: null,
},
remarks: [
{
code: 'bicycle-transport',
summary: 'Limited bicycle transport possible',
text: 'Limited bicycle transport possible',
type: 'status',
},
],
origin: null,
destination: {
type: 'station',
id: '8004400',
name: 'Niedernhausen(Taunus)',
},
},
{
tripId: '20250322-2b5b5913-2b2d-39da-b29d-2fff067ba6a2',
stop: {
type: 'station',
id: '8098105',
name: 'Frankfurt Hbf (tief)',
},
when: null,
plannedWhen: '2025-03-22T00:52:00+01:00',
prognosedWhen: null,
delay: null,
platform: null,
plannedPlatform: '101',
prognosedPlatform: '101',
direction: 'Frankfurt(Main)Süd',
provenance: null,
line: {
type: 'line',
id: 's4',
fahrtNr: '4',
name: 'S4',
public: true,
adminCode: '800528',
productName: 'S',
mode: 'train',
product: 'suburban',
operator: null,
},
remarks: [
{
code: 'unplanned-info',
summary: 'Defective signal box',
text: 'Defective signal box',
type: 'status',
},
{
code: 'canceled-trip',
summary: 'Trip cancelled',
text: 'Trip cancelled',
type: 'warning',
},
],
origin: null,
destination: {
type: 'station',
id: '8002041',
name: 'Frankfurt(Main)Süd',
},
cancelled: true,
},
];
export {
dbDepartures,
};

File diff suppressed because one or more lines are too long

View file

@ -212,6 +212,7 @@ const dbNavJourney = {
amount: 43.99,
currency: 'EUR',
hint: null,
partialFare: false,
},
tickets: [
{

580
test/fixtures/dbris-departures.js vendored Normal file
View file

@ -0,0 +1,580 @@
const dbDepartures = [
{
tripId: '20250321-69f912f7-2b47-3ffd-b00f-708e8c568f6b',
stop: {
type: 'station',
id: '8098105',
name: 'Frankfurt Hbf (tief)',
},
when: '2025-03-22T00:43:00+01:00',
plannedWhen: '2025-03-22T00:42:00+01:00',
delay: 60,
platform: '103',
plannedPlatform: '103',
direction: 'Wiesbaden Hbf',
provenance: null,
line: {
type: 'line',
id: 's-1',
fahrtNr: '35182',
name: 'S 1',
public: true,
adminCode: '800528',
productName: 'S',
mode: 'train',
product: 'suburban',
operator: {
type: 'operator',
id: 'DB',
name: 'DB Regio, S-Bahn Rhein-Main',
},
},
remarks: [
{
code: '43',
summary: 'Verspätung eines vorausfahrenden Zuges',
text: 'Verspätung eines vorausfahrenden Zuges',
type: 'status',
},
{
code: 'FK',
summary: 'Fahrradmitnahme begrenzt möglich',
text: 'Fahrradmitnahme begrenzt möglich',
type: 'hint',
},
{
code: 'EH',
summary: 'Fahrzeuggebundene Einstiegshilfe vorhanden',
text: 'Fahrzeuggebundene Einstiegshilfe vorhanden',
type: 'hint',
},
],
origin: null,
destination: {
type: 'station',
id: '8000250',
name: 'Wiesbaden Hbf',
},
},
{
tripId: '20250322-fcf0de07-fc6e-3aeb-9757-3b08e6760c3f',
stop: {
type: 'station',
id: '8098105',
name: 'Frankfurt Hbf (tief)',
},
when: '2025-03-22T00:44:00+01:00',
plannedWhen: '2025-03-22T00:44:00+01:00',
delay: null,
platform: null,
plannedPlatform: null,
direction: null,
provenance: null,
line: {
type: 'line',
id: 'bus-sev-353821',
fahrtNr: '353821',
name: 'Bus SEV',
public: true,
adminCode: 'B4',
productName: 'Bus',
mode: 'train',
product: 'regional',
operator: {
type: 'operator',
id: '---',
name: 'Busse/SEV S-Bahn Rhein-Main',
},
},
remarks: [],
origin: null,
destination: {
type: 'station',
id: '8000752',
name: 'Bad Soden(Taunus)',
},
},
{
tripId: '20250321-bff43415-303b-36ad-ae4e-8d9605fc3f1e',
stop: {
type: 'station',
id: '8098105',
name: 'Frankfurt Hbf (tief)',
},
when: null,
plannedWhen: '2025-03-22T00:44:00+01:00',
prognosedWhen: null,
delay: null,
platform: null,
plannedPlatform: '102',
prognosedPlatform: '102',
direction: 'Offenbach(Main)Ost',
provenance: null,
line: {
type: 'line',
id: 's-8',
fahrtNr: '35883',
name: 'S 8',
public: true,
adminCode: '800528',
productName: 'S',
mode: 'train',
product: 'suburban',
operator: {
type: 'operator',
id: 'DB',
name: 'DB Regio, S-Bahn Rhein-Main',
},
},
remarks: [
{
code: undefined,
summary: 'S8: Kein Betrieb möglich. Grund: Erkrankung von Stellwerkspersonal. Dauer der Beeinträchtigung bis Betriebsschluss. Ersatzverkehr mit Bussen eingerichtet. Andere Verkehrsmittel mit einbeziehen. Reiseverbindung frühzeitig prüfen.',
text: 'Auf der S8 ist kein Betrieb möglich. Der Grund ist eine Erkrankung von Stellwerkspersonal. Dauer der Beeinträchtigung bis Betriebsschluss. Wir haben für Sie einen Ersatzverkehr mit 8 Bussen der Firma Holiday-Reisen GmbH zwischen Wiesbaden Hbf und Frankfurt(M) Flughafen Regionalbf eingerichtet. Außerdem haben wir für Sie ab ca. 01:00 Uhr einen Ersatzverkehr mit 4 Bussen der Firma Holiday-Reisen GmbH zwischen Hanau Hbf und Offenbach(Main)Ost eingerichtet. Beziehen Sie auch andere Verkehrsmittel mit ein, um an Ihr Reiseziel zu gelangen. Bitte informieren Sie sich frühzeitig über Ihre geplanten Verbindungen und wählen Sie gegebenenfalls eine frühere Zugverbindung, um Ihre Anschlüsse an Ihren Umsteigebahnhöfen erreichen zu können.',
type: 'warning',
},
{
code: '40',
summary: 'defektes Stellwerk',
text: 'defektes Stellwerk',
type: 'status',
},
{
code: 'CUSTOMER_TEXT',
summary: 'Auf der S8 und S9 kommt es zwischen Wiesbaden Hbf - Frankfurt(M) Flughafen Regionalbf - Frankfurt(Main)Hbf - Offenbach(Main)Ost in den Nächten bis zum 22.03.25 zu geänderten Fahrtzeiten, einzelnen Umleitungen, Teil- und Zugausfällen. Der Grund sind Bauarbeiten. Auf dem jeweils ausfallenden Abschnitt haben wir für Sie einen Ersatzverkehr mit Bussen eingerichtet. Die Mitnahme von Fahrrädern im Bus ist ausgeschlossen. Bitte informieren Sie sich frühzeitig über Ihre geplanten Verbindungen und wählen Sie gegebenenfalls eine frühere Zugverbindung, um Ihre Anschlüsse an Ihren Umsteigebahnhöfen erreichen zu können. Die Fahrplanänderungen zu dieser Baumaßnahme sind in die Online-Fahrplanauskünfte eingearbeitet.',
text: 'Auf der S8 und S9 kommt es zwischen Wiesbaden Hbf - Frankfurt(M) Flughafen Regionalbf - Frankfurt(Main)Hbf - Offenbach(Main)Ost in den Nächten bis zum 22.03.25 zu geänderten Fahrtzeiten, einzelnen Umleitungen, Teil- und Zugausfällen. Der Grund sind Bauarbeiten. Auf dem jeweils ausfallenden Abschnitt haben wir für Sie einen Ersatzverkehr mit Bussen eingerichtet. Die Mitnahme von Fahrrädern im Bus ist ausgeschlossen. Bitte informieren Sie sich frühzeitig über Ihre geplanten Verbindungen und wählen Sie gegebenenfalls eine frühere Zugverbindung, um Ihre Anschlüsse an Ihren Umsteigebahnhöfen erreichen zu können. Die Fahrplanänderungen zu dieser Baumaßnahme sind in die Online-Fahrplanauskünfte eingearbeitet.',
type: 'status',
},
{
code: 'FK',
summary: 'Fahrradmitnahme begrenzt möglich',
text: 'Fahrradmitnahme begrenzt möglich',
type: 'hint',
},
{
code: 'EH',
summary: 'Fahrzeuggebundene Einstiegshilfe vorhanden',
text: 'Fahrzeuggebundene Einstiegshilfe vorhanden',
type: 'hint',
},
],
origin: null,
destination: {
type: 'station',
id: '8004645',
name: 'Offenbach(Main)Ost',
},
cancelled: true,
},
{
tripId: '20250322-f2c61208-75b1-3c19-a580-172988addf2c',
stop: {
type: 'station',
id: '8098105',
name: 'Frankfurt Hbf (tief)',
},
when: null,
plannedWhen: '2025-03-22T00:47:00+01:00',
prognosedWhen: null,
delay: null,
platform: null,
plannedPlatform: '103',
prognosedPlatform: '103',
direction: 'Wiesbaden Hbf',
provenance: null,
line: {
type: 'line',
id: 's-8',
fahrtNr: '35884',
name: 'S 8',
public: true,
adminCode: '800528',
productName: 'S',
mode: 'train',
product: 'suburban',
operator: {
type: 'operator',
id: 'DB',
name: 'DB Regio, S-Bahn Rhein-Main',
},
},
remarks: [
{
code: undefined,
summary: 'S8: Kein Betrieb möglich. Grund: Erkrankung von Stellwerkspersonal. Dauer der Beeinträchtigung bis Betriebsschluss. Ersatzverkehr mit Bussen eingerichtet. Andere Verkehrsmittel mit einbeziehen. Reiseverbindung frühzeitig prüfen.',
text: 'Auf der S8 ist kein Betrieb möglich. Der Grund ist eine Erkrankung von Stellwerkspersonal. Dauer der Beeinträchtigung bis Betriebsschluss. Wir haben für Sie einen Ersatzverkehr mit 8 Bussen der Firma Holiday-Reisen GmbH zwischen Wiesbaden Hbf und Frankfurt(M) Flughafen Regionalbf eingerichtet. Außerdem haben wir für Sie ab ca. 01:00 Uhr einen Ersatzverkehr mit 4 Bussen der Firma Holiday-Reisen GmbH zwischen Hanau Hbf und Offenbach(Main)Ost eingerichtet. Beziehen Sie auch andere Verkehrsmittel mit ein, um an Ihr Reiseziel zu gelangen. Bitte informieren Sie sich frühzeitig über Ihre geplanten Verbindungen und wählen Sie gegebenenfalls eine frühere Zugverbindung, um Ihre Anschlüsse an Ihren Umsteigebahnhöfen erreichen zu können.',
type: 'warning',
},
{
code: '40',
summary: 'defektes Stellwerk',
text: 'defektes Stellwerk',
type: 'status',
},
{
code: 'CUSTOMER_TEXT',
summary: 'Die Halte Frankfurt(Main)-Gateway Gardens, Frankfurt(M) Flughafen Regionalbf und Kelsterbach entfallen. Bitte prüfen Sie Ihre Reiseverbindung kurz vor der Abfahrt des Zuges.',
text: 'Die Halte Frankfurt(Main)-Gateway Gardens, Frankfurt(M) Flughafen Regionalbf und Kelsterbach entfallen. Bitte prüfen Sie Ihre Reiseverbindung kurz vor der Abfahrt des Zuges.',
type: 'status',
},
{
code: 'CUSTOMER_TEXT',
summary: 'Auf der S8 und S9 kommt es zwischen Wiesbaden Hbf - Frankfurt(M) Flughafen Regionalbf - Frankfurt(Main)Hbf - Offenbach(Main)Ost in den Nächten bis zum 22.03.25 zu geänderten Fahrtzeiten, einzelnen Umleitungen, Teil- und Zugausfällen. Der Grund sind Bauarbeiten. Auf dem jeweils ausfallenden Abschnitt haben wir für Sie einen Ersatzverkehr mit Bussen eingerichtet. Die Mitnahme von Fahrrädern im Bus ist ausgeschlossen. Bitte informieren Sie sich frühzeitig über Ihre geplanten Verbindungen und wählen Sie gegebenenfalls eine frühere Zugverbindung, um Ihre Anschlüsse an Ihren Umsteigebahnhöfen erreichen zu können. Die Fahrplanänderungen zu dieser Baumaßnahme sind in die Online-Fahrplanauskünfte eingearbeitet.',
text: 'Auf der S8 und S9 kommt es zwischen Wiesbaden Hbf - Frankfurt(M) Flughafen Regionalbf - Frankfurt(Main)Hbf - Offenbach(Main)Ost in den Nächten bis zum 22.03.25 zu geänderten Fahrtzeiten, einzelnen Umleitungen, Teil- und Zugausfällen. Der Grund sind Bauarbeiten. Auf dem jeweils ausfallenden Abschnitt haben wir für Sie einen Ersatzverkehr mit Bussen eingerichtet. Die Mitnahme von Fahrrädern im Bus ist ausgeschlossen. Bitte informieren Sie sich frühzeitig über Ihre geplanten Verbindungen und wählen Sie gegebenenfalls eine frühere Zugverbindung, um Ihre Anschlüsse an Ihren Umsteigebahnhöfen erreichen zu können. Die Fahrplanänderungen zu dieser Baumaßnahme sind in die Online-Fahrplanauskünfte eingearbeitet.',
type: 'status',
},
{
code: 'FK',
summary: 'Fahrradmitnahme begrenzt möglich',
text: 'Fahrradmitnahme begrenzt möglich',
type: 'hint',
},
{
code: 'EH',
summary: 'Fahrzeuggebundene Einstiegshilfe vorhanden',
text: 'Fahrzeuggebundene Einstiegshilfe vorhanden',
type: 'hint',
},
],
origin: null,
destination: {
type: 'station',
id: '8000250',
name: 'Wiesbaden Hbf',
},
cancelled: true,
},
{
tripId: '20250322-1423f343-0076-3f29-b1e8-004dc1f27068',
stop: {
type: 'station',
id: '8098105',
name: 'Frankfurt Hbf (tief)',
},
when: '2025-03-22T00:47:00+01:00',
plannedWhen: '2025-03-22T00:47:00+01:00',
delay: 0,
platform: '101',
plannedPlatform: '101',
direction: 'Darmstadt Hbf',
provenance: null,
line: {
type: 'line',
id: 's-6',
fahrtNr: '36683',
name: 'S 6',
public: true,
adminCode: '800528',
productName: 'S',
mode: 'train',
product: 'suburban',
operator: {
type: 'operator',
id: 'DB',
name: 'DB Regio, S-Bahn Rhein-Main',
},
},
remarks: [
{
code: 'FK',
summary: 'Fahrradmitnahme begrenzt möglich',
text: 'Fahrradmitnahme begrenzt möglich',
type: 'hint',
},
{
code: 'EH',
summary: 'Fahrzeuggebundene Einstiegshilfe vorhanden',
text: 'Fahrzeuggebundene Einstiegshilfe vorhanden',
type: 'hint',
},
],
origin: null,
destination: {
type: 'station',
id: '8000068',
name: 'Darmstadt Hbf',
},
},
{
tripId: '20250322-841901b7-7420-3890-8691-05c8cbbacdce',
stop: {
type: 'station',
id: '8098105',
name: 'Frankfurt Hbf (tief)',
},
when: '2025-03-22T01:05:00+01:00',
plannedWhen: '2025-03-22T00:49:00+01:00',
delay: 960,
platform: '102',
plannedPlatform: '102',
direction: 'Rödermark-Ober Roden',
provenance: null,
line: {
type: 'line',
id: 's-1',
fahrtNr: '35185',
name: 'S 1',
public: true,
adminCode: '800528',
productName: 'S',
mode: 'train',
product: 'suburban',
operator: {
type: 'operator',
id: 'DB',
name: 'DB Regio, S-Bahn Rhein-Main',
},
},
remarks: [
{
code: '64',
summary: 'Reparatur an einer Weiche',
text: 'Reparatur an einer Weiche',
type: 'status',
},
{
code: 'FK',
summary: 'Fahrradmitnahme begrenzt möglich',
text: 'Fahrradmitnahme begrenzt möglich',
type: 'hint',
},
{
code: 'EH',
summary: 'Fahrzeuggebundene Einstiegshilfe vorhanden',
text: 'Fahrzeuggebundene Einstiegshilfe vorhanden',
type: 'hint',
},
],
origin: null,
destination: {
type: 'station',
id: '8000285',
name: 'Rödermark-Ober Roden',
},
},
{
tripId: '20250322-08673537-4773-3e90-afa3-ab467119a609',
stop: {
type: 'station',
id: '100010',
name: 'Hauptbahnhof, Frankfurt a.M.',
},
when: '2025-03-22T00:50:00+01:00',
plannedWhen: '2025-03-22T00:50:00+01:00',
delay: 0,
platform: null,
plannedPlatform: null,
direction: 'Bockenheimer Warte, Frankfurt a.M.',
provenance: null,
line: {
type: 'line',
id: 'u-4',
fahrtNr: '1954',
name: 'U 4',
public: true,
adminCode: 'rmv255',
productName: 'U',
mode: 'train',
product: 'subway',
operator: {
type: 'operator',
id: 'DPN',
name: 'Nahreisezug',
},
},
remarks: [],
origin: null,
destination: {
type: 'station',
id: '101201',
name: 'Bockenheimer Warte, Frankfurt a.M.',
},
},
{
tripId: '20250322-29a07ddd-969e-3660-ae2e-4d6c6af4a866',
stop: {
type: 'station',
id: '8000105',
name: 'Frankfurt(Main)Hbf',
},
when: null,
plannedWhen: '2025-03-22T00:50:00+01:00',
prognosedWhen: null,
delay: null,
platform: null,
plannedPlatform: '2',
prognosedPlatform: '2',
direction: 'Riedstadt-Goddelau',
provenance: null,
line: {
type: 'line',
id: 's-7',
fahrtNr: '35787',
name: 'S 7',
public: true,
adminCode: '800528',
productName: 'S',
mode: 'train',
product: 'suburban',
operator: {
type: 'operator',
id: 'DB',
name: 'DB Regio, S-Bahn Rhein-Main',
},
},
remarks: [
{
code: undefined,
summary: 'S7: Kein Betrieb möglich. Grund: Kurzfristige Erkrankung von Personal. Am 21.03.25/22.03.25 von 18:00 Uhr bis voraussichtlich 03:00 Uhr. Alternative Reisemöglichkeit: RE70. Die Züge halten zusätzlich in Riedstadt-Wolfskehlen - Groß Gerau-Dornheim - Zeppelinheim. Reiseverbindung frühzeitig prüfen.',
text: 'Auf der S7 ist kein Betrieb möglich. Der Grund ist eine kurzfristige Erkrankung von Personal. Die Beeinträchtigung ist am 21.03.25/22.03.25 von 18:00 Uhr bis voraussichtlich 03:00 Uhr. Alternative Reisemöglichkeit: RE70. Die Züge halten zusätzlich für Sie in Riedstadt-Wolfskehlen - Groß Gerau-Dornheim - Zeppelinheim zum Ein- und Ausstieg. Bitte informieren Sie sich frühzeitig über Ihre geplanten Verbindungen und wählen Sie gegebenenfalls eine frühere Zugverbindung, um Ihre Anschlüsse an Ihren Umsteigebahnhöfen erreichen zu können.',
type: 'warning',
},
{
code: '49',
summary: 'kurzfristiger Personalausfall',
text: 'kurzfristiger Personalausfall',
type: 'status',
},
{
code: 'FK',
summary: 'Fahrradmitnahme begrenzt möglich',
text: 'Fahrradmitnahme begrenzt möglich',
type: 'hint',
},
{
code: 'EH',
summary: 'Fahrzeuggebundene Einstiegshilfe vorhanden',
text: 'Fahrzeuggebundene Einstiegshilfe vorhanden',
type: 'hint',
},
],
origin: null,
destination: {
type: 'station',
id: '8000126',
name: 'Riedstadt-Goddelau',
},
cancelled: true,
},
{
tripId: '20250322-01575b72-00ad-36da-bc88-49410d87321a',
stop: {
type: 'station',
id: '8098105',
name: 'Frankfurt Hbf (tief)',
},
when: '2025-03-22T00:52:00+01:00',
plannedWhen: '2025-03-22T00:52:00+01:00',
delay: 0,
platform: '103',
plannedPlatform: '103',
direction: 'Niedernhausen(Taunus)',
provenance: null,
line: {
type: 'line',
id: 's-2',
fahrtNr: '35284',
name: 'S 2',
public: true,
adminCode: '800528',
productName: 'S',
mode: 'train',
product: 'suburban',
operator: {
type: 'operator',
id: 'DB',
name: 'DB Regio, S-Bahn Rhein-Main',
},
},
remarks: [
{
code: 'FK',
summary: 'Fahrradmitnahme begrenzt möglich',
text: 'Fahrradmitnahme begrenzt möglich',
type: 'hint',
},
{
code: 'EH',
summary: 'Fahrzeuggebundene Einstiegshilfe vorhanden',
text: 'Fahrzeuggebundene Einstiegshilfe vorhanden',
type: 'hint',
},
],
origin: null,
destination: {
type: 'station',
id: '8004400',
name: 'Niedernhausen(Taunus)',
},
},
{
tripId: '20250322-2b5b5913-2b2d-39da-b29d-2fff067ba6a2',
stop: {
type: 'station',
id: '8098105',
name: 'Frankfurt Hbf (tief)',
},
when: null,
plannedWhen: '2025-03-22T00:52:00+01:00',
prognosedWhen: null,
delay: null,
platform: null,
plannedPlatform: '101',
prognosedPlatform: '101',
direction: 'Frankfurt(Main)Süd',
provenance: null,
line: {
type: 'line',
id: 's-4',
fahrtNr: '35483',
name: 'S 4',
public: true,
adminCode: '800528',
productName: 'S',
mode: 'train',
product: 'suburban',
operator: {
type: 'operator',
id: 'DB',
name: 'DB Regio, S-Bahn Rhein-Main',
},
},
remarks: [
{
code: undefined,
summary: 'S3 und S4: Kein Betrieb möglich. Am 21./22.03.25 von 20:00 Uhr bis voraussichtlich 04:00 Uhr. Grund: Erkrankung von Stellwerkspersonal. Ersatzverkehr mit Bussen eingerichtet. Reiseverbindung frühzeitig prüfen.',
text: 'Auf der S3 und S4 ist kein Betrieb möglich. Die Beeinträchtigung ist am 21./22.03.25 von 20:00 Uhr bis voraussichtlich 04:00 Uhr. Der Grund ist eine Erkrankung von Stellwerkspersonal. Wir haben für Sie einen Ersatzverkehr mit 8 Bussen der Firma Holiday-Reisen GmbH zwischen Frankfurt(Main)Hbf und Bad Soden(Taunus) eingerichtet. Außerdem haben wir für Sie einen Ersatzverkehr mit 7 Bussen der Firma Holiday-Reisen GmbH zwischen Frankfurt(Main)Hbf und Kronberg(Taunus) eingerichtet. Bitte informieren Sie sich frühzeitig über Ihre geplanten Verbindungen und wählen Sie gegebenenfalls eine frühere Zugverbindung, um Ihre Anschlüsse an Ihren Umsteigebahnhöfen erreichen zu können.',
type: 'warning',
},
{
code: '40',
summary: 'defektes Stellwerk',
text: 'defektes Stellwerk',
type: 'status',
},
{
code: 'FK',
summary: 'Fahrradmitnahme begrenzt möglich',
text: 'Fahrradmitnahme begrenzt möglich',
type: 'hint',
},
{
code: 'EH',
summary: 'Fahrzeuggebundene Einstiegshilfe vorhanden',
text: 'Fahrzeuggebundene Einstiegshilfe vorhanden',
type: 'hint',
},
],
origin: null,
destination: {
type: 'station',
id: '8002041',
name: 'Frankfurt(Main)Süd',
},
cancelled: true,
},
];
export {
dbDepartures,
};

1
test/fixtures/dbris-departures.json vendored Normal file

File diff suppressed because one or more lines are too long

1508
test/fixtures/dbweb-departures.js vendored Normal file

File diff suppressed because it is too large Load diff

189
test/fixtures/dbweb-departures.json vendored Normal file
View file

@ -0,0 +1,189 @@
{
"entries": [
{
"bahnhofsId": "8000295",
"zeit": "2025-02-08T15:31:00",
"ezZeit": "2025-02-08T16:05:00",
"gleis": "2",
"ueber": [
"Osterburken",
"Möckmühl",
"Bad Friedrichshall Hbf",
"Neckarsulm",
"Heilbronn Hbf",
"Bietigheim-Bissingen",
"Ludwigsburg",
"Stuttgart Hbf"
],
"journeyId": "2|#VN#1#ST#1738783727#PI#0#ZI#315246#TA#0#DA#80225#1S#8000260#1T#1437#LS#8000096#LT#1653#PU#80#RT#1#CA#DPN#ZE#19073#ZB#RE 19073#PC#3#FR#8000260#FT#1437#TO#8000096#TT#1653#",
"meldungen": [],
"verkehrmittel": {
"name": "RE 19073",
"kurzText": "RE",
"mittelText": "RE 8",
"langText": "RE 19073",
"produktGattung": "REGIONAL"
},
"terminus": "Stuttgart Hbf"
},
{
"bahnhofsId": "508987",
"zeit": "2025-02-08T16:20:00",
"ueber": [
"Bahnhof, Osterburken",
"Rathaus, Osterburken",
"RIO, Osterburken",
"Merchingen Ort, Ravenstein",
"Hüngheim Ort, Ravenstein",
"Oberwittstadt Ort, Ravenstein",
"Unterwittstadt Ort, Ravenstein",
"Erlenbach, Ravenstein"
],
"journeyId": "2|#VN#1#ST#1738783727#PI#0#ZI#500575#TA#0#DA#80225#1S#508987#1T#1620#LS#506182#LT#1655#PU#80#RT#1#CA#Bus#ZE#844#ZB#Bus 844#PC#5#FR#508987#FT#1620#TO#506182#TT#1655#",
"meldungen": [],
"verkehrmittel": {
"name": "Bus 844",
"linienNummer": "844",
"kurzText": "Bus",
"mittelText": "Bus 844",
"langText": "Bus 844",
"produktGattung": "BUS"
},
"terminus": "Erlenbach"
},
{
"bahnhofsId": "8000295",
"zeit": "2025-02-08T16:27:00",
"ezZeit": "2025-02-08T16:27:00",
"gleis": "4",
"ueber": [
"Osterburken",
"Lauda",
"Würzburg Hbf"
],
"journeyId": "2|#VN#1#ST#1738783727#PI#0#ZI#315248#TA#0#DA#80225#1S#8000096#1T#1506#LS#8000260#LT#1721#PU#80#RT#1#CA#DPN#ZE#19074#ZB#RE 19074#PC#3#FR#8000096#FT#1506#TO#8000260#TT#1721#",
"meldungen": [],
"verkehrmittel": {
"name": "RE 19074",
"kurzText": "RE",
"mittelText": "RE 8",
"langText": "RE 19074",
"produktGattung": "REGIONAL"
},
"terminus": "Würzburg Hbf"
},
{
"bahnhofsId": "8000295",
"zeit": "2025-02-08T16:31:00",
"ezZeit": "2025-02-08T16:39:00",
"gleis": "2",
"ueber": [
"Osterburken",
"Möckmühl",
"Bad Friedrichshall Hbf",
"Neckarsulm",
"Heilbronn Hbf",
"Bietigheim-Bissingen",
"Ludwigsburg",
"Stuttgart Hbf"
],
"journeyId": "2|#VN#1#ST#1738783727#PI#0#ZI#316113#TA#0#DA#80225#1S#8000260#1T#1537#LS#8000096#LT#1756#PU#80#RT#1#CA#DPN#ZE#63379#ZB#RE 63379#PC#3#FR#8000260#FT#1537#TO#8000096#TT#1756#",
"meldungen": [
{
"prioritaet": "NIEDRIG",
"text": "Keine rollstuhlgerechte Einrichtung, kein behindertengerechtes WC im Zug. Mobilitätseingeschränkte Reisende wenden sich bzgl. eventuell erforderlicher Umbuchungen an unsere Mobilitätsservice-Zentrale unter 030 65212888."
}
],
"verkehrmittel": {
"name": "RE 63379",
"kurzText": "RE",
"mittelText": "RE 8",
"langText": "RE 63379",
"produktGattung": "REGIONAL"
},
"terminus": "Stuttgart Hbf"
},
{
"bahnhofsId": "510853",
"zeit": "2025-02-08T16:35:00",
"ueber": [
"Bahnhof, Osterburken",
"Hohenstadt Ort, Ahorn (Baden)",
"Eubigheim Kirche, Ahorn (Baden)",
"Eubigheim Obereubigheim Ort, Ahorn (Baden)",
"Buch Ort, Ahorn (Baden)",
"Uiffingen Ort, Boxberg (Baden)",
"Angeltürn Ort, Boxberg (Baden)",
"Rathaus, Boxberg (Baden)"
],
"journeyId": "2|#VN#1#ST#1738783727#PI#0#ZI#1108875#TA#4#DA#80225#1S#510853#1T#1635#LS#421730#LT#1713#PU#80#RT#1#CA#rfb#ZE#9839#ZB#RUF 9839#PC#9#FR#510853#FT#1635#TO#421730#TT#1713#",
"meldungen": [],
"verkehrmittel": {
"name": "RUF 9839",
"linienNummer": "9839",
"kurzText": "RUF",
"mittelText": "RUF 9839",
"langText": "RUF 9839",
"produktGattung": "ANRUFPFLICHTIG"
},
"terminus": "Rathaus, Boxberg (Baden)"
},
{
"bahnhofsId": "8000295",
"zeit": "2025-02-08T16:36:00",
"gleis": "1",
"ueber": [
"Osterburken",
"Adelsheim Nord",
"Zimmern(b Seckach)",
"Seckach",
"Eicholzheim",
"Oberschefflenz",
"Auerbach(b Mosbach, Baden)",
"Homburg(Saar)Hbf"
],
"journeyId": "2|#VN#1#ST#1738783727#PI#0#ZI#248243#TA#0#DA#80225#1S#8000295#1T#1636#LS#8000176#LT#2006#PU#80#RT#1#CA#s#ZE#1#ZB#S 1#PC#4#FR#8000295#FT#1636#TO#8000176#TT#2006#",
"meldungen": [],
"verkehrmittel": {
"name": "S 1",
"linienNummer": "1",
"kurzText": "S",
"mittelText": "S 1",
"langText": "S 1",
"produktGattung": "SBAHN"
},
"terminus": "Homburg(Saar)Hbf"
},
{
"bahnhofsId": "8000295",
"zeit": "2025-02-08T16:36:00",
"gleis": "3",
"ueber": [
"Osterburken",
"Adelsheim Ost",
"Sennfeld",
"Roigheim",
"Möckmühl",
"Züttlingen",
"Siglingen",
"Tübingen Hbf"
],
"journeyId": "2|#VN#1#ST#1738783727#PI#0#ZI#898552#TA#0#DA#80225#1S#8000295#1T#1636#LS#8000141#LT#1922#PU#80#RT#1#CA#DPN#ZE#19329#ZB#MEX19329#PC#3#FR#8000295#FT#1636#TO#8000141#TT#1922#",
"meldungen": [
{
"prioritaet": "HOCH",
"text": "Halt entfällt",
"type": "HALT_AUSFALL"
}
],
"verkehrmittel": {
"name": "MEX19329",
"kurzText": "MEX",
"mittelText": "MEX 18",
"langText": "MEX19329",
"produktGattung": "REGIONAL"
},
"terminus": "Tübingen Hbf"
}
]
}

View file

@ -1,4 +1,4 @@
const dbJourney = {
const dbwebJourney = {
type: 'journey',
legs: [
{
@ -292,7 +292,7 @@ const dbJourney = {
},
],
refreshToken: '¶HKI¶T$A=1@O=Köln Hbf@X=6958730@Y=50943029@L=8000207@a=128@$A=1@O=Köln Messe/Deutz@X=6975000@Y=50940872@L=8003368@a=128@$202504110511$202504110512$S 12$$1$$$$$$§W$A=1@O=Köln Messe/Deutz@X=6975000@Y=50940872@L=8003368@a=128@$A=1@O=Köln Messe/Deutz Gl.11-12@X=6974065@Y=50941717@L=8073368@a=128@$202504110512$202504110519$$$1$$$$$$§T$A=1@O=Köln Messe/Deutz Gl.11-12@X=6974065@Y=50941717@L=8073368@a=128@$A=1@O=Nürnberg Hbf@X=11082989@Y=49445615@L=8000284@a=128@$202504110520$202504110858$ICE 523$$1$$$$$$',
price: {amount: 31.49, currency: 'EUR', hint: null},
price: {amount: 31.49, currency: 'EUR', hint: null, partialFare: false},
tickets: [{
name: 'from',
priceObj: {
@ -304,5 +304,5 @@ const dbJourney = {
};
export {
dbJourney,
dbwebJourney,
};

View file

@ -210,6 +210,7 @@ const dbJourney = {
amount: 27.99,
currency: 'EUR',
hint: null,
partialFare: false,
},
tickets: [
{

View file

@ -1,4 +1,4 @@
const dbTrip = {
const dbwebTrip = {
trip: {
id: 'foo',
origin: {
@ -470,5 +470,5 @@ const dbTrip = {
};
export {
dbTrip,
dbwebTrip,
};

47
test/format/db-trip.js Normal file
View file

@ -0,0 +1,47 @@
import tap from 'tap';
import {profile as rawProfile} from '../../p/db/index.js';
import {createClient} from '../../index.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});
const {profile} = client;
const opt = {
stopovers: true,
polyline: false,
remarks: true,
language: 'en',
};
const tripIdHafas = '2|#VN#1#ST#1738783727#PI#0#ZI#222242#TA#0#DA#70225#1S#8000237#1T#1317#LS#8000261#LT#2002#PU#80#RT#1#CA#ICE#ZE#1007#ZB#ICE 1007#PC#0#FR#8000237#FT#1317#TO#8000261#TT#2002#';
const tripIdRis = '20250207-e6b2807e-bb48-39f9-89eb-8491ebc4b32c';
const reqDbNavExpected = {
endpoint: 'https://app.vendo.noncd.db.de/mob/zuglauf/',
path: '2%7C%23VN%231%23ST%231738783727%23PI%230%23ZI%23222242%23TA%230%23DA%2370225%231S%238000237%231T%231317%23LS%238000261%23LT%232002%23PU%2380%23RT%231%23CA%23ICE%23ZE%231007%23ZB%23ICE%201007%23PC%230%23FR%238000237%23FT%231317%23TO%238000261%23TT%232002%23',
headers: {
'Accept': 'application/x.db.vendo.mob.zuglauf.v2+json',
'Content-Type': 'application/x.db.vendo.mob.zuglauf.v2+json',
},
method: 'get',
};
const reqDbRegioGuideExpected = {
endpoint: 'https://regio-guide.de/@prd/zupo-travel-information/api/public/ri/journey/',
path: '20250207-e6b2807e-bb48-39f9-89eb-8491ebc4b32c',
method: 'get',
};
tap.test('db trip(): dynamic request formatting', (t) => {
const ctx = {profile, opt};
t.notHas(client.profile, 'tripEndpoint');
const reqDbNav = profile.formatTripReq(ctx, tripIdHafas);
delete reqDbNav.headers['X-Correlation-ID'];
const reqDbRegioGuide = profile.formatTripReq(ctx, tripIdRis);
t.same(reqDbNav, reqDbNavExpected);
t.same(reqDbRegioGuide, reqDbRegioGuideExpected);
t.end();
});

View file

@ -41,6 +41,10 @@ const berlinWienQuery0 = Object.freeze(
einstiegsTypList: [
'STANDARD',
],
fahrverguenstigungen: {
deutschlandTicketVorhanden: undefined,
nurDeutschlandTicketVerbindungen: undefined,
},
klasse: 'KLASSE_2',
reiseHin: {
wunsch: {

View file

@ -0,0 +1,47 @@
import tap from 'tap';
import {createClient} from '../../index.js';
import {profile as rawProfile} from '../../p/dbweb/index.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test');
const {profile} = client;
const opt = {
when: new Date('2025-02-09T23:55:00+01:00'),
remarks: true,
stopovers: true,
language: 'en',
};
const berlinArrivalsQuery = {
endpoint: 'https://int.bahn.de/web/api/reiseloesung/',
path: 'ankuenfte',
query: {
ortExtId: '8011160',
zeit: '23:55',
datum: '2025-02-09',
mitVias: true,
verkehrsmittel: [
'ICE',
'EC_IC',
'IR',
'REGIONAL',
'SBAHN',
'BUS',
'SCHIFF',
'UBAHN',
'TRAM',
'ANRUFPFLICHTIG',
],
},
method: 'GET',
};
tap.test('formats an arrivals() request correctly', (t) => {
const ctx = {profile, opt};
const req = profile.formatStationBoardReq(ctx, '8011160', 'arrivals');
t.same(req, berlinArrivalsQuery);
t.end();
});

View file

@ -1,7 +1,7 @@
import tap from 'tap';
import {createClient} from '../../index.js';
import {profile as rawProfile} from '../../p/db/index.js';
import {profile as rawProfile} from '../../p/dbweb/index.js';
import {data as loyaltyCards} from '../../format/loyalty-cards.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});
@ -57,8 +57,8 @@ const berlinWienQuery0 = Object.freeze(
sitzplatzOnly: false,
bikeCarriage: false,
reservierungsKontingenteVorhanden: false,
nurDeutschlandTicketVerbindungen: false,
deutschlandTicketVorhanden: false,
nurDeutschlandTicketVerbindungen: undefined,
deutschlandTicketVorhanden: undefined,
maxUmstiege: null,
zwischenhalte: null,
minUmstiegszeit: 0,
@ -91,7 +91,7 @@ tap.test('formats a journeys() request correctly (DB)', (t) => {
});
tap.test('formats a journeys() request with BC correctly (DB)', (t) => {
tap.test('formats a journeys() request with BC correctly (dbweb)', (t) => {
const ctx = {profile, opt};
const req = profile.formatJourneysReq(ctx, '8098160', '8000284', new Date('2024-12-07T23:50:12+01:00'), true, null);
@ -115,7 +115,7 @@ tap.test('formats a journeys() request with BC correctly (DB)', (t) => {
t.end();
});
tap.test('formats a journeys() request with unlimited transfers (DB)', (t) => {
tap.test('formats a journeys() request with unlimited transfers (dbweb)', (t) => {
const _opt = {...opt};
const ctx = {profile, opt: _opt};

View file

@ -1,8 +1,3 @@
// 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 {
checkIfResponseIsOk as checkIfResIsOk,

497
test/parse/dbnav-journey.js Normal file
View file

@ -0,0 +1,497 @@
import tap from 'tap';
import {parseJourney as parseJourneyDbnav} from '../../p/dbnav/parse-journey.js';
import {parseJourney as parseJourneyDefault} from '../../parse/journey.js';
import {parseJourneyLeg} from '../../parse/journey-leg.js';
import {parseWhen} from '../../parse/when.js';
import {parseDateTime} from '../../parse/date-time.js';
import {parsePlatform} from '../../parse/platform.js';
const ctx = {
opt: {},
profile: {
enrichStation: (ctx, stop) => stop,
parseCancelled: _ => null,
parseDateTime,
parseJourneyLeg,
parseLine: _ => null,
parseLoadFactor: _ => null,
parseLocation: _ => ({latitude: null, longitude: null}),
parseOperator: _ => null,
parsePlatform,
parsePrice: _ => null,
parseTickets: _ => null,
parseWhen,
timezone: 'Europe/Berlin',
locale: 'de-DE',
},
};
const input = {
reiseDauer: 480,
alternative: true,
checksum: 'bfc0da75_3',
umstiegeAnzahl: 0,
schemaVersion: '1.23.8',
schemaName: 'trip',
kontext: '¶HKI¶G@F$A=4@O=Berlin, Fernsehturm (Tourismus)@X=13409694@Y=52521301@L=991671997@a=128@$A=1@O=Berlin Alexanderplatz@X=13410728@Y=52521409@L=618011155@a=128@$202503061452$202503061454$$$1$$$$$$§W$A=1@O=Berlin Alexanderplatz@X=13410728@Y=52521409@L=618011155@a=128@$A=1@O=Berlin Alexanderplatz@X=13410962@Y=52521481@L=8011155@a=128@$202503061454$202503061454$$$1$$$$$$§T$A=1@O=Berlin Alexanderplatz@X=13410962@Y=52521481@L=8011155@a=128@$A=1@O=Berlin Hbf@X=13369549@Y=52525589@L=8011160@a=128@$202503061454$202503061459$RE 73737$$1$$$$$$¶GP¶ft@0@2000@120@1@100@1@0@0@@@@@false@0@-1@0@-1@-1@$f@$f@$f@$f@$f@$§bt@0@2000@120@1@100@1@0@0@@@@@false@0@-1@0@-1@-1@$f@$f@$f@$f@$f@$§tf@$f@$f@$f@$f@$f@$§¶KC¶#VE#2#CF#100#CA#0#CM#0#SICT#0#AM#81#AM2#0#RT#7#¶KCC¶I1ZFIzEjRVJHIzIjSElOIzAjRUNLIzB8MHwwfDB8MHwwfDB8MHwwfDB8MHwwfDB8MCNHQU0jNjAzMjUxNDU0IwpGI1ZOIzAjU1QjMTc0MDU3ODc3MiNQSSMxI1BVIzkwI1pJIzMzNTU3OTIxMjQjREEjNjAzMjUjRlIjNjE4MDExMTU1I1RPIzgwMTExNTUjRlQjMTQ1NCNUVCMxNDU0I1RTIzMxI0ZGIyNGViMwI1NUIzE3NDEwMzIwNzkjUEkjMCNQVSM4MCMKWiNWTiMxI1NUIzE3NDEwMzIwNzkjUEkjMCNaSSM1NDI4NjMjVEEjMiNEQSM2MDMyNSMxUyM4MDEwMTEzIzFUIzEzNTUjTFMjODAxMDIyNCNMVCMxNjQ3I1BVIzgwI1JUIzEjQ0EjRFBOI1pFIzczNzM3I1pCI1JFIDczNzM3I1BDIzMjRlIjODAxMTE1NSNGVCMxNDU0I1RPIzgwMTExNjAjVFQjMTQ1OSM=¶KRCC¶#VE#1#VOLL#MRTF#IST#¶SC¶1_H4sIAAAAAAACA22PXUvDMBiF/8p4rxTqSPq1NVCoXSkoxY0xhiJehDXbKmkz8zEspf/dtFUQ9C7n8OSc83ZwZRII4PnSBwfYp7YiS+f7bI5da0j2AaSDxtQ5kMAZHikQ5IAwOqOaWdpFboA8FMJo7qp6MLEfBAhZ6zgm3GEH3ps251oWQF470O1lwDbbdWahWpSDenhaWXGl3IwRyPWgfxtHrc6nKdg2l+xSiMMUw6vSkvexn6zjlEleNc4sZ7JR7KyNrGc3O2FkpWqjbpMijiIcLnAULZLnGHs+isLIT17iwA1c7CGU2HKlp6PysZlK+acK26AlwhiH/3w4MZ1td0C0NGxUG8Fbu4r9sh7tpIa1qTBNqYAcKVffLFWKV0r/sOwgNlTS2kJd3/df/HilQasBAAA=',
echtzeitNotizen: [],
himNotizen: [],
auslastungsInfos: [
{
klasse: 'KLASSE_1',
stufe: 0,
anzeigeTextKurz: 'No occupancy information available',
},
{
klasse: 'KLASSE_2',
stufe: 0,
anzeigeTextKurz: 'No occupancy information available',
},
],
serviceDays: [
{
irregular: 'not 8. until 19. Mar 2025',
letztesDatumInZeitraum: '2025-12-13',
planungsZeitraumAnfang: '2024-12-15',
planungsZeitraumEnde: '2025-12-13',
regular: 'daily',
wochentage: [
'DO',
'FR',
],
},
],
verbindungsAbschnitte: [
{
reservierungsMeldungen: [],
nummer: 0,
abschnittsDauer: 120,
typ: 'FUSSWEG',
halte: [],
distanz: 137,
himNotizen: [],
echtzeitNotizen: [],
attributNotizen: [],
abgangsOrt: {
name: 'Berlin, Fernsehturm (Tourismus)',
locationId: 'A=4@O=Berlin, Fernsehturm (Tourismus)@X=13409694@Y=52521301@U=91@L=991671997@',
evaNr: '991671997',
position: {
latitude: 52.5213,
longitude: 13.409694,
},
},
abgangsDatum: '2025-03-06T14:52:00+01:00',
ezAbgangsDatum: '2025-03-06T14:52:00Z', // upstream bug: wrong time zone offset
ankunftsOrt: {
name: 'Berlin Alexanderplatz',
locationId: 'A=1@O=Berlin Alexanderplatz@X=13410962@Y=52521481@U=80@L=8011155@i=U×008003135@',
evaNr: '8011155',
position: {
latitude: 52.521526,
longitude: 13.411088,
},
stationId: '0053',
},
ankunftsDatum: '2025-03-06T14:54:00+01:00',
ezAnkunftsDatum: '2025-03-06T14:54:00Z', // upstream bug: wrong time zone offset
produktGattung: 'SONSTIGE',
wagenreihung: false,
auslastungsInfos: [
{
klasse: 'KLASSE_1',
stufe: 0,
anzeigeTextKurz: 'No occupancy information available',
},
{
klasse: 'KLASSE_2',
stufe: 0,
anzeigeTextKurz: 'No occupancy information available',
},
],
parallelZuege: [],
},
{
administrationId: 'OWRE__',
risZuglaufId: 'RE_73737',
risAbfahrtId: '8011155_2025-03-06T14:54:00+01:00',
kurztext: 'RE',
mitteltext: 'RE 1',
langtext: 'RE 1 (73737)',
zuglaufId: '2|#VN#1#ST#1741032079#PI#0#ZI#542863#TA#2#DA#60325#1S#8010113#1T#1355#LS#8010224#LT#1647#PU#80#RT#1#CA#DPN#ZE#73737#ZB#RE 73737#PC#3#FR#8010113#FT#1355#TO#8010224#TT#1647#',
reservierungsMeldungen: [],
nummer: 1,
abschnittsDauer: 300,
typ: 'FAHRZEUG',
halte: [
{
abgangsDatum: '2025-03-06T14:54:00+01:00',
ezAbgangsDatum: '2025-03-06T14:55:00+01:00',
ort: {
name: 'Berlin Alexanderplatz',
locationId: 'A=1@O=Berlin Alexanderplatz@X=13410962@Y=52521481@U=80@L=8011155@i=U×008003135@',
evaNr: '8011155',
position: {
latitude: 52.521526,
longitude: 13.411088,
},
stationId: '0053',
},
gleis: '2',
attributNotizen: [],
echtzeitNotizen: [],
himNotizen: [],
auslastungsInfos: [
{
klasse: 'KLASSE_1',
stufe: 0,
anzeigeTextKurz: 'No occupancy information available',
},
{
klasse: 'KLASSE_2',
stufe: 0,
anzeigeTextKurz: 'No occupancy information available',
},
],
},
{
ankunftsDatum: '2025-03-06T14:56:00+01:00',
abgangsDatum: '2025-03-06T14:57:00+01:00',
ezAbgangsDatum: '2025-03-06T14:58:00+01:00',
ezAnkunftsDatum: '2025-03-06T14:57:00+01:00',
ort: {
name: 'Berlin Friedrichstraße',
locationId: 'A=1@O=Berlin Friedrichstraße@X=13387203@Y=52520376@U=80@L=8011306@i=U×008003137@',
evaNr: '8011306',
position: {
latitude: 52.520332,
longitude: 13.386925,
},
stationId: '0527',
},
gleis: '4',
attributNotizen: [],
echtzeitNotizen: [],
himNotizen: [],
auslastungsInfos: [
{
klasse: 'KLASSE_1',
stufe: 0,
anzeigeTextKurz: 'No occupancy information available',
},
{
klasse: 'KLASSE_2',
stufe: 0,
anzeigeTextKurz: 'No occupancy information available',
},
],
},
{
ankunftsDatum: '2025-03-06T14:59:00+01:00',
ezAnkunftsDatum: '2025-03-06T15:00:00+01:00',
ort: {
name: 'Berlin Hbf',
locationId: 'A=1@O=Berlin Hbf@X=13369549@Y=52525589@U=80@L=8011160@i=U×008065969@',
evaNr: '8011160',
position: {
latitude: 52.524925,
longitude: 13.369629,
},
stationId: '1071',
},
gleis: '14',
attributNotizen: [],
echtzeitNotizen: [],
himNotizen: [],
auslastungsInfos: [
{
klasse: 'KLASSE_1',
stufe: 0,
anzeigeTextKurz: 'No occupancy information available',
},
{
klasse: 'KLASSE_2',
stufe: 0,
anzeigeTextKurz: 'No occupancy information available',
},
],
},
],
himNotizen: [],
echtzeitNotizen: [],
attributNotizen: [
{
text: 'Number of bicycles conveyed limited',
key: 'FB',
priority: 260,
},
{
text: 'Conveyance of groups is limited',
key: 'GL',
priority: 330,
},
{
text: 'space for wheelchairs',
key: 'RO',
priority: 560,
},
{
text: 'vehicle-mounted access aid',
key: 'EH',
priority: 560,
},
{
text: 'Behindertengerechte Ausstattung',
key: 'EA',
priority: 560,
},
{
text: 'power sockets for laptop',
key: 'LS',
priority: 605,
},
{
text: 'air conditioning',
key: 'KL',
priority: 610,
},
{
text: 'Wifi available',
key: 'WV',
priority: 710,
},
{
text: 'Ostdeutsche Eisenbahn GmbH',
key: 'OP',
},
],
abgangsOrt: {
name: 'Berlin Alexanderplatz',
locationId: 'A=1@O=Berlin Alexanderplatz@X=13410962@Y=52521481@U=80@L=8011155@i=U×008003135@',
evaNr: '8011155',
position: {
latitude: 52.521526,
longitude: 13.411088,
},
stationId: '0053',
},
abgangsDatum: '2025-03-06T14:54:00+01:00',
ezAbgangsDatum: '2025-03-06T14:55:00+01:00',
ankunftsOrt: {
name: 'Berlin Hbf',
locationId: 'A=1@O=Berlin Hbf@X=13369549@Y=52525589@U=80@L=8011160@i=U×008065969@',
evaNr: '8011160',
position: {
latitude: 52.524925,
longitude: 13.369629,
},
stationId: '1071',
},
ankunftsDatum: '2025-03-06T14:59:00+01:00',
ezAnkunftsDatum: '2025-03-06T15:00:00+01:00',
verkehrsmittelNummer: '73737',
richtung: 'Magdeburg Hbf',
produktGattung: 'RB',
wagenreihung: false,
auslastungsInfos: [
{
klasse: 'KLASSE_1',
stufe: 0,
anzeigeTextKurz: 'No occupancy information available',
},
{
klasse: 'KLASSE_2',
stufe: 0,
anzeigeTextKurz: 'No occupancy information available',
},
],
parallelZuege: [],
polylineGroup: {
polylineDesc: [
{
coordinates: [
{
longitude: 13.411088,
latitude: 52.521526,
},
{
longitude: 13.411088,
latitude: 52.521526,
},
],
delta: false,
},
{
coordinates: [
{
longitude: 13.411088,
latitude: 52.521526,
},
{
longitude: 13.409676,
latitude: 52.522344,
},
{
longitude: 13.408202,
latitude: 52.522964,
},
{
longitude: 13.407177,
latitude: 52.523243,
},
{
longitude: 13.406377,
latitude: 52.523324,
},
{
longitude: 13.40502,
latitude: 52.523252,
},
{
longitude: 13.402233,
latitude: 52.522533,
},
{
longitude: 13.402233,
latitude: 52.522533,
},
{
longitude: 13.401191,
latitude: 52.52238,
},
{
longitude: 13.398629,
latitude: 52.522281,
},
{
longitude: 13.397613,
latitude: 52.522137,
},
{
longitude: 13.396669,
latitude: 52.52185,
},
{
longitude: 13.394125,
latitude: 52.520618,
},
{
longitude: 13.392831,
latitude: 52.520259,
},
{
longitude: 13.389451,
latitude: 52.520025,
},
{
longitude: 13.388309,
latitude: 52.520088,
},
{
longitude: 13.386925,
latitude: 52.520331,
},
{
longitude: 13.38634,
latitude: 52.520564,
},
{
longitude: 13.386322,
latitude: 52.520555,
},
{
longitude: 13.386376,
latitude: 52.520636,
},
{
longitude: 13.382844,
latitude: 52.521391,
},
{
longitude: 13.380129,
latitude: 52.521436,
},
{
longitude: 13.378906,
latitude: 52.521679,
},
{
longitude: 13.377576,
latitude: 52.522308,
},
{
longitude: 13.375598,
latitude: 52.52416,
},
{
longitude: 13.37452,
latitude: 52.524744,
},
{
longitude: 13.373126,
latitude: 52.525158,
},
{
longitude: 13.372398,
latitude: 52.525257,
},
{
longitude: 13.370043,
latitude: 52.525239,
},
{
longitude: 13.370043,
latitude: 52.525239,
},
{
longitude: 13.369567,
latitude: 52.525203,
},
],
delta: false,
},
{
coordinates: [
{
longitude: 13.369567,
latitude: 52.525203,
},
{
longitude: 13.369629,
latitude: 52.524924,
},
],
delta: false,
},
],
},
},
],
}
tap.test('dbnav profile fixes time zone bug', (t) => { // see https://github.com/public-transport/db-vendo-client/issues/24
const parsedJourney = parseJourneyDbnav(ctx, structuredClone(input));
const expectedDeparture = '2025-03-06T14:52:00+01:00';
t.equal(parsedJourney.legs[0].departure, expectedDeparture)
const expectedArrival = '2025-03-06T14:52:00+01:00';
t.equal(parsedJourney.legs[0].arrival, expectedArrival)
const expectedDelay = 0;
t.equal(parsedJourney.legs[0].departureDelay, expectedDelay)
t.equal(parsedJourney.legs[0].arrivalDelay, expectedDelay)
t.end();
})
tap.test('dbnav profile parses journey without time zone bug like other profiles', (t) => {
const _input = structuredClone(input);
// fix bug by hand
_input.verbindungsAbschnitte[0].ezAbgangsDatum = '2025-03-06T14:52:00+01:00';
_input.verbindungsAbschnitte[0].ezAnkunftsDatum = '2025-03-06T14:54:00+01:00';
const expected = parseJourneyDefault(ctx, _input)
t.same(parseJourneyDbnav(ctx, _input), expected);
t.end();
})

Some files were not shown because too many files have changed in this diff Show more