mirror of
https://github.com/public-transport/db-vendo-client.git
synced 2025-06-07 12:52:34 +03:00
Compare commits
59 commits
Author | SHA1 | Date | |
---|---|---|---|
|
b59d7b3084 | ||
|
db4c03054a | ||
|
eac21d188b | ||
|
ad09f8b1be | ||
|
c4d0a55d41 | ||
|
29aab87cdf | ||
|
883eb8c8de | ||
|
b20cf1060a | ||
|
b887c674d4 | ||
|
b3e0e764e2 | ||
|
2ea47f7792 | ||
|
6c2081c14e | ||
|
f741a13670 | ||
|
bcaad526c7 | ||
|
162b946bac | ||
|
14b80dbf33 | ||
|
1927f98906 | ||
|
0ef3935a35 | ||
|
b04a671b50 | ||
|
9975a6c9ac | ||
|
960371e2ec | ||
|
88acdd1620 | ||
|
25cbb288ca | ||
|
a6e84be2df | ||
|
de63bf0a37 | ||
|
040a8f44e4 | ||
|
6b67a77823 | ||
|
debb45a929 | ||
|
53b385a865 | ||
|
185870db3d | ||
|
16829f839c | ||
|
9fe4972d2b | ||
|
1aeb246622 | ||
|
6d1d0c626f | ||
|
afa99b0742 | ||
|
229dbac93e | ||
|
7a1e513fa2 | ||
|
f1302b0a7b | ||
|
177a3cab3f | ||
|
71d1a4f1a9 | ||
|
6ff406ea79 | ||
|
2a23e1ad9b | ||
|
9314e59053 | ||
|
69c098744a | ||
|
c671e995cb | ||
|
1e7977a8bb | ||
|
ff559c83dd | ||
|
76d6121f88 | ||
|
206e709e6a | ||
|
7d10f409ef | ||
|
179ada6f08 | ||
|
4c8c503e48 | ||
|
3c7227635a | ||
|
22c839847f | ||
|
1b0858a253 | ||
|
a59a3d78dc | ||
|
911a6d371e | ||
|
2b55f7148f | ||
|
715541f060 |
114 changed files with 18654 additions and 2521 deletions
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
35
.github/workflows/test.yml
vendored
35
.github/workflows/test.yml
vendored
|
@ -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
|
||||
|
||||
|
|
|
@ -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
40
api.js
|
@ -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
644
cspell.config.json
Normal 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"
|
||||
]
|
||||
}
|
19
docs/api.md
Normal file
19
docs/api.md
Normal file
|
@ -0,0 +1,19 @@
|
|||
# `db-vendo-client` API
|
||||
|
||||
Also see the [root readme](https://github.com/public-transport/db-vendo-client) for a shortlist of differences of db-vendo-client to hafas-client and of differences between the profiles.
|
||||
|
||||
- [`journeys(from, to, [opt])`](journeys.md) – get journeys between locations
|
||||
- [`refreshJourney(refreshToken, [opt])`](refresh-journey.md) – fetch up-to-date/more details of a `journey`
|
||||
- `journeysFromTrip(tripId, previousStopover, to, [opt])` – not supported
|
||||
- [`trip(id, lineName, [opt])`](trip.md) – get details for a trip
|
||||
- `tripsByName(lineNameOrFahrtNr, [opt])` – not supported
|
||||
- [`departures(station, [opt])`](departures.md) – query the next departures at a station
|
||||
- [`arrivals(station, [opt])`](arrivals.md) – query the next arrivals at a station
|
||||
- [`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 supported
|
||||
- `reachableFrom(address, [opt])` – not supported
|
||||
- `remarks([opt])` – not supported
|
||||
- `lines(query, [opt])` – not supported
|
||||
- `serverInfo([opt])` – not supported
|
3
docs/arrivals.md
Normal file
3
docs/arrivals.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# `arrivals(station, [opt])`
|
||||
|
||||
Just like [`departures(station, [opt])`](departures.md), except that it resolves with arrival times instead of departure times.
|
|
@ -45,6 +45,7 @@ Notes:
|
|||
* uses RIS trip IDs, does not expose them directly in the routing-search response
|
||||
* loadFactor for some regional services, not for long distance services
|
||||
* boards up to 12 hours
|
||||
* routing-search returns polylines (!)
|
||||
|
||||
## Vendo/Movas Navigator API
|
||||
https://app.vendo.noncd.db.de/mob/
|
||||
|
@ -57,6 +58,7 @@ EPs:
|
|||
* zuglauf
|
||||
* zuglaeufe/ICE_947/halte/by-abfahrt/8000207_2024 (coach sequence)
|
||||
* angebote/recon (tickets)
|
||||
* trip/recon (polylines)
|
||||
|
||||
Notes:
|
||||
* see [traffic dumps](dumps/)
|
||||
|
@ -66,6 +68,8 @@ Notes:
|
|||
* boards only 1 hour (or unknown param)
|
||||
* does not contain machine-readable cancelled info in the boards (only "Halt entfällt" string), but contains relevant remarks
|
||||
* loadFactor only on journeys (?)
|
||||
* polylines only for zuglauf and trip/recon
|
||||
* limited remarks on boards
|
||||
|
||||
## Vendo/Movas bahn.de API
|
||||
https://int.bahn.de/web/api/
|
||||
|
|
215
docs/departures.md
Normal file
215
docs/departures.md
Normal file
|
@ -0,0 +1,215 @@
|
|||
# `departures(station, [opt])`
|
||||
|
||||
`station` must be in one of these formats:
|
||||
|
||||
```js
|
||||
// a station ID, in a format compatible to the profile you use
|
||||
'900000013102'
|
||||
|
||||
// an FPTF `station` object
|
||||
{
|
||||
type: 'station',
|
||||
id: '900000013102',
|
||||
name: 'foo station',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 1.23,
|
||||
longitude: 3.21
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
With `opt`, you can override the default options, which look like this:
|
||||
|
||||
```js
|
||||
{
|
||||
when: new Date(),
|
||||
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"
|
||||
subStops: true, // not supported
|
||||
entrances: true, // not supported
|
||||
linesOfStops: false, // not supported
|
||||
remarks: true, // parse & expose hints & warnings?
|
||||
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,
|
||||
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 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`/`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 the `db` and `dbnav` profile, only the most important remarks will be contained in the boards.
|
||||
|
||||
```js
|
||||
import {createClient} from 'db-vendo-client'
|
||||
import {profile as dbnavProfile} from 'db-vendo-client/p/dbnav/index.js'
|
||||
|
||||
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
|
||||
const client = createClient(dbnavProfile, userAgent)
|
||||
|
||||
// S Charlottenburg
|
||||
const {
|
||||
departures,
|
||||
realtimeDataUpdatedAt,
|
||||
} = await client.departures('8089165', {duration: 3})
|
||||
```
|
||||
|
||||
`realtimeDataUpdatedAt` is currently not set in db-vendo-client, because the upstream APIs don't provide it.
|
||||
|
||||
`departures` may look like this:
|
||||
|
||||
```js
|
||||
[ {
|
||||
tripId: '1|31431|28|86|17122017',
|
||||
trip: 31431,
|
||||
direction: 'S Spandau',
|
||||
// Depending on the HAFAS endpoint, the destination may be present:
|
||||
destination: {
|
||||
type: 'stop',
|
||||
id: '8089165',
|
||||
name: 'S Spandau',
|
||||
location: {
|
||||
type: 'location',
|
||||
id: '8089165',
|
||||
latitude: 52.534794,
|
||||
longitude: 13.197477
|
||||
},
|
||||
products: {
|
||||
suburban: true,
|
||||
subway: true,
|
||||
tram: false,
|
||||
bus: true,
|
||||
ferry: false,
|
||||
express: true,
|
||||
regional: true,
|
||||
},
|
||||
},
|
||||
line: {
|
||||
type: 'line',
|
||||
id: '18299',
|
||||
fahrtNr: '12345',
|
||||
mode: 'train',
|
||||
product: 'suburban',
|
||||
public: true,
|
||||
name: 'S9',
|
||||
symbol: 'S',
|
||||
nr: 9,
|
||||
metro: false,
|
||||
express: false,
|
||||
night: false,
|
||||
operator: {
|
||||
type: 'operator',
|
||||
id: 's-bahn-berlin-gmbh',
|
||||
name: 'S-Bahn Berlin GmbH'
|
||||
}
|
||||
},
|
||||
currentTripPosition: {
|
||||
type: 'location',
|
||||
latitude: 52.500851,
|
||||
longitude: 13.283755,
|
||||
},
|
||||
|
||||
stop: {
|
||||
type: 'station',
|
||||
id: '900000024101',
|
||||
name: 'S Charlottenburg',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 52.504806,
|
||||
longitude: 13.303846
|
||||
},
|
||||
products: {
|
||||
suburban: true,
|
||||
subway: false,
|
||||
tram: false,
|
||||
bus: true,
|
||||
ferry: false,
|
||||
express: false,
|
||||
regional: true
|
||||
}
|
||||
},
|
||||
|
||||
when: '2017-12-17T19:32:00+01:00',
|
||||
plannedWhen: '2017-12-17T19:32:00+01:00',
|
||||
delay: null,
|
||||
platform: '2',
|
||||
plannedPlatform: '2'
|
||||
}, {
|
||||
cancelled: true,
|
||||
tripId: '1|30977|8|86|17122017',
|
||||
trip: 30977,
|
||||
direction: 'S Westkreuz',
|
||||
line: {
|
||||
type: 'line',
|
||||
id: '16441',
|
||||
fahrtNr: '54321',
|
||||
mode: 'train',
|
||||
product: 'suburban',
|
||||
public: true,
|
||||
name: 'S5',
|
||||
symbol: 'S',
|
||||
nr: 5,
|
||||
metro: false,
|
||||
express: false,
|
||||
night: false,
|
||||
operator: { /* … */ }
|
||||
},
|
||||
currentTripPosition: {
|
||||
type: 'location',
|
||||
latitude: 52.505004,
|
||||
longitude: 13.322391,
|
||||
},
|
||||
|
||||
stop: { /* … */ },
|
||||
|
||||
when: null,
|
||||
plannedWhen: '2017-12-17T19:33:00+01:00'
|
||||
delay: null,
|
||||
platform: null,
|
||||
plannedPlatform: '2',
|
||||
prognosedPlatform: '2'
|
||||
}, {
|
||||
tripId: '1|28671|4|86|17122017',
|
||||
trip: 28671,
|
||||
direction: 'U Rudow',
|
||||
line: {
|
||||
type: 'line',
|
||||
id: '19494',
|
||||
fahrtNr: '11111',
|
||||
mode: 'train',
|
||||
product: 'subway',
|
||||
public: true,
|
||||
name: 'U7',
|
||||
symbol: 'U',
|
||||
nr: 7,
|
||||
metro: false,
|
||||
express: false,
|
||||
night: false,
|
||||
operator: { /* … */ }
|
||||
},
|
||||
currentTripPosition: {
|
||||
type: 'location',
|
||||
latitude: 52.49864,
|
||||
longitude: 13.307622,
|
||||
},
|
||||
|
||||
stop: { /* … */ },
|
||||
|
||||
when: '2017-12-17T19:35:00+01:00',
|
||||
plannedWhen: '2017-12-17T19:35:00+01:00',
|
||||
delay: 0,
|
||||
platform: null,
|
||||
plannedPlatform: null
|
||||
} ]
|
||||
```
|
321
docs/journeys.md
Normal file
321
docs/journeys.md
Normal file
|
@ -0,0 +1,321 @@
|
|||
# `journeys(from, to, [opt])`
|
||||
|
||||
`from` and `to` each must be in one of these formats:
|
||||
|
||||
```js
|
||||
// a station ID, in a format compatible to the profile you use
|
||||
'900000013102'
|
||||
|
||||
// an FPTF `station` object
|
||||
{
|
||||
type: 'station',
|
||||
id: '900000013102',
|
||||
name: 'foo station',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 1.23,
|
||||
longitude: 3.21
|
||||
}
|
||||
}
|
||||
|
||||
// a point of interest, which is an FPTF `location` object
|
||||
{
|
||||
type: 'location',
|
||||
id: '123',
|
||||
poi: true,
|
||||
name: 'foo restaurant',
|
||||
latitude: 1.23,
|
||||
longitude: 3.21
|
||||
}
|
||||
|
||||
// an address, which is an FPTF `location` object
|
||||
{
|
||||
type: 'location',
|
||||
address: 'foo street 1',
|
||||
latitude: 1.23,
|
||||
longitude: 3.21
|
||||
}
|
||||
```
|
||||
|
||||
With `opt`, you can override the default options, which look like this:
|
||||
|
||||
```js
|
||||
{
|
||||
// Use either `departure` or `arrival` to specify a date/time.
|
||||
departure: new Date(),
|
||||
arrival: null,
|
||||
|
||||
earlierThan: null, // ref to get journeys earlier than the last query
|
||||
laterThan: null, // ref to get journeys later than the last query
|
||||
|
||||
results: null, // number of journeys – `null` means "whatever HAFAS returns"
|
||||
via: null, // let journeys pass this station
|
||||
stopovers: false, // return stations on the way?
|
||||
transfers: -1, // Maximum nr of transfers. Default: Let HAFAS decide.
|
||||
transferTime: 0, // minimum time for a single transfer in minutes
|
||||
accessibility: 'none', // not supported
|
||||
bike: false, // only bike-friendly journeys
|
||||
walkingSpeed: 'normal', // not supported
|
||||
// Consider walking to nearby stations at the beginning of a journey?
|
||||
startWithWalking: true, // always true (?)
|
||||
products: {
|
||||
// these entries may vary from profile to profile
|
||||
suburban: true,
|
||||
subway: true,
|
||||
tram: true,
|
||||
bus: true,
|
||||
ferry: true,
|
||||
nationalExpress: true,
|
||||
national: true,
|
||||
regional: true
|
||||
regionalExpress: true // this is actually FlixTrain and co.
|
||||
},
|
||||
tickets: false, // return tickets? only available for [refreshJourney](refresh-journey.md)
|
||||
polylines: false, // return a shape for each leg? only available for [refreshJourney](refresh-journey.md)
|
||||
subStops: true, // not supported
|
||||
entrances: true, // not supported
|
||||
remarks: true, // parse & expose hints & warnings?
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
## 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 returned `departure` and `arrival` times include the current delay. The `departureDelay`/`arrivalDelay` fields express how much they differ from the schedule.
|
||||
|
||||
|
||||
```js
|
||||
import {createClient} 'db-vendo-client'
|
||||
import {profile as dbProfile} from 'db-vendo-client/p/db/index.js'
|
||||
|
||||
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
|
||||
const client = createClient(dbProfile, userAgent)
|
||||
|
||||
// Frankfurt to Stuttgart
|
||||
await client.journeys('8000105', '8000096', {
|
||||
results: 1,
|
||||
stopovers: true
|
||||
})
|
||||
```
|
||||
|
||||
`journeys()` will resolve with an object with the following fields:
|
||||
- `journeys`
|
||||
- `earlierRef`/`laterRef` – pass them as `opt.earlierThan`/`opt.laterThan` into another `journeys()` call to retrieve the next "page" of journeys
|
||||
- `realtimeDataUpdatedAt` – is currently not set in db-vendo-client, because the upstream APIs don't provide it.
|
||||
|
||||
This object might look like this:
|
||||
|
||||
```js
|
||||
{
|
||||
journeys: [ {
|
||||
legs: [ {
|
||||
tripId: '1|32615|6|86|10072018',
|
||||
direction: 'S Ahrensfelde',
|
||||
line: {
|
||||
type: 'line',
|
||||
id: '16845',
|
||||
fahrtNr: '12345',
|
||||
name: 'S7',
|
||||
public: true,
|
||||
mode: 'train',
|
||||
product: 'suburban',
|
||||
operator: {
|
||||
type: 'operator',
|
||||
id: 's-bahn-berlin-gmbh',
|
||||
name: 'S-Bahn Berlin GmbH'
|
||||
},
|
||||
symbol: 'S',
|
||||
nr: 7,
|
||||
metro: false,
|
||||
express: false,
|
||||
night: false
|
||||
},
|
||||
currentLocation: {
|
||||
type: 'location',
|
||||
latitude: 52.51384,
|
||||
longitude: 13.526806,
|
||||
},
|
||||
|
||||
origin: {
|
||||
type: 'station',
|
||||
id: '900000003201',
|
||||
name: 'S+U Berlin Hauptbahnhof',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 52.52585,
|
||||
longitude: 13.368928
|
||||
},
|
||||
products: {
|
||||
suburban: true,
|
||||
subway: true,
|
||||
tram: true,
|
||||
bus: true,
|
||||
ferry: false,
|
||||
express: true,
|
||||
regional: true
|
||||
}
|
||||
},
|
||||
departure: '2018-07-10T23:54:00+02:00',
|
||||
plannedDeparture: '2018-07-10T23:53:00+02:00',
|
||||
departureDelay: 60,
|
||||
departurePlatform: '15',
|
||||
plannedDeparturePlatform: '14',
|
||||
|
||||
destination: {
|
||||
type: 'station',
|
||||
id: '900000100004',
|
||||
name: 'S+U Jannowitzbrücke',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 52.504806,
|
||||
longitude: 13.303846
|
||||
},
|
||||
products: { /* … */ }
|
||||
},
|
||||
arrival: '2018-07-11T00:02:00+02:00',
|
||||
plannedArrival: '2018-07-11T00:01:00+02:00',
|
||||
arrivalDelay: 60,
|
||||
arrivalPlatform: '3',
|
||||
plannedArrivalPlatform: '3',
|
||||
|
||||
stopovers: [ {
|
||||
stop: {
|
||||
type: 'station',
|
||||
id: '900000003201',
|
||||
name: 'S+U Berlin Hauptbahnhof',
|
||||
/* … */
|
||||
},
|
||||
|
||||
arrival: null,
|
||||
plannedArrival: null,
|
||||
arrivalPlatform: null,
|
||||
plannedArrivalPlatform: null,
|
||||
departure: null,
|
||||
plannedDeparture: null,
|
||||
departurePlatform: null,
|
||||
plannedDeparturePlatform: null,
|
||||
|
||||
remarks: [
|
||||
{type: 'hint', code: 'bf', text: 'barrier-free'},
|
||||
{type: 'hint', code: 'FB', text: 'Bicycle conveyance'}
|
||||
]
|
||||
}, {
|
||||
stop: {
|
||||
type: 'station',
|
||||
id: '900000100001',
|
||||
name: 'S+U Friedrichstr.',
|
||||
/* … */
|
||||
},
|
||||
|
||||
cancelled: true,
|
||||
arrival: null,
|
||||
plannedArrival: '2018-07-10T23:55:00+02:00',
|
||||
prognosedArrival: '2018-07-10T23:56:00+02:00',
|
||||
arrivalDelay: 60,
|
||||
arrivalPlatform: null,
|
||||
plannedArrivalPlatform: null,
|
||||
|
||||
departure: null,
|
||||
plannedDeparture: '2018-07-10T23:56:00+02:00',
|
||||
prognosedDeparture: '2018-07-10T23:57:00+02:00',
|
||||
departureDelay: 60,
|
||||
departurePlatform: null,
|
||||
plannedDeparturePlatform: null,
|
||||
|
||||
remarks: [ /* … */ ]
|
||||
},
|
||||
/* … */
|
||||
{
|
||||
stop: {
|
||||
type: 'station',
|
||||
id: '900000100004',
|
||||
name: 'S+U Jannowitzbrücke',
|
||||
/* … */
|
||||
},
|
||||
|
||||
arrival: '2018-07-11T00:02:00+02:00',
|
||||
plannedArrival: '2018-07-11T00:01:00+02:00',
|
||||
arrivalDelay: 60,
|
||||
arrivalPlatform: null,
|
||||
plannedArrivalPlatform: null,
|
||||
|
||||
departure: '2018-07-11T00:02:00+02:00',
|
||||
plannedDeparture: '2018-07-11T00:02:00+02:00',
|
||||
departureDelay: null,
|
||||
departurePlatform: null,
|
||||
plannedDeparturePlatform: null,
|
||||
|
||||
remarks: [ /* … */ ]
|
||||
} ]
|
||||
}, {
|
||||
public: true,
|
||||
walking: true,
|
||||
distance: 558,
|
||||
|
||||
origin: {
|
||||
type: 'station',
|
||||
id: '900000100004',
|
||||
name: 'S+U Jannowitzbrücke',
|
||||
location: { /* … */ },
|
||||
products: { /* … */ }
|
||||
},
|
||||
departure: '2018-07-11T00:01:00+02:00',
|
||||
|
||||
destination: {
|
||||
type: 'station',
|
||||
id: '900000100008',
|
||||
name: 'U Heinrich-Heine-Str.',
|
||||
location: { /* … */ },
|
||||
products: { /* … */ }
|
||||
},
|
||||
arrival: '2018-07-11T00:10:00+02:00'
|
||||
} ]
|
||||
} ],
|
||||
earlierRef: '…', // use with the `earlierThan` option
|
||||
laterRef: '…' // use with the `laterThan` option
|
||||
realtimeDataUpdatedAt: 1531259400, // 2018-07-10T23:50:00+02
|
||||
}
|
||||
```
|
||||
If a journey leg has been cancelled, a `cancelled: true` will be added. Also, `departure`/`departureDelay`/`departurePlatform` and `arrival`/`arrivalDelay`/`arrivalPlatform` will be `null`.
|
||||
|
||||
To get more journeys earlier/later than the current set of results, pass `earlierRef`/`laterRef` into `opt.earlierThan`/`opt.laterThan`. For example, query *later* journeys as follows:
|
||||
|
||||
```js
|
||||
const hbf = '900000003201'
|
||||
const heinrichHeineStr = '900000100008'
|
||||
|
||||
const res1 = await client.journeys(hbf, heinrichHeineStr)
|
||||
const lastJourney = res1.journeys[res1.journeys.length - 1]
|
||||
console.log('departure of last journey', lastJourney.legs[0].departure)
|
||||
|
||||
// get later journeys
|
||||
const res2 = await client.journeys(hbf, heinrichHeineStr, {
|
||||
laterThan: res1.laterRef
|
||||
})
|
||||
const firstLaterJourney = res2.journeys[res2.journeys.length - 1]
|
||||
console.log('departure of first (later) journey', firstLaterJourney.legs[0].departure)
|
||||
```
|
||||
|
||||
```
|
||||
departure of last journey 2017-12-17T19:07:00+01:00
|
||||
departure of first (later) journey 2017-12-17T19:19:00+01:00
|
||||
```
|
||||
|
||||
## Using the `loyaltyCard` option
|
||||
|
||||
```js
|
||||
import {data as loyaltyCards} from 'db-vendo-client/format/loyalty-cards.js' // see there for a list
|
||||
|
||||
hafas.journeys(from, to, {
|
||||
loyaltyCard: {type: data.BAHNCARD, discount: 25}
|
||||
})
|
||||
```
|
||||
|
||||
## The `routingMode` option
|
||||
|
||||
The `routingMode` option is not supported by db-vendo-client. The behavior will be the same as the [`HYBRID` mode of hafas-client](https://github.com/public-transport/hafas-client/blob/main/p/db/readme.md#using-the-routingmode-option), i.e. cancelled trains/infeasible journeys will also be contained for informational purpose.
|
70
docs/locations.md
Normal file
70
docs/locations.md
Normal file
|
@ -0,0 +1,70 @@
|
|||
# `locations(query, [opt])`
|
||||
|
||||
`query` must be an string (e.g. `'Alexanderplatz'`).
|
||||
|
||||
With `opt`, you can override the default options, which look like this:
|
||||
|
||||
```js
|
||||
{
|
||||
fuzzy: true // not supported
|
||||
, results: 5 // how many search results?
|
||||
, stops: true // return stops/stations?
|
||||
, addresses: true
|
||||
, poi: true // points of interest
|
||||
, subStops: true // not supported
|
||||
, entrances: true // not supported
|
||||
, linesOfStops: false // not supported
|
||||
, language: 'en' // language to get results in
|
||||
}
|
||||
```
|
||||
|
||||
## Response
|
||||
|
||||
|
||||
```js
|
||||
import {createClient} from 'db-vendo-client'
|
||||
import {profile as dbnavProfile} from 'db-vendo-client/p/dbnav/index.js'
|
||||
|
||||
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
|
||||
const client = createClient(dbnavProfile, userAgent)
|
||||
|
||||
await client.locations('Alexanderplatz', {results: 3})
|
||||
```
|
||||
|
||||
The result may look like this:
|
||||
|
||||
```js
|
||||
[ {
|
||||
type: 'stop',
|
||||
id: '900000100003',
|
||||
name: 'S+U Alexanderplatz',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 52.521508,
|
||||
longitude: 13.411267
|
||||
},
|
||||
products: {
|
||||
suburban: true,
|
||||
subway: true,
|
||||
tram: true,
|
||||
bus: true,
|
||||
ferry: false,
|
||||
express: false,
|
||||
regional: true
|
||||
}
|
||||
}, { // point of interest
|
||||
type: 'location',
|
||||
id: '900980709',
|
||||
poi: true,
|
||||
name: 'Berlin, Holiday Inn Centre Alexanderplatz****',
|
||||
latitude: 52.523549,
|
||||
longitude: 13.418441
|
||||
}, { // point of interest
|
||||
type: 'location',
|
||||
id: '900980176',
|
||||
poi: true,
|
||||
name: 'Berlin, Hotel Agon am Alexanderplatz',
|
||||
latitude: 52.524556,
|
||||
longitude: 13.420266
|
||||
} ]
|
||||
```
|
83
docs/nearby.md
Normal file
83
docs/nearby.md
Normal file
|
@ -0,0 +1,83 @@
|
|||
# `nearby(location, [opt])`
|
||||
|
||||
This method can be used to find stops/stations & POIs close to a location. Note that it is not supported by every profile/endpoint.
|
||||
|
||||
`location` must be an [*FPTF* `location` object](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md#location-objects).
|
||||
|
||||
With `opt`, you can override the default options, which look like this:
|
||||
|
||||
```js
|
||||
{
|
||||
results: 8, // maximum number of results
|
||||
distance: null, // maximum walking distance in meters
|
||||
poi: false, // not supported
|
||||
stops: true, // return stops/stations?
|
||||
subStops: true, // not supported
|
||||
entrances: true, // not supported
|
||||
linesOfStops: false, // not supported
|
||||
language: 'en' // language to get results in
|
||||
}
|
||||
```
|
||||
|
||||
## Response
|
||||
|
||||
```js
|
||||
import {createClient} from 'db-vendo-client'
|
||||
import {profile as dbProfile} from 'db-vendo-client/p/db/index.js'
|
||||
|
||||
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
|
||||
const client = createClient(dbProfile, userAgent)
|
||||
|
||||
await client.nearby({
|
||||
type: 'location',
|
||||
latitude: 52.5137344,
|
||||
longitude: 13.4744798
|
||||
}, {distance: 400})
|
||||
```
|
||||
|
||||
The result may look like this:
|
||||
|
||||
```js
|
||||
[ {
|
||||
type: 'stop',
|
||||
id: '900000120001',
|
||||
name: 'S+U Frankfurter Allee',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 52.513616,
|
||||
longitude: 13.475298
|
||||
},
|
||||
products: {
|
||||
suburban: true,
|
||||
subway: true,
|
||||
tram: true,
|
||||
bus: true,
|
||||
ferry: false,
|
||||
express: false,
|
||||
regional: false
|
||||
},
|
||||
distance: 56
|
||||
}, {
|
||||
type: 'stop',
|
||||
id: '900000120540',
|
||||
name: 'Scharnweberstr./Weichselstr.',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 52.512339,
|
||||
longitude: 13.470174
|
||||
},
|
||||
products: { /* … */ },
|
||||
distance: 330
|
||||
}, {
|
||||
type: 'stop',
|
||||
id: '900000160544',
|
||||
name: 'Rathaus Lichtenberg',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 52.515908,
|
||||
longitude: 13.479073
|
||||
},
|
||||
products: { /* … */ },
|
||||
distance: 394
|
||||
} ]
|
||||
```
|
2492
docs/openapi.yaml
Normal file
2492
docs/openapi.yaml
Normal file
File diff suppressed because it is too large
Load diff
154
docs/readme.md
Normal file
154
docs/readme.md
Normal file
|
@ -0,0 +1,154 @@
|
|||
# `db-vendo-client` documentation
|
||||
|
||||
**[JS API documentation](api.md)**
|
||||
|
||||
[REST API OpenAPI schema](openapi.yaml) ([open in Swagger Editor](https://editor.swagger.io/?url=https://raw.githubusercontent.com/public-transport/db-vendo-client/refs/heads/main/docs/openapi.yaml))
|
||||
|
||||
## Migrating from an old (v5) `hafas-client` version
|
||||
|
||||
`db-vendo-client` tries to be as compatible as possible with `hafas-client` v6. If you were still on v5 or earlier, see the [`5` → `6` migration guide](https://github.com/public-transport/hafas-client/blob/main/docs/migrating-to-6.md) of `hafas-client`.
|
||||
|
||||
## Throttling requests
|
||||
|
||||
There's opt-in support for throttling requests to the endpoint.
|
||||
|
||||
```js
|
||||
import {createClient} from 'db-vendo-client'
|
||||
import {withThrottling} from 'db-vendo-client/throttle.js'
|
||||
import {profile as dbProfile} from 'db-vendo-client/p/db/index.js'
|
||||
|
||||
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
|
||||
|
||||
// create a throttled HAFAS client with Deutsche Bahn profile
|
||||
const client = createClient(withThrottling(dbProfile), userAgent)
|
||||
|
||||
// Berlin Jungfernheide to München Hbf
|
||||
await client.journeys('8011167', '8000261', {results: 1})
|
||||
```
|
||||
|
||||
You can also pass custom values for the nr of requests (`limit`) per interval into `withThrottling`:
|
||||
|
||||
```js
|
||||
// 2 requests per second
|
||||
const throttledDbProfile = withThrottling(dbProfile, 2, 1000)
|
||||
const client = createClient(throttledDbProfile, userAgent)
|
||||
```
|
||||
|
||||
## Retrying failed requests
|
||||
|
||||
There's opt-in support for retrying failed requests to the endpoint.
|
||||
|
||||
```js
|
||||
import {createClient} from 'db-vendo-client'
|
||||
import {withRetrying} from 'db-vendo-client/retry.js'
|
||||
import {profile as dbProfile} from 'db-vendo-client/p/db/index.js'
|
||||
|
||||
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
|
||||
|
||||
// create a client with Deutsche Bahn profile that will retry on HAFAS errors
|
||||
const client = createClient(withRetrying(dbProfile), userAgent)
|
||||
```
|
||||
|
||||
You can pass custom options into `withRetrying`. They will be passed into [`retry`](https://github.com/tim-kos/node-retry#tutorial).
|
||||
|
||||
```js
|
||||
// retry 2 times, after 10 seconds & 30 seconds
|
||||
const retryingDbProfile = withRetrying(dbProfile, {
|
||||
retries: 2,
|
||||
minTimeout: 10 * 1000,
|
||||
factor: 3
|
||||
})
|
||||
const client = createClient(retryingDbProfile, userAgent)
|
||||
```
|
||||
|
||||
## User agent randomization
|
||||
|
||||
By default, `db-vendo-client` does not randomize the client name that you pass into `createClient`, and sends it as [`User-Agent`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) as it is. At least DB Navigator always sends the same user agent as well (cf. `dbnav` profile). You can turn on randomization by setting `profile.randomizeUserAgent` to `false`:
|
||||
|
||||
```js
|
||||
const client = createClient({
|
||||
...someProfile,
|
||||
randomizeUserAgent: true,
|
||||
}, userAgent)
|
||||
```
|
||||
|
||||
## Logging requests
|
||||
|
||||
You can use `profile.logRequest` and `profile.logResponse` to process the raw [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response), respectively.
|
||||
|
||||
As an example, we can implement a custom logger:
|
||||
|
||||
```js
|
||||
import {createClient} from 'db-vendo-client'
|
||||
import {profile as dbProfile} from 'db-vendo-client/p/db/index.js'
|
||||
|
||||
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
|
||||
|
||||
const logRequest = (ctx, fetchRequest, requestId) => {
|
||||
// ctx looks just like with the other profile.* hooks:
|
||||
const {dbProfile, opt} = ctx
|
||||
|
||||
console.debug(requestId, fetchRequest.headers, fetchRequest.body + '')
|
||||
}
|
||||
|
||||
const logResponse = (ctx, fetchResponse, body, requestId) => {
|
||||
console.debug(requestId, fetchResponse.headers, body + '')
|
||||
}
|
||||
|
||||
// create a client with Deutsche Bahn profile that debug-logs
|
||||
const client = createClient({
|
||||
...dbProfile,
|
||||
logRequest,
|
||||
logResponse,
|
||||
}, userAgent)
|
||||
```
|
||||
|
||||
```js
|
||||
// logRequest output:
|
||||
'29d0e3' {
|
||||
accept: 'application/json',
|
||||
'accept-encoding': 'gzip, br, deflate',
|
||||
'content-type': 'application/json',
|
||||
connection: 'keep-alive',
|
||||
'user-agent': 'hafas842c51-clie842c51nt debug C842c51LI'
|
||||
} {"lang":"de","svcReqL":[{"cfg":{"polyEnc":"GPA"},"meth":"LocMatch",…
|
||||
// logResponse output:
|
||||
'29d0e3' {
|
||||
'content-encoding': 'gzip',
|
||||
'content-length': '1010',
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
date: 'Thu, 06 Oct 2022 12:31:09 GMT',
|
||||
server: 'Apache',
|
||||
vary: 'User-Agent'
|
||||
} {"ver":"1.45","lang":"deu","id":"sb42zgck4mxtxm4s","err":"OK","graph"…
|
||||
```
|
||||
|
||||
The default `profile.logRequest` [`console.error`](https://nodejs.org/docs/latest-v10.x/api/console.html#console_console_error_data_args)s the request body, if you have set `$DEBUG` to `hafas-client`. Likewise, `profile.logResponse` `console.error`s the response body.
|
||||
|
||||
## Error handling
|
||||
|
||||
Unexpected errors – e.g. due to bugs in `db-vendo-client` itself – aside, its methods may reject with the following errors:
|
||||
|
||||
- `Error` – A generic error, e.g. if the DB backend returned a HTTP error.
|
||||
- `HafasError` – A generic error to signal that something HAFAS-related went wrong, either in the client, or in the HAFAS endpoint.
|
||||
|
||||
Each `HafasError` error has the following properties:
|
||||
|
||||
- `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.
|
||||
- `request` – The [Fetch API `Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) of the request.
|
||||
- `url` – The URL of the request.
|
||||
- `response` – The [Fetch API `Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response).
|
||||
|
||||
|
||||
## Using `db-vendo-client` from another language
|
||||
|
||||
If you want to use `db-vendo-client` to access DB APIs but work with non-Node.js environments, you can use it together with [hafas-rest-api](https://github.com/public-transport/hafas-rest-api) to create a REST API (see the [root readme](https://github.com/public-transport/db-vendo-client/tree/main#usage) and the Docker image).
|
||||
Or use [`hafas-client-rpc`](https://github.com/derhuerst/hafas-client-rpc) to create a [JSON-RPC](https://www.jsonrpc.org) interface that you can send commands to.
|
||||
|
||||
|
||||
## General documentation and notes for DB APIs
|
||||
|
||||
[`db-apis.md`](db-apis.md)
|
39
docs/refresh-journey.md
Normal file
39
docs/refresh-journey.md
Normal file
|
@ -0,0 +1,39 @@
|
|||
# `refreshJourney(refreshToken, [opt])`
|
||||
|
||||
`refreshToken` must be a string, taken from `journey.refreshToken`.
|
||||
|
||||
With `opt`, you can override the default options, which look like this:
|
||||
|
||||
```js
|
||||
{
|
||||
stopovers: false, // return stations on the way?
|
||||
polylines: false, // return a shape for each leg? mutually exclusive with tickets
|
||||
tickets: false, // return tickets? mutually exclusive with polylines
|
||||
subStops: true, // not supported
|
||||
entrances: true, // not supported
|
||||
remarks: true, // parse & expose hints & warnings?
|
||||
language: 'en' // language to get results in
|
||||
}
|
||||
```
|
||||
|
||||
## Response
|
||||
|
||||
```js
|
||||
import {createClient} from 'db-vendo-client'
|
||||
import {profile as dbProfile} from 'db-vendo-client/p/db/index.js'
|
||||
|
||||
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
|
||||
const client = createClient(dbProfile, userAgent)
|
||||
|
||||
const {journeys} = await client.journeys('8000105', '8000096', {results: 1})
|
||||
|
||||
// later, fetch up-to-date info on the journey
|
||||
const {
|
||||
journey,
|
||||
realtimeDataUpdatedAt,
|
||||
} = await client.refreshJourney(journeys[0].refreshToken, {stopovers: true, remarks: true})
|
||||
```
|
||||
|
||||
`journey` is a *single* [*Friendly Public Transport Format* v2 draft](https://github.com/public-transport/friendly-public-transport-format/blob/3bd36faa721e85d9f5ca58fb0f38cdbedb87bbca/spec/readme.md) `journey`, in the same format as returned by [`journeys()`](journeys.md).
|
||||
|
||||
`realtimeDataUpdatedAt` is currently not set in db-vendo-client, because the upstream APIs don't provide it.
|
87
docs/stop.md
Normal file
87
docs/stop.md
Normal file
|
@ -0,0 +1,87 @@
|
|||
# `stop(id, [opt])`
|
||||
|
||||
This endpoint is not available with `dbweb` profile.
|
||||
|
||||
`id` must be in one of these formats:
|
||||
|
||||
```js
|
||||
// a stop/station ID, in a format compatible with the profile you use
|
||||
'900000123456'
|
||||
|
||||
// an FPTF `stop`/`station` object
|
||||
{
|
||||
type: 'station',
|
||||
id: '900000123456',
|
||||
name: 'foo station',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 1.23,
|
||||
longitude: 3.21
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
With `opt`, you can override the default options, which look like this:
|
||||
|
||||
```js
|
||||
{
|
||||
subStops: true, // not supported
|
||||
entrances: true, // not supported
|
||||
linesOfStops: false, // parse & expose lines at the stop/station?
|
||||
language: 'en' // language to get results in
|
||||
}
|
||||
```
|
||||
|
||||
## Response
|
||||
|
||||
|
||||
```js
|
||||
import {createClient} from 'hafas-client'
|
||||
import {profile as dbProfile} from 'hafas-client/p/db/index.js'
|
||||
|
||||
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
|
||||
const client = createClient(dbProfile, userAgent)
|
||||
|
||||
await client.stop('900000042101') // U Spichernstr.
|
||||
```
|
||||
|
||||
The result may look like this:
|
||||
|
||||
```js
|
||||
{
|
||||
type: 'stop',
|
||||
id: '900000042101',
|
||||
name: 'U Spichernstr.',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 52.496581,
|
||||
longitude: 13.330616
|
||||
},
|
||||
products: {
|
||||
suburban: false,
|
||||
subway: true,
|
||||
tram: false,
|
||||
bus: true,
|
||||
ferry: false,
|
||||
express: false,
|
||||
regional: false
|
||||
},
|
||||
lines: [ {
|
||||
type: 'line',
|
||||
id: 'u1',
|
||||
mode: 'train',
|
||||
product: 'subway',
|
||||
public: true,
|
||||
name: 'U1',
|
||||
},
|
||||
// …
|
||||
{
|
||||
type: 'line',
|
||||
id: 'n9',
|
||||
mode: 'bus',
|
||||
product: 'bus',
|
||||
public: true,
|
||||
name: 'N9',
|
||||
} ]
|
||||
}
|
||||
```
|
26
docs/tests.md
Normal file
26
docs/tests.md
Normal file
|
@ -0,0 +1,26 @@
|
|||
# automated tests in `db-vendo-client`
|
||||
|
||||
Because transit data is inherently dynamic (e.g. a different set of departures being returned for a stop now than in 10 minutes), and because it is of paramount importance that `db-vendo-client` actually works with HAFAS endpoints *as they currently work*, its testing setup is a bit unusual.
|
||||
|
||||
`db-vendo-client` has three kinds of automated tests:
|
||||
- unit tests, which test individual aspects of the case base in isolation (e.g. the parsing of HAFAS-formatted dates & times) – run via `npm run test-unit`
|
||||
- end-to-end (E2E) tests, which run actual HTTP requests against their respective profile's HAFAS endpoint – run via `npm run test-e2e`
|
||||
- integration tests, which are the E2E tests running against pre-recorded (and checked-in) HTTP request fixtures – run via `npm run test-integration`
|
||||
|
||||
Because the E2E & integration tests are based on the same code, when changing this code, you should also update the integration test fixtures accordingly.
|
||||
|
||||
*Note:* In order to be as reproducible as possible, the tests query transit data for a certain *fixed* point in time on the future, hard-coded in each profile's test suite (a.k.a. each file `test/e2e/*.js`). In combination with the recording & mocking of HTTP requests, this effectively makes the integration tests deterministic.
|
||||
|
||||
## adding integration test fixtures
|
||||
|
||||
As an example, let's assume that we have added an entirely new test to [the *DB* profile's E2E tests](../test/e2e/db.js).
|
||||
|
||||
The behaviour of the HTTP request recording (into fixtures) and mocking (using the recorded fixtures) is controlled via an environment variable `$VCR_MODE`:
|
||||
- By running the test(s) with `VCR_MODE=record`, we can record the HTTP requests being made. The tests will run just like without `$VCR_MODE`, except that they will query data for date+time specified in `T_MOCK` (e.g. [here](https://github.com/public-transport/db-vendo-client/blob/8ff945c07515155380de0acb33584e474d6d547c/test/e2e/db.js#L33)).
|
||||
- Then, by running the test(s) with `VCR_MODE=playback`, because their HTTP requests match the pre-recorded fixtures, they work on the corresponding mocked HTTP responses.
|
||||
|
||||
Usually, you would not want to update all *already existing* recorded HTTP request fixtures of the test suite you have made changes in, as they are unrelated to the test you have added. To only record your *added* test, temporarily change `tap.test(…)` to read `tap.only(…)`, and run with `TAP_ONLY=1 VCR_MODE=record`; This will skip all unrelated tests entirely.
|
||||
|
||||
Then, check the augmented fixtures (in `test/e2e/fixtures`) into Git, and revert the `tap.only(…)` change. To make sure that everything works, run the entire test suite/file (*without `TAP_ONLY=1`*) with `VCR_MODE=playback`.
|
||||
|
||||
*Note:* It might be that the test suite/file you want to augment hasn't been updated in a while, so that `T_MOCK` is in the past. In this case, recording additional fixtures from actual HTTP requests of your added test is usually not possible because the HAFAS API is unable to serve old transit data. In this case, you will first have to change `T_MOCK` to a future date+time (a weekday as "normal" as possible) and re-record all tests' HTTP requests; Don't hesitate to get in touch with me if you have trouble with this.
|
183
docs/trip.md
Normal file
183
docs/trip.md
Normal file
|
@ -0,0 +1,183 @@
|
|||
# `trip(id, [opt])`
|
||||
|
||||
This method can be used to refetch information about a trip – a vehicle stopping at a set of stops at specific times.
|
||||
|
||||
Let's say you used [`journeys`](journeys.md) and now want to get more up-to-date data about the arrival/departure of a leg. You'd pass in the trip ID from `leg.tripId`, e.g. `'1|24983|22|86|18062017'`, and the name of the line from `leg.line.name` like this:
|
||||
|
||||
```js
|
||||
import {createClient} from 'db-vendo-client'
|
||||
import {profile as dbnavProfile} from 'db-vendo-client/p/dbnav/index.js'
|
||||
|
||||
const userAgent = 'link-to-your-project-or-email' // adapt this to your project!
|
||||
const client = createClient(dbnavProfile, userAgent)
|
||||
|
||||
const {journeys} = client.journeys('8000096', '8000105', {results: 1})
|
||||
const leg = journeys[0].legs[0]
|
||||
|
||||
await client.trip(leg.tripId)
|
||||
```
|
||||
|
||||
With `opt`, you can override the default options, which look like this:
|
||||
|
||||
```js
|
||||
{
|
||||
stopovers: true, // return stations on the way?
|
||||
polyline: false, // return a shape for the trip? only supported with HAFAS trip id (i.e. not with a trip id from a departure/arrival board of the `db` profile)
|
||||
subStops: true, // not supported
|
||||
entrances: true, // not supported
|
||||
remarks: true, // parse & expose hints & warnings?
|
||||
language: 'en' // language to get results in
|
||||
}
|
||||
```
|
||||
|
||||
## 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 returned `departure` and `arrival` times include the current delay. The `departureDelay`/`arrivalDelay` fields express how much they differ from the schedule.
|
||||
|
||||
|
||||
```js
|
||||
import {createClient} from 'db-vendo-client'
|
||||
import {profile as dbnavProfile} from 'db-vendo-client/p/dbnav/index.js'
|
||||
|
||||
const client = createClient(dbnavProfile)
|
||||
|
||||
const {
|
||||
trip,
|
||||
realtimeDataUpdatedAt,
|
||||
} = await client.trip('1|31431|28|86|17122017', 'S9', {
|
||||
when: 1513534689273,
|
||||
})
|
||||
```
|
||||
|
||||
`realtimeDataUpdatedAt` is currently not set in db-vendo-client, because the upstream APIs don't provide it.
|
||||
|
||||
When running the code above, `trip` looked like this:
|
||||
|
||||
```js
|
||||
{
|
||||
id: '1|31431|28|86|17122017',
|
||||
direction: 'S Spandau',
|
||||
line: {
|
||||
type: 'line',
|
||||
id: '18299',
|
||||
fahrtNr: '12345',
|
||||
name: 'S9',
|
||||
public: true,
|
||||
mode: 'train',
|
||||
product: 'suburban',
|
||||
symbol: 'S',
|
||||
nr: 9,
|
||||
metro: false,
|
||||
express: false,
|
||||
night: false,
|
||||
operator: {
|
||||
type: 'operator',
|
||||
id: 's-bahn-berlin-gmbh',
|
||||
name: 'S-Bahn Berlin GmbH'
|
||||
}
|
||||
},
|
||||
currentLocation: {
|
||||
type: 'location',
|
||||
latitude: 52.447455,
|
||||
longitude: 13.522464,
|
||||
},
|
||||
|
||||
origin: {
|
||||
type: 'station',
|
||||
id: '900000260005',
|
||||
name: 'S Flughafen Berlin-Schönefeld',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 52.390796,
|
||||
longitude: 13.51352
|
||||
},
|
||||
products: {
|
||||
suburban: true,
|
||||
subway: false,
|
||||
tram: false,
|
||||
bus: true,
|
||||
ferry: false,
|
||||
express: false,
|
||||
regional: true
|
||||
}
|
||||
},
|
||||
departure: '2017-12-17T18:37:00+01:00',
|
||||
plannedDeparture: '2017-12-17T18:37:00+01:00',
|
||||
departureDelay: null,
|
||||
departurePlatform: '13',
|
||||
plannedDeparturePlatform: '13',
|
||||
|
||||
destination: {
|
||||
type: 'station',
|
||||
id: '900000029101',
|
||||
name: 'S Spandau',
|
||||
location: {
|
||||
type: 'location',
|
||||
latitude: 52.534794,
|
||||
longitude: 13.197477
|
||||
},
|
||||
products: {
|
||||
suburban: true,
|
||||
subway: false,
|
||||
tram: false,
|
||||
bus: true,
|
||||
ferry: false,
|
||||
express: true,
|
||||
regional: true
|
||||
}
|
||||
},
|
||||
arrival: '2017-12-17T19:50:30+01:00',
|
||||
plannedArrival: '2017-12-17T19:49:00+01:00',
|
||||
arrivalDelay: 90,
|
||||
arrivalPlatform: '3a',
|
||||
plannedArrivalPlatform: '2',
|
||||
|
||||
stopovers: [ /* … */ ]
|
||||
}
|
||||
```
|
||||
|
||||
### `polyline` option
|
||||
|
||||
Only supported with HAFAS trip id (i.e. not with a trip id from a departure/arrival board of the `db` profile).
|
||||
|
||||
If you pass `polyline: true`, the trip will have a `polyline` field, containing a [GeoJSON](http://geojson.org) [`FeatureCollection`](https://tools.ietf.org/html/rfc7946#section-3.3) of [`Point`s](https://tools.ietf.org/html/rfc7946#appendix-A.1).
|
||||
|
||||
```js
|
||||
{
|
||||
type: 'FeatureCollection',
|
||||
features: [
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [13.3875, 52.43993] // longitude, latitude
|
||||
}
|
||||
},
|
||||
/* … */
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [13.38892, 52.49448] // longitude, latitude
|
||||
}
|
||||
},
|
||||
/* … */
|
||||
{
|
||||
// intermediate point, without associated station
|
||||
type: 'Feature',
|
||||
properties: {},
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [13.28599, 52.58742] // longitude, latitude
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [13.28406, 52.58915] // longitude, latitude
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
62
eslint.config.js
Normal file
62
eslint.config.js
Normal 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;
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
88
index.js
88
index.js
|
@ -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) => {
|
||||
items[station.id] = station;
|
||||
})
|
||||
.once('end', () => {
|
||||
if (profile.DEBUG) {
|
||||
console.log('Loaded station index.');
|
||||
}
|
||||
resolve(items);
|
||||
})
|
||||
.once('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
for await (const station of dbHafasStations.readFullStations()) {
|
||||
items[station.id] = station;
|
||||
items[station.name] = station;
|
||||
}
|
||||
if (profile.DEBUG) {
|
||||
console.log('Loaded station index.');
|
||||
}
|
||||
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,7 +78,7 @@ 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) {
|
||||
|
@ -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
109
lib/api-parsers.js
Normal 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,
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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, {
|
||||
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 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
|
||||
});
|
||||
const pool = roundRobin(agents);
|
||||
getAgent = () => pool.get();
|
||||
}
|
||||
getAgent = () => agent;
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +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';
|
||||
import {formatLocationFilter} from './location-filter.js';
|
||||
import {formatLocationsReq} from './locations-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';
|
||||
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,
|
||||
formatLocationsReq,
|
||||
formatLocationFilter,
|
||||
refreshJourneysEndpointTickets, refreshJourneysEndpointPolyline,
|
||||
|
||||
formatLocationsReq, formatLocationFilter,
|
||||
locationsEndpoint,
|
||||
|
||||
formatStopReq, parseStop,
|
||||
stopEndpoint,
|
||||
|
||||
formatNearbyReq,
|
||||
nearbyEndpoint,
|
||||
|
||||
formatTripReq,
|
||||
tripEndpoint_dbnav, tripEndpoint_dbregioguide,
|
||||
|
||||
formatStationBoardReq,
|
||||
boardEndpoint,
|
||||
};
|
||||
|
||||
|
||||
export {
|
||||
profile,
|
||||
};
|
||||
|
|
18
p/db/trip-req.js
Normal file
18
p/db/trip-req.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
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('#')) {
|
||||
_profile['tripEndpoint'] = profile.tripEndpoint_dbnav;
|
||||
return hafasFormatTripReq({profile: _profile, opt}, id);
|
||||
}
|
||||
|
||||
_profile['tripEndpoint'] = profile.tripEndpoint_dbregioguide;
|
||||
return risTripReq({profile: _profile, opt}, id);
|
||||
};
|
||||
|
||||
export {
|
||||
formatTripReq,
|
||||
};
|
4
p/dbbahnhof/base.json
Normal file
4
p/dbbahnhof/base.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"boardEndpoint": "https://www.bahnhof.de/api/boards/",
|
||||
"defaultLanguage": "en"
|
||||
}
|
31
p/dbbahnhof/index.js
Normal file
31
p/dbbahnhof/index.js
Normal 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,
|
||||
};
|
30
p/dbbahnhof/station-board-req.js
Normal file
30
p/dbbahnhof/station-board-req.js
Normal 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,
|
||||
};
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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
34
p/dbnav/parse-journey.js
Normal 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
5
p/dbregioguide/base.json
Normal 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
32
p/dbregioguide/index.js
Normal 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,
|
||||
};
|
24
p/dbregioguide/station-board-req.js
Normal file
24
p/dbregioguide/station-board-req.js
Normal 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,
|
||||
};
|
11
p/dbregioguide/trip-req.js
Normal file
11
p/dbregioguide/trip-req.js
Normal 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
4
p/dbris/base.json
Normal 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
31
p/dbris/index.js
Normal 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,
|
||||
};
|
34
p/dbris/station-board-req.js
Normal file
34
p/dbris/station-board-req.js
Normal 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,
|
||||
};
|
|
@ -1,9 +1,11 @@
|
|||
{
|
||||
"journeysEndpoint": "https://int.bahn.de/web/api/angebote/fahrplan",
|
||||
"refreshJourneysEndpoint": "https://int.bahn.de/web/api/angebote/recon",
|
||||
"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",
|
||||
"boardEndpoint": "https://regio-guide.de/@prd/zupo-travel-information/api/public/ri/board/",
|
||||
"boardEndpoint": "https://int.bahn.de/web/api/reiseloesung/",
|
||||
"defaultLanguage": "en"
|
||||
}
|
|
@ -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
30
p/dbweb/index.js
Normal 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,
|
||||
};
|
|
@ -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,28 +35,39 @@ 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} = ctx;
|
||||
let query = {
|
||||
ctxRecon: refreshToken,
|
||||
deutschlandTicketVorhanden: false,
|
||||
nurDeutschlandTicketVerbindungen: false,
|
||||
reservierungsKontingenteVorhanden: false,
|
||||
};
|
||||
query = Object.assign(query, profile.formatTravellers(ctx));
|
||||
return {
|
||||
endpoint: profile.refreshJourneysEndpoint,
|
||||
body: query,
|
||||
method: 'post',
|
||||
};
|
||||
const {profile, opt} = ctx;
|
||||
if (opt.tickets) {
|
||||
let query = {
|
||||
ctxRecon: refreshToken,
|
||||
deutschlandTicketVorhanden: false,
|
||||
nurDeutschlandTicketVerbindungen: false,
|
||||
reservierungsKontingenteVorhanden: false,
|
||||
};
|
||||
query = Object.assign(query, profile.formatTravellers(ctx));
|
||||
return {
|
||||
endpoint: profile.refreshJourneysEndpointTickets,
|
||||
body: query,
|
||||
method: 'post',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
endpoint: profile.refreshJourneysEndpointPolyline,
|
||||
body: {
|
||||
ctxRecon: refreshToken,
|
||||
poly: true,
|
||||
},
|
||||
method: 'post',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
20
p/dbweb/station-board-req.js
Normal file
20
p/dbweb/station-board-req.js
Normal 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
3933
package-lock.json
generated
File diff suppressed because it is too large
Load diff
54
package.json
54
package.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "db-vendo-client",
|
||||
"description": "Client for bahn.de public transport APIs.",
|
||||
"version": "6.3.2",
|
||||
"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"
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -13,13 +13,19 @@ const locationFallback = (id, name, fallbackLocations) => {
|
|||
const parseJourneyLeg = (ctx, pt, date, fallbackLocations) => { // pt = raw leg
|
||||
const {profile, opt} = ctx;
|
||||
|
||||
const stops = pt.halte?.length && pt.halte || pt.stops?.length && pt.stops || [];
|
||||
const res = {
|
||||
origin: pt.halte?.length > 0 && profile.parseLocation(ctx, pt.halte[0].ort || pt.halte[0]) || pt.abgangsOrt?.name && profile.parseLocation(ctx, pt.abgangsOrt) || locationFallback(pt.abfahrtsOrtExtId, pt.abfahrtsOrt, fallbackLocations),
|
||||
destination: pt.halte?.length > 0 && profile.parseLocation(ctx, pt.halte[pt.halte.length - 1].ort || pt.halte[pt.halte.length - 1]) || pt.ankunftsOrt?.name && profile.parseLocation(ctx, pt.ankunftsOrt) || locationFallback(pt.ankunftsOrtExtId, pt.ankunftsOrt, fallbackLocations),
|
||||
origin: stops.length && profile.parseLocation(ctx, stops[0].ort || stops[0].station || stops[0])
|
||||
|| pt.abgangsOrt?.name && profile.parseLocation(ctx, pt.abgangsOrt)
|
||||
|| locationFallback(pt.abfahrtsOrtExtId, pt.abfahrtsOrt, fallbackLocations),
|
||||
destination: stops.length && profile.parseLocation(ctx, stops[stops.length - 1].ort || stops[stops.length - 1].station || stops[stops.length - 1])
|
||||
|| pt.ankunftsOrt?.name && profile.parseLocation(ctx, pt.ankunftsOrt)
|
||||
|| locationFallback(pt.ankunftsOrtExtId, pt.ankunftsOrt, fallbackLocations),
|
||||
};
|
||||
|
||||
const cancelledDep = pt.halte?.length > 0 && profile.parseCancelled(pt.halte[0]);
|
||||
const dep = profile.parseWhen(ctx, date, pt.abfahrtsZeitpunkt || pt.abgangsDatum || pt.halte?.length > 0 && pt.halte[0].abgangsDatum, pt.ezAbfahrtsZeitpunkt || pt.ezAbgangsDatum || pt.halte?.length > 0 && pt.halte[0].ezAbgangsDatum, cancelledDep);
|
||||
const cancelledDep = stops.length && profile.parseCancelled(stops[0]);
|
||||
const dep = profile.parseWhen(ctx, date, pt.abfahrtsZeitpunkt || pt.abgangsDatum || stops.length && (stops[0].abgangsDatum || stops[0].departureTime?.target), pt.ezAbfahrtsZeitpunkt || pt.ezAbgangsDatum || stops.length && (stops[0].ezAbgangsDatum || stops[0].departureTime?.timeType != 'SCHEDULE' && stops[0].departureTime?.predicted), cancelledDep,
|
||||
);
|
||||
res.departure = dep.when;
|
||||
res.plannedDeparture = dep.plannedWhen;
|
||||
res.departureDelay = dep.delay;
|
||||
|
@ -27,8 +33,9 @@ const parseJourneyLeg = (ctx, pt, date, fallbackLocations) => { // pt = raw leg
|
|||
res.prognosedDeparture = dep.prognosedWhen;
|
||||
}
|
||||
|
||||
const cancelledArr = pt.halte?.length > 0 && profile.parseCancelled(pt.halte[pt.halte.length - 1]);
|
||||
const arr = profile.parseWhen(ctx, date, pt.ankunftsZeitpunkt || pt.ankunftsDatum || pt.halte?.length > 0 && pt.halte[pt.halte.length - 1].ankunftsDatum, pt.ezAnkunftsZeitpunkt || pt.ezAnkunftsDatum || pt.halte?.length > 0 && pt.halte[pt.halte.length - 1].ezAkunftsDatum, cancelledArr);
|
||||
const cancelledArr = stops.length && profile.parseCancelled(stops[stops.length - 1]);
|
||||
const arr = profile.parseWhen(ctx, date, pt.ankunftsZeitpunkt || pt.ankunftsDatum || stops.length && (stops[stops.length - 1].ankunftsDatum || stops[stops.length - 1].arrivalTime?.target), pt.ezAnkunftsZeitpunkt || pt.ezAnkunftsDatum || stops.length && (stops[stops.length - 1].ezAnkunftsDatum || stops[stops.length - 1].arrivalTime?.timeType != 'SCHEDULE' && stops[stops.length - 1].arrivalTime?.predicted), cancelledArr,
|
||||
);
|
||||
res.arrival = arr.when;
|
||||
res.plannedArrival = arr.plannedWhen;
|
||||
res.arrivalDelay = arr.delay;
|
||||
|
@ -67,8 +74,14 @@ const parseJourneyLeg = (ctx, pt, date, fallbackLocations) => { // pt = raw leg
|
|||
res.direction = pt.verkehrsmittel?.richtung || pt.richtung || null;
|
||||
|
||||
// TODO res.currentLocation
|
||||
if (pt.halte?.length > 0) {
|
||||
const arrPl = profile.parsePlatform(ctx, pt.halte[pt.halte.length - 1].gleis, pt.halte[pt.halte.length - 1].ezGleis, cancelledArr);
|
||||
// TODO trainStartDate?
|
||||
|
||||
if (stops.length) {
|
||||
const arrPl = profile.parsePlatform(ctx,
|
||||
stops[stops.length - 1].gleis || stops[stops.length - 1].track?.target,
|
||||
stops[stops.length - 1].ezGleis || stops[stops.length - 1].track?.prediction,
|
||||
cancelledArr,
|
||||
);
|
||||
res.arrivalPlatform = arrPl.platform;
|
||||
res.plannedArrivalPlatform = arrPl.plannedPlatform;
|
||||
if (arrPl.prognosedPlatform) {
|
||||
|
@ -76,7 +89,11 @@ const parseJourneyLeg = (ctx, pt, date, fallbackLocations) => { // pt = raw leg
|
|||
}
|
||||
// res.arrivalPrognosisType = null; // TODO
|
||||
|
||||
const depPl = profile.parsePlatform(ctx, pt.halte[0].gleis, pt.halte[0].ezGleis, cancelledDep);
|
||||
const depPl = profile.parsePlatform(ctx,
|
||||
stops[0].gleis || stops[0].track?.target,
|
||||
stops[0].ezGleis || stops[0].track?.prediction,
|
||||
cancelledDep,
|
||||
);
|
||||
res.departurePlatform = depPl.platform;
|
||||
res.plannedDeparturePlatform = depPl.plannedPlatform;
|
||||
if (depPl.prognosedPlatform) {
|
||||
|
@ -86,7 +103,7 @@ const parseJourneyLeg = (ctx, pt, date, fallbackLocations) => { // pt = raw leg
|
|||
|
||||
|
||||
if (opt.stopovers) {
|
||||
res.stopovers = pt.halte.map(s => profile.parseStopover(ctx, s, date));
|
||||
res.stopovers = stops.map(s => profile.parseStopover(ctx, s, date));
|
||||
// filter stations the train passes without stopping, as this doesn't comply with fptf (yet)
|
||||
res.stopovers = res.stopovers.filter((x) => !x.passBy);
|
||||
}
|
||||
|
|
|
@ -29,6 +29,17 @@ const parseLocationsFromCtxRecon = (ctx, j) => {
|
|||
}, {});
|
||||
};
|
||||
|
||||
const trimJourneyId = (journeyId) => {
|
||||
if (!journeyId) {
|
||||
return null;
|
||||
}
|
||||
const endOfHafasId = journeyId.lastIndexOf('$');
|
||||
if (endOfHafasId != -1) {
|
||||
return journeyId.substring(0, endOfHafasId + 1);
|
||||
}
|
||||
return journeyId;
|
||||
};
|
||||
|
||||
const parseJourney = (ctx, jj) => { // j = raw journey
|
||||
const {profile, opt} = ctx;
|
||||
const j = jj.verbindung || jj;
|
||||
|
@ -46,7 +57,7 @@ const parseJourney = (ctx, jj) => { // j = raw journey
|
|||
const res = {
|
||||
type: 'journey',
|
||||
legs,
|
||||
refreshToken: j.ctxRecon || j.kontext || null,
|
||||
refreshToken: trimJourneyId(j.ctxRecon || j.kontext),
|
||||
};
|
||||
|
||||
// TODO freq
|
||||
|
@ -58,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);
|
||||
|
|
|
@ -2,25 +2,25 @@ import slugg from 'slugg';
|
|||
|
||||
const parseLine = (ctx, p) => {
|
||||
const profile = ctx.profile;
|
||||
const fahrtNr = p.verkehrsmittel?.nummer || p.transport?.number || p.train?.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.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.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.kurztext;
|
||||
const foundProduct = profile.products.find(pp => pp.vendo == p.verkehrsmittel?.produktGattung || pp.ris == p.transport?.type || pp.ris == p.train?.type || pp.ris_alt == p.train?.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;
|
||||
|
||||
res.operator = profile.parseOperator(ctx, p.verkehrsmittel?.zugattribute || p.zugattribute || p.attributNotizen || p.administration);
|
||||
res.operator = profile.parseOperator(ctx, p.verkehrsmittel?.zugattribute || p.zugattribute || p.attributNotizen || p.administration || p);
|
||||
return res;
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import slugg from 'slugg';
|
||||
|
||||
const parseOperator = (ctx, zugattrib) => {
|
||||
if (!zugattrib) {
|
||||
return null;
|
||||
}
|
||||
if (zugattrib.operatorName) {
|
||||
if (zugattrib?.operatorName) {
|
||||
return {
|
||||
type: 'operator',
|
||||
id: zugattrib.operatorCode,
|
||||
name: zugattrib.operatorName,
|
||||
};
|
||||
}
|
||||
if (!zugattrib || !Array.isArray(zugattrib)) {
|
||||
return null;
|
||||
}
|
||||
const bef = zugattrib.find(z => z.key == 'BEF' || z.key == 'OP');
|
||||
if (!bef) {
|
||||
return null;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 => {
|
||||
|
@ -10,13 +8,20 @@ const parseRemarks = (ctx, ref) => {
|
|||
}) || [],
|
||||
ref.himMeldungen || [],
|
||||
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);
|
||||
|
@ -28,18 +33,18 @@ 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,
|
||||
summary: remark.nachrichtKurz || remark.value || remark.ueberschrift || remark.text
|
||||
|| Object.values(remark.descriptions || {})
|
||||
.shift()?.textShort,
|
||||
text: remark.nachrichtLang || remark.value || remark.text
|
||||
|| Object.values(remark.descriptions || {})
|
||||
.shift()?.text,
|
||||
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,
|
||||
text: remark.nachrichtLang || remark.value || remark.text || remark.caption
|
||||
|| Object.values(remark.descriptions || {})
|
||||
.shift()?.text,
|
||||
type: type,
|
||||
};
|
||||
if (remark.modDateTime || remark.letzteAktualisierung) {
|
||||
|
@ -201,11 +206,16 @@ const parseRemarks = (ctx, ref) => {
|
|||
*/
|
||||
|
||||
const parseCancelled = (ref) => {
|
||||
return ref.canceled || ref.cancelled || (ref.risNotizen || ref.echtzeitNotizen) && Boolean((ref.risNotizen || ref.echtzeitNotizen).find(r => r.key == 'text.realtime.stop.cancelled'
|
||||
|| r.type == 'HALT_AUSFALL'
|
||||
|| r.text == 'Halt entfällt'
|
||||
|| r.text == '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 {
|
||||
|
|
|
@ -2,13 +2,15 @@ const parseStopover = (ctx, st, date) => { // st = raw stopover
|
|||
const {profile, opt} = ctx;
|
||||
|
||||
const cancelled = profile.parseCancelled(st);
|
||||
const arr = profile.parseWhen(ctx, date, st.ankunftsZeitpunkt || st.ankunftsDatum, st.ezAnkunftsZeitpunkt || st.ezAnkunftsDatum, cancelled);
|
||||
const arrPl = profile.parsePlatform(ctx, st.gleis, st.ezGleis);
|
||||
const dep = profile.parseWhen(ctx, date, st.abfahrtsZeitpunkt || st.abgangsDatum, st.ezAbfahrtsZeitpunkt || st.ezAbgangsDatum, cancelled);
|
||||
const depPl = profile.parsePlatform(ctx, st.gleis, st.ezGleis);
|
||||
const arr = profile.parseWhen(ctx, date, st.ankunftsZeitpunkt || st.ankunftsDatum || st.arrivalTime?.target, st.ezAnkunftsZeitpunkt || st.ezAnkunftsDatum || st.arrivalTime?.timeType != 'SCHEDULE' && st.arrivalTime?.predicted, cancelled,
|
||||
);
|
||||
const arrPl = profile.parsePlatform(ctx, st.gleis || st.track?.target, st.ezGleis || st.track?.prediction);
|
||||
const dep = profile.parseWhen(ctx, date, st.abfahrtsZeitpunkt || st.abgangsDatum || st.departureTime?.target, st.ezAbfahrtsZeitpunkt || st.ezAbgangsDatum || st.departureTime?.timeType != 'SCHEDULE' && st.departureTime?.predicted, cancelled,
|
||||
);
|
||||
const depPl = arrPl;
|
||||
|
||||
const res = {
|
||||
stop: profile.parseLocation(ctx, st.ort || st) || null,
|
||||
stop: profile.parseLocation(ctx, st.ort || st.station || st) || null,
|
||||
arrival: arr.when,
|
||||
plannedArrival: arr.plannedWhen,
|
||||
arrivalDelay: arr.delay,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -3,10 +3,10 @@ const parseTrip = (ctx, t, id) => { // t = raw trip
|
|||
|
||||
// pretend the trip is a leg in a journey
|
||||
const trip = profile.parseJourneyLeg(ctx, t);
|
||||
trip.id = trip.tripId || id; // TODO journeyId
|
||||
trip.id = trip.tripId || id;
|
||||
delete trip.tripId;
|
||||
delete trip.reachable;
|
||||
trip.cancelled = profile.parseCancelled(t);
|
||||
trip.cancelled = Boolean(profile.parseCancelled(t));
|
||||
|
||||
// TODO opt.scheduledDays
|
||||
return trip;
|
||||
|
|
66
readme.md
66
readme.md
|
@ -1,51 +1,57 @@
|
|||
# db-vendo-client
|
||||
|
||||
**A client for the new "vendo"/"movas" bahn.de APIs, a drop-in replacement for [hafas-client](https://github.com/public-transport/hafas-client/).**
|
||||
**A client for the new "vendo"/"movas" Deutsche Bahn APIs, a drop-in replacement for [hafas-client](https://github.com/public-transport/hafas-client/).**
|
||||
|
||||
[](https://www.npmjs.com/package/db-vendo-client)
|
||||

|
||||
[](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
|
||||
* `locations()`, `nearby()`
|
||||
* `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 | ✅ (still no `remarks()` endpoint) |
|
||||
| cancelled trips | not contained in boards | contained with cancelled flag |
|
||||
| tickets | only for `refreshJourney()` | only for `refreshJourney()`, mutually exclusive with polylines |
|
||||
| polylines | only for `trip()` | 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 :)
|
||||
|
||||
Also consult the relevant **[documentation](https://github.com/public-transport/hafas-client/blob/main/docs/readme.md)** of [hafas-client](https://github.com/public-transport/hafas-client/) (but beware of the limited functionality of db-vendo-client).
|
||||
|
||||
Also consult the **[documentation](docs/readme.md)**.
|
||||
|
||||
## Background
|
||||
|
||||
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 to become less and less reliable (server unreachable, missing prices, etc.) This project aims to enable easy switching to the new APIs. However, not all information will be available from the new APIs.
|
||||
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.
|
||||
|
||||
|
@ -67,8 +73,20 @@ docker run \
|
|||
ghcr.io/public-transport/db-vendo-client
|
||||
```
|
||||
|
||||
You may want to generate a client for your programming language for this REST API using the [OpenAPI schema](docs/openapi.yaml) ([open in Swagger Editor](https://editor.swagger.io/?url=https://raw.githubusercontent.com/public-transport/db-vendo-client/refs/heads/main/docs/openapi.yaml)). Note
|
||||
that this is to be seen more as a starting point for implementation, e.g. some profile-specific details like tickets are missing from this API definition.
|
||||
|
||||
There are [community-maintained TypeScript typings available as `@types/hafas-client`](https://www.npmjs.com/package/@types/hafas-client).
|
||||
|
||||
> [!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
|
||||
|
|
|
@ -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();
|
||||
});
|
29
test/dbbahnhof-departures.js
Normal file
29
test/dbbahnhof-departures.js
Normal 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();
|
||||
});
|
|
@ -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});
|
||||
|
|
|
@ -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});
|
||||
|
|
|
@ -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});
|
||||
|
|
|
@ -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});
|
||||
|
|
23
test/dbregioguide-trip.js
Normal file
23
test/dbregioguide-trip.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import tap from 'tap';
|
||||
|
||||
import {createClient} from '../index.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;
|
||||
|
||||
const opt = {
|
||||
stopovers: true,
|
||||
remarks: true,
|
||||
products: {},
|
||||
};
|
||||
|
||||
tap.test('parses a regio guide trip correctly (DB)', (t) => {
|
||||
const ctx = {profile, opt, common: null, res};
|
||||
const trip = profile.parseTrip(ctx, res, 'foo');
|
||||
|
||||
t.same(trip, expected.trip);
|
||||
t.end();
|
||||
});
|
|
@ -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
28
test/dbris-departures.js
Normal 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
87
test/dbweb-departures.js
Normal 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();
|
||||
});
|
|
@ -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]);
|
||||
|
|
@ -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]);
|
||||
|
|
@ -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;
|
239
test/e2e/db.js
239
test/e2e/db.js
|
@ -99,138 +99,139 @@ 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,
|
||||
});
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
if (journey.tickets) {
|
||||
assertValidTickets(t, journey.tickets);
|
||||
t.end();
|
||||
});
|
||||
|
||||
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, {
|
||||
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();
|
||||
});
|
||||
|
||||
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,
|
||||
t.end();
|
||||
});
|
||||
|
||||
await testJourneysStationToAddress({
|
||||
test: t,
|
||||
res,
|
||||
validate,
|
||||
fromId: blnSchwedterStr,
|
||||
to: torfstr,
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
// todo: journeys, only one product
|
||||
|
||||
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,
|
||||
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();
|
||||
});
|
||||
|
||||
await testJourneysStationToPoi({
|
||||
test: t,
|
||||
res,
|
||||
validate,
|
||||
fromId: blnSchwedterStr,
|
||||
to: atze,
|
||||
});
|
||||
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,
|
||||
});
|
||||
|
||||
tap.test('journeys: via works – with detour', async (t) => {
|
||||
// Going from Westhafen to Wedding via Württembergalle without detour
|
||||
// is currently impossible. We check if the routing engine computes a detour.
|
||||
const res = await client.journeys(westhafen, wedding, {
|
||||
via: württembergallee,
|
||||
results: 1,
|
||||
departure: when,
|
||||
stopovers: true,
|
||||
await testJourneysStationToAddress({
|
||||
test: t,
|
||||
res,
|
||||
validate,
|
||||
fromId: blnSchwedterStr,
|
||||
to: torfstr,
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
await testJourneysWithDetour({
|
||||
test: t,
|
||||
res,
|
||||
validate,
|
||||
detourIds: [württembergallee],
|
||||
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();
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
// todo: walkingSpeed "Berlin - Charlottenburg, Hallerstraße" -> jungfernheide
|
||||
// todo: 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,
|
||||
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) => {
|
||||
|
@ -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
146
test/e2e/dbbahnhof.js
Normal 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();
|
||||
});
|
||||
|
|
@ -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,131 +151,133 @@ tap.test('refreshJourney – valid tickets', async (t) => {
|
|||
|
||||
// todo: journeys, only one product
|
||||
|
||||
tap.test('journeys – fails with no product', async (t) => {
|
||||
await journeysFailsWithNoProduct({
|
||||
test: t,
|
||||
fetchJourneys: client.journeys,
|
||||
fromId: blnSchwedterStr,
|
||||
toId: münchenHbf,
|
||||
when,
|
||||
products: dbProfile.products,
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('Berlin Schwedter Str. to Torfstraße 17', async (t) => {
|
||||
const torfstr = {
|
||||
type: 'location',
|
||||
address: 'Torfstraße 17',
|
||||
latitude: 52.5416823,
|
||||
longitude: 13.3491223,
|
||||
};
|
||||
const res = await client.journeys(blnSchwedterStr, torfstr, {
|
||||
results: 3,
|
||||
departure: when,
|
||||
});
|
||||
|
||||
await testJourneysStationToAddress({
|
||||
test: t,
|
||||
res,
|
||||
validate,
|
||||
fromId: blnSchwedterStr,
|
||||
to: torfstr,
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('Berlin Schwedter Str. to ATZE Musiktheater', async (t) => {
|
||||
const atze = {
|
||||
type: 'location',
|
||||
id: '991598902',
|
||||
poi: true,
|
||||
name: 'Berlin, Atze Musiktheater für Kinder (Kultur und U',
|
||||
latitude: 52.542417,
|
||||
longitude: 13.350437,
|
||||
};
|
||||
const res = await client.journeys(blnSchwedterStr, atze, {
|
||||
results: 3,
|
||||
departure: when,
|
||||
});
|
||||
|
||||
await testJourneysStationToPoi({
|
||||
test: t,
|
||||
res,
|
||||
validate,
|
||||
fromId: blnSchwedterStr,
|
||||
to: atze,
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('journeys: via works – with detour', async (t) => {
|
||||
// Going from Westhafen to Wedding via Württembergalle without detour
|
||||
// is currently impossible. We check if the routing engine computes a detour.
|
||||
const res = await client.journeys(westhafen, wedding, {
|
||||
via: württembergallee,
|
||||
results: 1,
|
||||
departure: when,
|
||||
stopovers: true,
|
||||
});
|
||||
|
||||
await testJourneysWithDetour({
|
||||
test: t,
|
||||
res,
|
||||
validate,
|
||||
detourIds: [württembergallee],
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
// todo: walkingSpeed "Berlin - Charlottenburg, Hallerstraße" -> jungfernheide
|
||||
// todo: without detour
|
||||
|
||||
|
||||
// todo: with the DB endpoint, earlierRef/laterRef is missing queries many days in the future
|
||||
tap.skip('earlier/later journeys, Jungfernheide -> München Hbf', async (t) => {
|
||||
await testEarlierLaterJourneys({
|
||||
test: t,
|
||||
fetchJourneys: client.journeys,
|
||||
validate,
|
||||
fromId: jungfernheide,
|
||||
toId: münchenHbf,
|
||||
when,
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
if (!process.env.VCR_MODE) {
|
||||
tap.test('journeys – leg cycle & alternatives', async (t) => {
|
||||
await testLegCycleAlternatives({
|
||||
if (!process.env.VCR_OFF) {
|
||||
tap.test('journeys – fails with no product', async (t) => {
|
||||
await journeysFailsWithNoProduct({
|
||||
test: t,
|
||||
fetchJourneys: client.journeys,
|
||||
fromId: blnTiergarten,
|
||||
toId: blnJannowitzbrücke,
|
||||
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 = 1763542800 * 1000;
|
||||
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.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';
|
||||
|
|
499
test/e2e/dbregioguide.js
Normal file
499
test/e2e/dbregioguide.js
Normal 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
501
test/e2e/dbweb.js
Normal 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
|
@ -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
492
test/fixtures/dbbahnhof-departures.js
vendored
Normal 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,
|
||||
};
|
1
test/fixtures/dbbahnhof-departures.json
vendored
Normal file
1
test/fixtures/dbbahnhof-departures.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
test/fixtures/dbnav-refresh-journey.js
vendored
1
test/fixtures/dbnav-refresh-journey.js
vendored
|
@ -212,6 +212,7 @@ const dbNavJourney = {
|
|||
amount: 43.99,
|
||||
currency: 'EUR',
|
||||
hint: null,
|
||||
partialFare: false,
|
||||
},
|
||||
tickets: [
|
||||
{
|
||||
|
|
376
test/fixtures/dbregioguide-trip.js
vendored
Normal file
376
test/fixtures/dbregioguide-trip.js
vendored
Normal file
|
@ -0,0 +1,376 @@
|
|||
const dbTrip = {
|
||||
trip: {
|
||||
id: '20250117-c85e57a7-7ac5-3736-9f8f-b37a1f660e4c',
|
||||
origin: {
|
||||
type: 'station',
|
||||
id: '8004168',
|
||||
name: 'München Flughafen Terminal',
|
||||
location: {
|
||||
type: 'location',
|
||||
id: '8004168',
|
||||
latitude: 48.353728,
|
||||
longitude: 11.78597,
|
||||
},
|
||||
},
|
||||
destination: {
|
||||
type: 'station',
|
||||
id: '8000309',
|
||||
name: 'Regensburg Hbf',
|
||||
location: {
|
||||
type: 'location',
|
||||
id: '8000309',
|
||||
latitude: 49.011672,
|
||||
longitude: 12.099617,
|
||||
},
|
||||
},
|
||||
departure: '2025-01-17T15:16:00+01:00',
|
||||
plannedDeparture: '2025-01-17T15:16:00+01:00',
|
||||
departureDelay: null,
|
||||
arrival: '2025-01-17T16:41:00+01:00',
|
||||
plannedArrival: '2025-01-17T16:41:00+01:00',
|
||||
arrivalDelay: null,
|
||||
line: {
|
||||
type: 'line',
|
||||
id: 'ag-re22-84100',
|
||||
fahrtNr: '84100',
|
||||
name: 'ag RE22',
|
||||
adminCode: 'S9',
|
||||
productName: 'ag',
|
||||
product: 'regional',
|
||||
mode: 'train',
|
||||
public: true,
|
||||
operator: {
|
||||
type: 'operator',
|
||||
id: 'ag',
|
||||
name: 'agilis',
|
||||
},
|
||||
},
|
||||
direction: null,
|
||||
arrivalPlatform: '5',
|
||||
plannedArrivalPlatform: '5',
|
||||
departurePlatform: '1',
|
||||
plannedDeparturePlatform: '1',
|
||||
stopovers: [
|
||||
{
|
||||
stop: {
|
||||
type: 'station',
|
||||
id: '8004168',
|
||||
name: 'München Flughafen Terminal',
|
||||
location: {
|
||||
type: 'location',
|
||||
id: '8004168',
|
||||
latitude: 48.353728,
|
||||
longitude: 11.78597,
|
||||
},
|
||||
},
|
||||
arrival: null,
|
||||
plannedArrival: null,
|
||||
arrivalDelay: null,
|
||||
arrivalPlatform: '1',
|
||||
arrivalPrognosisType: null,
|
||||
plannedArrivalPlatform: '1',
|
||||
departure: '2025-01-17T15:16:00+01:00',
|
||||
plannedDeparture: '2025-01-17T15:16:00+01:00',
|
||||
departureDelay: null,
|
||||
departurePlatform: '1',
|
||||
departurePrognosisType: null,
|
||||
plannedDeparturePlatform: '1',
|
||||
remarks: [],
|
||||
},
|
||||
{
|
||||
stop: {
|
||||
type: 'station',
|
||||
id: '8004167',
|
||||
name: 'München Flughafen Besucherpark',
|
||||
location: {
|
||||
type: 'location',
|
||||
id: '8004167',
|
||||
latitude: 48.352095,
|
||||
longitude: 11.764185,
|
||||
},
|
||||
},
|
||||
arrival: '2025-01-17T15:18:00+01:00',
|
||||
plannedArrival: '2025-01-17T15:18:00+01:00',
|
||||
arrivalDelay: null,
|
||||
arrivalPlatform: '1',
|
||||
arrivalPrognosisType: null,
|
||||
plannedArrivalPlatform: '1',
|
||||
departure: '2025-01-17T15:18:00+01:00',
|
||||
plannedDeparture: '2025-01-17T15:18:00+01:00',
|
||||
departureDelay: null,
|
||||
departurePlatform: '1',
|
||||
departurePrognosisType: null,
|
||||
plannedDeparturePlatform: '1',
|
||||
remarks: [],
|
||||
},
|
||||
{
|
||||
stop: {
|
||||
type: 'station',
|
||||
id: '8002078',
|
||||
name: 'Freising',
|
||||
location: {
|
||||
type: 'location',
|
||||
id: '8002078',
|
||||
latitude: 48.395195,
|
||||
longitude: 11.744539,
|
||||
},
|
||||
},
|
||||
arrival: '2025-01-17T15:28:00+01:00',
|
||||
plannedArrival: '2025-01-17T15:28:00+01:00',
|
||||
arrivalDelay: null,
|
||||
arrivalPlatform: '4',
|
||||
arrivalPrognosisType: null,
|
||||
plannedArrivalPlatform: '4',
|
||||
departure: '2025-01-17T15:29:00+01:00',
|
||||
plannedDeparture: '2025-01-17T15:29:00+01:00',
|
||||
departureDelay: null,
|
||||
departurePlatform: '4',
|
||||
departurePrognosisType: null,
|
||||
plannedDeparturePlatform: '4',
|
||||
remarks: [],
|
||||
},
|
||||
{
|
||||
stop: {
|
||||
type: 'station',
|
||||
id: '8004084',
|
||||
name: 'Moosburg',
|
||||
location: {
|
||||
type: 'location',
|
||||
id: '8004084',
|
||||
latitude: 48.47033,
|
||||
longitude: 11.930382,
|
||||
},
|
||||
},
|
||||
arrival: '2025-01-17T15:37:00+01:00',
|
||||
plannedArrival: '2025-01-17T15:37:00+01:00',
|
||||
arrivalDelay: null,
|
||||
arrivalPlatform: '1',
|
||||
arrivalPrognosisType: null,
|
||||
plannedArrivalPlatform: '1',
|
||||
departure: '2025-01-17T15:38:00+01:00',
|
||||
plannedDeparture: '2025-01-17T15:38:00+01:00',
|
||||
departureDelay: null,
|
||||
departurePlatform: '1',
|
||||
departurePrognosisType: null,
|
||||
plannedDeparturePlatform: '1',
|
||||
remarks: [],
|
||||
},
|
||||
{
|
||||
stop: {
|
||||
type: 'station',
|
||||
id: '8000217',
|
||||
name: 'Landshut(Bay)Hbf',
|
||||
location: {
|
||||
type: 'location',
|
||||
id: '8000217',
|
||||
latitude: 48.547492,
|
||||
longitude: 12.13593,
|
||||
},
|
||||
},
|
||||
arrival: '2025-01-17T15:50:00+01:00',
|
||||
plannedArrival: '2025-01-17T15:50:00+01:00',
|
||||
arrivalDelay: null,
|
||||
arrivalPlatform: '5',
|
||||
arrivalPrognosisType: null,
|
||||
plannedArrivalPlatform: '5',
|
||||
departure: '2025-01-17T15:53:00+01:00',
|
||||
plannedDeparture: '2025-01-17T15:53:00+01:00',
|
||||
departureDelay: null,
|
||||
departurePlatform: '5',
|
||||
departurePrognosisType: null,
|
||||
plannedDeparturePlatform: '5',
|
||||
remarks: [],
|
||||
},
|
||||
{
|
||||
stop: {
|
||||
type: 'station',
|
||||
id: '8001835',
|
||||
name: 'Ergoldsbach',
|
||||
location: {
|
||||
type: 'location',
|
||||
id: '8001835',
|
||||
latitude: 48.693868,
|
||||
longitude: 12.201874,
|
||||
},
|
||||
},
|
||||
arrival: '2025-01-17T16:05:00+01:00',
|
||||
plannedArrival: '2025-01-17T16:05:00+01:00',
|
||||
arrivalDelay: null,
|
||||
arrivalPlatform: '1',
|
||||
arrivalPrognosisType: null,
|
||||
plannedArrivalPlatform: '1',
|
||||
departure: '2025-01-17T16:06:00+01:00',
|
||||
plannedDeparture: '2025-01-17T16:06:00+01:00',
|
||||
departureDelay: null,
|
||||
departurePlatform: '1',
|
||||
departurePrognosisType: null,
|
||||
plannedDeparturePlatform: '1',
|
||||
remarks: [],
|
||||
},
|
||||
{
|
||||
stop: {
|
||||
type: 'station',
|
||||
id: '8000688',
|
||||
name: 'Neufahrn(Niederbay)',
|
||||
location: {
|
||||
type: 'location',
|
||||
id: '8000688',
|
||||
latitude: 48.729884,
|
||||
longitude: 12.19046,
|
||||
},
|
||||
},
|
||||
arrival: '2025-01-17T16:09:00+01:00',
|
||||
plannedArrival: '2025-01-17T16:09:00+01:00',
|
||||
arrivalDelay: null,
|
||||
arrivalPlatform: '2',
|
||||
arrivalPrognosisType: null,
|
||||
plannedArrivalPlatform: '2',
|
||||
departure: '2025-01-17T16:10:00+01:00',
|
||||
plannedDeparture: '2025-01-17T16:10:00+01:00',
|
||||
departureDelay: null,
|
||||
departurePlatform: '2',
|
||||
departurePrognosisType: null,
|
||||
plannedDeparturePlatform: '2',
|
||||
remarks: [],
|
||||
},
|
||||
{
|
||||
stop: {
|
||||
type: 'station',
|
||||
id: '8001679',
|
||||
name: 'Eggmühl',
|
||||
location: {
|
||||
type: 'location',
|
||||
id: '8001679',
|
||||
latitude: 48.836497,
|
||||
longitude: 12.182192,
|
||||
},
|
||||
},
|
||||
arrival: '2025-01-17T16:19:00+01:00',
|
||||
plannedArrival: '2025-01-17T16:19:00+01:00',
|
||||
arrivalDelay: null,
|
||||
arrivalPlatform: '3',
|
||||
arrivalPrognosisType: null,
|
||||
plannedArrivalPlatform: '3',
|
||||
departure: '2025-01-17T16:20:00+01:00',
|
||||
plannedDeparture: '2025-01-17T16:20:00+01:00',
|
||||
departureDelay: null,
|
||||
departurePlatform: '3',
|
||||
departurePrognosisType: null,
|
||||
plannedDeparturePlatform: '3',
|
||||
remarks: [],
|
||||
},
|
||||
{
|
||||
stop: {
|
||||
type: 'station',
|
||||
id: '8002506',
|
||||
name: 'Hagelstadt',
|
||||
location: {
|
||||
type: 'location',
|
||||
id: '8002506',
|
||||
latitude: 48.895859,
|
||||
longitude: 12.214829,
|
||||
},
|
||||
},
|
||||
arrival: '2025-01-17T16:25:00+01:00',
|
||||
plannedArrival: '2025-01-17T16:25:00+01:00',
|
||||
arrivalDelay: null,
|
||||
arrivalPlatform: '2',
|
||||
arrivalPrognosisType: null,
|
||||
plannedArrivalPlatform: '2',
|
||||
departure: '2025-01-17T16:26:00+01:00',
|
||||
plannedDeparture: '2025-01-17T16:26:00+01:00',
|
||||
departureDelay: null,
|
||||
departurePlatform: '2',
|
||||
departurePrognosisType: null,
|
||||
plannedDeparturePlatform: '2',
|
||||
remarks: [],
|
||||
},
|
||||
{
|
||||
stop: {
|
||||
type: 'station',
|
||||
id: '8003357',
|
||||
name: 'Köfering',
|
||||
location: {
|
||||
type: 'location',
|
||||
id: '8003357',
|
||||
latitude: 48.931716,
|
||||
longitude: 12.20875,
|
||||
},
|
||||
},
|
||||
arrival: '2025-01-17T16:29:00+01:00',
|
||||
plannedArrival: '2025-01-17T16:29:00+01:00',
|
||||
arrivalDelay: null,
|
||||
arrivalPlatform: '2',
|
||||
arrivalPrognosisType: null,
|
||||
plannedArrivalPlatform: '2',
|
||||
departure: '2025-01-17T16:30:00+01:00',
|
||||
plannedDeparture: '2025-01-17T16:30:00+01:00',
|
||||
departureDelay: null,
|
||||
departurePlatform: '2',
|
||||
departurePrognosisType: null,
|
||||
plannedDeparturePlatform: '2',
|
||||
remarks: [],
|
||||
},
|
||||
{
|
||||
stop: {
|
||||
type: 'station',
|
||||
id: '8004592',
|
||||
name: 'Obertraubling',
|
||||
location: {
|
||||
type: 'location',
|
||||
id: '8004592',
|
||||
latitude: 48.967537,
|
||||
longitude: 12.169996,
|
||||
},
|
||||
},
|
||||
arrival: '2025-01-17T16:33:00+01:00',
|
||||
plannedArrival: '2025-01-17T16:33:00+01:00',
|
||||
arrivalDelay: null,
|
||||
arrivalPlatform: '2',
|
||||
arrivalPrognosisType: null,
|
||||
plannedArrivalPlatform: '2',
|
||||
departure: '2025-01-17T16:34:00+01:00',
|
||||
plannedDeparture: '2025-01-17T16:34:00+01:00',
|
||||
departureDelay: null,
|
||||
departurePlatform: '2',
|
||||
departurePrognosisType: null,
|
||||
plannedDeparturePlatform: '2',
|
||||
remarks: [],
|
||||
},
|
||||
{
|
||||
stop: {
|
||||
type: 'station',
|
||||
id: '8000309',
|
||||
name: 'Regensburg Hbf',
|
||||
location: {
|
||||
type: 'location',
|
||||
id: '8000309',
|
||||
latitude: 49.011672,
|
||||
longitude: 12.099617,
|
||||
},
|
||||
},
|
||||
arrival: '2025-01-17T16:41:00+01:00',
|
||||
plannedArrival: '2025-01-17T16:41:00+01:00',
|
||||
arrivalDelay: null,
|
||||
arrivalPlatform: '5',
|
||||
arrivalPrognosisType: null,
|
||||
plannedArrivalPlatform: '5',
|
||||
departure: null,
|
||||
plannedDeparture: null,
|
||||
departureDelay: null,
|
||||
departurePlatform: '5',
|
||||
departurePrognosisType: null,
|
||||
plannedDeparturePlatform: '5',
|
||||
remarks: [],
|
||||
},
|
||||
// train split
|
||||
],
|
||||
remarks: [],
|
||||
cancelled: false,
|
||||
},
|
||||
realtimeDataUpdatedAt: null,
|
||||
};
|
||||
|
||||
export {
|
||||
dbTrip,
|
||||
};
|
410
test/fixtures/dbregioguide-trip.json
vendored
Normal file
410
test/fixtures/dbregioguide-trip.json
vendored
Normal file
|
@ -0,0 +1,410 @@
|
|||
{
|
||||
"name": "ag RE22",
|
||||
"no": 84100,
|
||||
"journeyId": "20250117-c85e57a7-7ac5-3736-9f8f-b37a1f660e4c",
|
||||
"tenantId": "bayern",
|
||||
"administrationId": "S9",
|
||||
"operatorName": "agilis",
|
||||
"operatorCode": "ag",
|
||||
"category": "ag",
|
||||
"type": "REGIONAL_TRAIN",
|
||||
"date": "2025-01-17T15:16:00+01:00",
|
||||
"stops": [
|
||||
{
|
||||
"status": "Normal",
|
||||
"departureId": "8004168_D_1",
|
||||
"station": {
|
||||
"evaNo": "8004168",
|
||||
"name": "München Flughafen Terminal",
|
||||
"position": {
|
||||
"latitude": 48.353728,
|
||||
"longitude": 11.78597
|
||||
}
|
||||
},
|
||||
"track": {
|
||||
"target": "1",
|
||||
"prediction": "1"
|
||||
},
|
||||
"messages": [],
|
||||
"departureTime": {
|
||||
"target": "2025-01-17T15:16:00+01:00",
|
||||
"predicted": "2025-01-17T15:16:00+01:00",
|
||||
"diff": 0,
|
||||
"targetTimeInMs": 1737123360000,
|
||||
"predictedTimeInMs": 1737123360000,
|
||||
"timeType": "SCHEDULE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"status": "Normal",
|
||||
"arrivalId": "8004167_A_1",
|
||||
"departureId": "8004167_D_1",
|
||||
"station": {
|
||||
"evaNo": "8004167",
|
||||
"name": "München Flughafen Besucherpark",
|
||||
"position": {
|
||||
"latitude": 48.352095,
|
||||
"longitude": 11.764185
|
||||
}
|
||||
},
|
||||
"track": {
|
||||
"target": "1",
|
||||
"prediction": "1"
|
||||
},
|
||||
"messages": [],
|
||||
"departureTime": {
|
||||
"target": "2025-01-17T15:18:00+01:00",
|
||||
"predicted": "2025-01-17T15:18:00+01:00",
|
||||
"diff": 0,
|
||||
"targetTimeInMs": 1737123480000,
|
||||
"predictedTimeInMs": 1737123480000,
|
||||
"timeType": "SCHEDULE"
|
||||
},
|
||||
"arrivalTime": {
|
||||
"target": "2025-01-17T15:18:00+01:00",
|
||||
"predicted": "2025-01-17T15:18:00+01:00",
|
||||
"diff": 0,
|
||||
"targetTimeInMs": 1737123480000,
|
||||
"predictedTimeInMs": 1737123480000,
|
||||
"timeType": "SCHEDULE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"status": "Normal",
|
||||
"arrivalId": "8002078_A_1",
|
||||
"departureId": "8002078_D_1",
|
||||
"station": {
|
||||
"evaNo": "8002078",
|
||||
"name": "Freising",
|
||||
"position": {
|
||||
"latitude": 48.395195,
|
||||
"longitude": 11.744539
|
||||
}
|
||||
},
|
||||
"track": {
|
||||
"target": "4",
|
||||
"prediction": "4"
|
||||
},
|
||||
"messages": [],
|
||||
"departureTime": {
|
||||
"target": "2025-01-17T15:29:00+01:00",
|
||||
"predicted": "2025-01-17T15:29:00+01:00",
|
||||
"diff": 0,
|
||||
"targetTimeInMs": 1737124140000,
|
||||
"predictedTimeInMs": 1737124140000,
|
||||
"timeType": "SCHEDULE"
|
||||
},
|
||||
"arrivalTime": {
|
||||
"target": "2025-01-17T15:28:00+01:00",
|
||||
"predicted": "2025-01-17T15:28:00+01:00",
|
||||
"diff": 0,
|
||||
"targetTimeInMs": 1737124080000,
|
||||
"predictedTimeInMs": 1737124080000,
|
||||
"timeType": "SCHEDULE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"status": "Normal",
|
||||
"arrivalId": "8004084_A_1",
|
||||
"departureId": "8004084_D_1",
|
||||
"station": {
|
||||
"evaNo": "8004084",
|
||||
"name": "Moosburg",
|
||||
"position": {
|
||||
"latitude": 48.47033,
|
||||
"longitude": 11.930382
|
||||
}
|
||||
},
|
||||
"track": {
|
||||
"target": "1",
|
||||
"prediction": "1"
|
||||
},
|
||||
"messages": [],
|
||||
"departureTime": {
|
||||
"target": "2025-01-17T15:38:00+01:00",
|
||||
"predicted": "2025-01-17T15:38:00+01:00",
|
||||
"diff": 0,
|
||||
"targetTimeInMs": 1737124680000,
|
||||
"predictedTimeInMs": 1737124680000,
|
||||
"timeType": "SCHEDULE"
|
||||
},
|
||||
"arrivalTime": {
|
||||
"target": "2025-01-17T15:37:00+01:00",
|
||||
"predicted": "2025-01-17T15:37:00+01:00",
|
||||
"diff": 0,
|
||||
"targetTimeInMs": 1737124620000,
|
||||
"predictedTimeInMs": 1737124620000,
|
||||
"timeType": "SCHEDULE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"status": "Normal",
|
||||
"arrivalId": "8000217_A_1",
|
||||
"departureId": "8000217_D_1",
|
||||
"station": {
|
||||
"evaNo": "8000217",
|
||||
"name": "Landshut(Bay)Hbf",
|
||||
"position": {
|
||||
"latitude": 48.547492,
|
||||
"longitude": 12.13593
|
||||
}
|
||||
},
|
||||
"track": {
|
||||
"target": "5",
|
||||
"prediction": "5"
|
||||
},
|
||||
"messages": [],
|
||||
"departureTime": {
|
||||
"target": "2025-01-17T15:53:00+01:00",
|
||||
"predicted": "2025-01-17T15:53:00+01:00",
|
||||
"diff": 0,
|
||||
"targetTimeInMs": 1737125580000,
|
||||
"predictedTimeInMs": 1737125580000,
|
||||
"timeType": "SCHEDULE"
|
||||
},
|
||||
"arrivalTime": {
|
||||
"target": "2025-01-17T15:50:00+01:00",
|
||||
"predicted": "2025-01-17T15:50:00+01:00",
|
||||
"diff": 0,
|
||||
"targetTimeInMs": 1737125400000,
|
||||
"predictedTimeInMs": 1737125400000,
|
||||
"timeType": "SCHEDULE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"status": "Normal",
|
||||
"arrivalId": "8001835_A_1",
|
||||
"departureId": "8001835_D_1",
|
||||
"station": {
|
||||
"evaNo": "8001835",
|
||||
"name": "Ergoldsbach",
|
||||
"position": {
|
||||
"latitude": 48.693868,
|
||||
"longitude": 12.201874
|
||||
}
|
||||
},
|
||||
"track": {
|
||||
"target": "1",
|
||||
"prediction": "1"
|
||||
},
|
||||
"messages": [],
|
||||
"departureTime": {
|
||||
"target": "2025-01-17T16:06:00+01:00",
|
||||
"predicted": "2025-01-17T16:06:00+01:00",
|
||||
"diff": 0,
|
||||
"targetTimeInMs": 1737126360000,
|
||||
"predictedTimeInMs": 1737126360000,
|
||||
"timeType": "SCHEDULE"
|
||||
},
|
||||
"arrivalTime": {
|
||||
"target": "2025-01-17T16:05:00+01:00",
|
||||
"predicted": "2025-01-17T16:05:00+01:00",
|
||||
"diff": 0,
|
||||
"targetTimeInMs": 1737126300000,
|
||||
"predictedTimeInMs": 1737126300000,
|
||||
"timeType": "SCHEDULE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"status": "Normal",
|
||||
"arrivalId": "8000688_A_1",
|
||||
"departureId": "8000688_D_1",
|
||||
"station": {
|
||||
"evaNo": "8000688",
|
||||
"name": "Neufahrn(Niederbay)",
|
||||
"position": {
|
||||
"latitude": 48.729884,
|
||||
"longitude": 12.19046
|
||||
}
|
||||
},
|
||||
"track": {
|
||||
"target": "2",
|
||||
"prediction": "2"
|
||||
},
|
||||
"messages": [],
|
||||
"departureTime": {
|
||||
"target": "2025-01-17T16:10:00+01:00",
|
||||
"predicted": "2025-01-17T16:10:00+01:00",
|
||||
"diff": 0,
|
||||
"targetTimeInMs": 1737126600000,
|
||||
"predictedTimeInMs": 1737126600000,
|
||||
"timeType": "SCHEDULE"
|
||||
},
|
||||
"arrivalTime": {
|
||||
"target": "2025-01-17T16:09:00+01:00",
|
||||
"predicted": "2025-01-17T16:09:00+01:00",
|
||||
"diff": 0,
|
||||
"targetTimeInMs": 1737126540000,
|
||||
"predictedTimeInMs": 1737126540000,
|
||||
"timeType": "SCHEDULE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"status": "Normal",
|
||||
"arrivalId": "8001679_A_1",
|
||||
"departureId": "8001679_D_1",
|
||||
"station": {
|
||||
"evaNo": "8001679",
|
||||
"name": "Eggmühl",
|
||||
"position": {
|
||||
"latitude": 48.836497,
|
||||
"longitude": 12.182192
|
||||
}
|
||||
},
|
||||
"track": {
|
||||
"target": "3",
|
||||
"prediction": "3"
|
||||
},
|
||||
"messages": [],
|
||||
"departureTime": {
|
||||
"target": "2025-01-17T16:20:00+01:00",
|
||||
"predicted": "2025-01-17T16:20:00+01:00",
|
||||
"diff": 0,
|
||||
"targetTimeInMs": 1737127200000,
|
||||
"predictedTimeInMs": 1737127200000,
|
||||
"timeType": "SCHEDULE"
|
||||
},
|
||||
"arrivalTime": {
|
||||
"target": "2025-01-17T16:19:00+01:00",
|
||||
"predicted": "2025-01-17T16:19:00+01:00",
|
||||
"diff": 0,
|
||||
"targetTimeInMs": 1737127140000,
|
||||
"predictedTimeInMs": 1737127140000,
|
||||
"timeType": "SCHEDULE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"status": "Normal",
|
||||
"arrivalId": "8002506_A_1",
|
||||
"departureId": "8002506_D_1",
|
||||
"station": {
|
||||
"evaNo": "8002506",
|
||||
"name": "Hagelstadt",
|
||||
"position": {
|
||||
"latitude": 48.895859,
|
||||
"longitude": 12.214829
|
||||
}
|
||||
},
|
||||
"track": {
|
||||
"target": "2",
|
||||
"prediction": "2"
|
||||
},
|
||||
"messages": [],
|
||||
"departureTime": {
|
||||
"target": "2025-01-17T16:26:00+01:00",
|
||||
"predicted": "2025-01-17T16:26:00+01:00",
|
||||
"diff": 0,
|
||||
"targetTimeInMs": 1737127560000,
|
||||
"predictedTimeInMs": 1737127560000,
|
||||
"timeType": "SCHEDULE"
|
||||
},
|
||||
"arrivalTime": {
|
||||
"target": "2025-01-17T16:25:00+01:00",
|
||||
"predicted": "2025-01-17T16:25:00+01:00",
|
||||
"diff": 0,
|
||||
"targetTimeInMs": 1737127500000,
|
||||
"predictedTimeInMs": 1737127500000,
|
||||
"timeType": "SCHEDULE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"status": "Normal",
|
||||
"arrivalId": "8003357_A_1",
|
||||
"departureId": "8003357_D_1",
|
||||
"station": {
|
||||
"evaNo": "8003357",
|
||||
"name": "Köfering",
|
||||
"position": {
|
||||
"latitude": 48.931716,
|
||||
"longitude": 12.20875
|
||||
}
|
||||
},
|
||||
"track": {
|
||||
"target": "2",
|
||||
"prediction": "2"
|
||||
},
|
||||
"messages": [],
|
||||
"departureTime": {
|
||||
"target": "2025-01-17T16:30:00+01:00",
|
||||
"predicted": "2025-01-17T16:30:00+01:00",
|
||||
"diff": 0,
|
||||
"targetTimeInMs": 1737127800000,
|
||||
"predictedTimeInMs": 1737127800000,
|
||||
"timeType": "SCHEDULE"
|
||||
},
|
||||
"arrivalTime": {
|
||||
"target": "2025-01-17T16:29:00+01:00",
|
||||
"predicted": "2025-01-17T16:29:00+01:00",
|
||||
"diff": 0,
|
||||
"targetTimeInMs": 1737127740000,
|
||||
"predictedTimeInMs": 1737127740000,
|
||||
"timeType": "SCHEDULE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"status": "Normal",
|
||||
"arrivalId": "8004592_A_1",
|
||||
"departureId": "8004592_D_1",
|
||||
"station": {
|
||||
"evaNo": "8004592",
|
||||
"name": "Obertraubling",
|
||||
"position": {
|
||||
"latitude": 48.967537,
|
||||
"longitude": 12.169996
|
||||
}
|
||||
},
|
||||
"track": {
|
||||
"target": "2",
|
||||
"prediction": "2"
|
||||
},
|
||||
"messages": [],
|
||||
"departureTime": {
|
||||
"target": "2025-01-17T16:34:00+01:00",
|
||||
"predicted": "2025-01-17T16:34:00+01:00",
|
||||
"diff": 0,
|
||||
"targetTimeInMs": 1737128040000,
|
||||
"predictedTimeInMs": 1737128040000,
|
||||
"timeType": "SCHEDULE"
|
||||
},
|
||||
"arrivalTime": {
|
||||
"target": "2025-01-17T16:33:00+01:00",
|
||||
"predicted": "2025-01-17T16:33:00+01:00",
|
||||
"diff": 0,
|
||||
"targetTimeInMs": 1737127980000,
|
||||
"predictedTimeInMs": 1737127980000,
|
||||
"timeType": "SCHEDULE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"status": "Normal",
|
||||
"arrivalId": "8000309_A_1",
|
||||
"station": {
|
||||
"evaNo": "8000309",
|
||||
"name": "Regensburg Hbf",
|
||||
"position": {
|
||||
"latitude": 49.011672,
|
||||
"longitude": 12.099617
|
||||
}
|
||||
},
|
||||
"track": {
|
||||
"target": "5",
|
||||
"prediction": "5"
|
||||
},
|
||||
"messages": [],
|
||||
"arrivalTime": {
|
||||
"target": "2025-01-17T16:41:00+01:00",
|
||||
"predicted": "2025-01-17T16:41:00+01:00",
|
||||
"diff": 0,
|
||||
"targetTimeInMs": 1737128460000,
|
||||
"predictedTimeInMs": 1737128460000,
|
||||
"timeType": "SCHEDULE"
|
||||
}
|
||||
}
|
||||
],
|
||||
"started": false,
|
||||
"finished": false,
|
||||
"hims": [],
|
||||
"validUntil": "2025-01-17T15:46:00.000Z",
|
||||
"validFrom": "2025-01-17T14:06:00.000Z",
|
||||
"isLoyaltyCaseEligible": false
|
||||
}
|
580
test/fixtures/dbris-departures.js
vendored
Normal file
580
test/fixtures/dbris-departures.js
vendored
Normal 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
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
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
189
test/fixtures/dbweb-departures.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
const dbJourney = {
|
||||
const dbwebJourney = {
|
||||
type: 'journey',
|
||||
legs: [
|
||||
{
|
||||
|
@ -291,8 +291,8 @@ 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$$$$$$¶KC¶#VE#2#CF#100#CA#0#CM#0#SICT#0#AM#81#AM2#0#RT#7#¶KCC¶I1ZFIzEjRVJHIzMjSElOIzAjRUNLIzcwNTkxMXw3MDU5MTF8NzA2MTM4fDcwNjEzOHwwfDB8NDg1fDcwNTg5N3wxfDB8MTh8MHwwfC0yMTQ3NDgzNjQ4I0dBTSMxMTA0MjUwNTExIwpaI1ZOIzEjU1QjMTczMzE3MzczMSNQSSMxI1pJIzE2MTQ3MyNUQSMxI0RBIzExMDQyNSMxUyM4MDAwMjA4IzFUIzUwNCNMUyM4MDAyNzUzI0xUIzU0NSNQVSM4MSNSVCMxI0NBI3MjWkUjMTIjWkIjUyAgICAgMTIjUEMjNCNGUiM4MDAwMjA3I0ZUIzUxMSNUTyM4MDAzMzY4I1RUIzUxMiMKRiNWTiMwI1NUIzE3MzMxNzM3MzEjUEkjMSNQVSM4MSNaSSMyMjgzODI4ODkzI0RBIzExMDQyNSNGUiM4MDAzMzY4I1RPIzgwNzMzNjgjRlQjNTEyI1RUIzUxOSNUUyMwI0ZGIyNGViMwIwpaI1ZOIzEjU1QjMTczMzE3MzczMSNQSSMxI1pJIzE1NTA2MyNUQSMwI0RBIzExMDQyNSMxUyM4MDAwMDgwIzFUIzM1OCNMUyM4MDAwMjYxI0xUIzEwMDYjUFUjODEjUlQjMSNDQSNJQ0UjWkUjNTIzI1pCI0lDRSAgNTIzI1BDIzAjRlIjODA3MzM2OCNGVCM1MjAjVE8jODAwMDI4NCNUVCM4NTgj¶KRCC¶#VE#1#¶SC¶1_H4sIAAAAAAACA32P306DMBjFX8X0GpevhUIhIUFGFv8sGzHOaIwXbHQTU2CWskgIz+GbeOXdXswCemE09qLpOT09v68tOnCJPIQnDkMG4q9Kiyic3EYTV2vJX5DXoqLOZ8ijRn8IkQcGKmsVJYrrMAFCwcIYDeZNlvcmUNBLW9uh4RQb6LloZkLJOfIeWqSafR+Lr5eRDuVl2quLxVSLQyLqXmEgJuoeh5mmT7uxWJNTvp+Xm7FGZKlOnvk4WPpXx3dRnJyvt8Gdb7uUOSYE9z4F1zKBuMHKZxDM9QZAwAlC/WbvY8c0scNsmwSZvzq+ATDA1KIs0INUavzgbJgikfJP7OL4IYs1l7svNMbAiMtczbZcy6I2pj/YzPqHTQh2zd/sHVdxKRqRFdpTsuaDdVnWsuBNWNZFWiFvm4hqvIiTqhJZpb6zfFPGiUxyHWq7rvsE0LytQvMBAAA=',
|
||||
price: {amount: 31.49, currency: 'EUR', hint: null},
|
||||
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, partialFare: false},
|
||||
tickets: [{
|
||||
name: 'from',
|
||||
priceObj: {
|
||||
|
@ -304,5 +304,5 @@ const dbJourney = {
|
|||
};
|
||||
|
||||
export {
|
||||
dbJourney,
|
||||
dbwebJourney,
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue