Compare commits

..

No commits in common. "5e502a6f442b42905bdda9ece70bd7ff43fa1b8e" and "932a4aa0dbf1e3d52799ce1d90b76c4157646332" have entirely different histories.

36 changed files with 1365 additions and 1911 deletions

54
.eslintrc.json Normal file
View file

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

View file

@ -6,20 +6,6 @@ env:
npm_config_cache: /tmp/npm-cache
jobs:
lint:
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
- run: npm run lint
unit-tests:
runs-on: ubuntu-latest
strategy:
@ -39,12 +25,13 @@ jobs:
- id: cache-npm
name: restore npm cache
uses: actions/cache@v4
uses: actions/cache@v3
with:
key: npm-cache-${{ github.ref_name }}-${{ matrix.node-version }}-unit-tests
key: npm-cache-${{ github.ref_name }}
path: ${{ env.npm_config_cache }}
- run: npm install
- run: npm run lint
- run: npm run test-unit
integration-tests:
@ -66,9 +53,9 @@ jobs:
- id: cache-npm
name: restore npm cache
uses: actions/cache@v4
uses: actions/cache@v3
with:
key: npm-cache-${{ github.ref_name }}-${{ matrix.node-version }}-integration-tests
key: npm-cache-${{ github.ref_name }}
path: ${{ env.npm_config_cache }}
- run: npm install
@ -79,7 +66,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x]
node-version: [16.x]
steps:
- name: checkout
uses: actions/checkout@v4
@ -90,9 +77,9 @@ jobs:
- id: cache-npm
name: restore npm cache
uses: actions/cache@v4
uses: actions/cache@v3
with:
key: npm-cache-${{ github.ref_name }}-${{ matrix.node-version }}-e2e-tests
key: npm-cache-${{ github.ref_name }}
path: ${{ env.npm_config_cache }}
- run: npm install

34
api.js
View file

@ -1,9 +1,32 @@
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 {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',
@ -22,15 +45,10 @@ const config = {
mapRouteParsers,
};
const profiles = {
db: dbProfile,
dbnav: dbnavProfile,
dbweb: dbwebProfile,
};
const start = async () => {
const vendo = createClient(
profiles[process.env.DB_PROFILE] || dbnavProfile,
process.env.DB_PROFILE == 'db' ? dbProfile : dbnavProfile,
process.env.USER_AGENT || 'link-to-your-project-or-email',
config,
);

View file

@ -1,7 +0,0 @@
import {createClient} from './index.js';
import {profile} from './p/dbnav/index.js';
const client = createClient(profile, 'hafas-client-debug');
const journeys = await client.journeys('8000105', '8000261', {results: 1});
console.log(journeys);

View file

@ -24,7 +24,7 @@ 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)
direction: null, // not supported
line: null, // not supported
duration: 10, // show departures for the next n minutes
results: null, // max. number of results; `null` means "whatever HAFAS wants"
@ -35,7 +35,7 @@ With `opt`, you can override the default options, which look like this:
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,
includeRelatedStations: true, // only true supported
language: 'en' // language to get results in
}
```

View file

@ -385,14 +385,13 @@ paths:
- bahncard-2nd-25
- bahncard-1st-50
- bahncard-2nd-50
- bahncard-1st-100
- bahncard-2nd-100
- vorteilscard
- halbtaxabo-railplus
- halbtaxabo
- generalabonnement-1st
- generalabonnement-2nd
- nl-40
- at-klimaticket
- voordeelurenabo-railplus
- voordeelurenabo
- shcard
- generalabonnement
- name: firstClass
in: query
description: Search for first-class options?
@ -2088,7 +2087,7 @@ components:
type: string
format: date-time
direction:
description: only show departures heading to this station, only supported for `dbweb` profile
description: only show departures heading to this station
default: undefined
type: string
line:
@ -2125,7 +2124,7 @@ components:
type: boolean
includeRelatedStations:
description: departures at related stations
default: true
default: false
type: boolean
products:
$ref: '#/components/schemas/Products'

View file

@ -1,61 +0,0 @@
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-mixed-operators': 'off',
'@stylistic/no-tabs': 'off',
'@stylistic/object-property-newline': 'off',
'@stylistic/one-var-declaration-per-line': 'off',
'@stylistic/operator-linebreak': ['error', 'before'],
'@stylistic/padded-blocks': 'off',
'@stylistic/quote-props': ['error', 'consistent-as-needed'],
'@stylistic/quotes': ['error', 'single'],
'curly': 'error',
'no-implicit-coercion': 'error',
'no-unused-vars': [
'error',
{
vars: 'all',
args: 'none',
ignoreRestSiblings: false,
},
],
},
},
{
files: ['test/**', '**/example.js'],
rules: {
'no-unused-vars': 'off',
'@stylistic/semi': 'off',
},
},
];
export default config;

View file

@ -6,10 +6,9 @@ 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 {
@ -20,7 +19,7 @@ const formatLoyaltyCard = (data) => {
const cls = data.class === 1 ? 'KLASSE_1' : 'KLASSE_2';
if (data.type.toString() === c.BAHNCARD.toString()) {
return {
art: 'BAHNCARD' + (data.business ? 'BUSINESS' : '') + data.discount,
art: 'BAHNCARD' + data.discount,
klasse: cls,
};
}
@ -36,24 +35,13 @@ const formatLoyaltyCard = (data) => {
klasse: 'KLASSENLOS',
};
}
// TODO Rest
if (data.type.toString() === c.GENERALABONNEMENT.toString()) {
return {
art: 'CH-GENERAL-ABONNEMENT',
klasse: cls,
};
}
if (data.type.toString() === c.NL_40.toString()) {
return {
art: 'NL-40_OHNE_RAILPLUS',
klasse: 'KLASSENLOS',
};
}
if (data.type.toString() === c.AT_KLIMATICKET.toString()) {
return {
art: 'KLIMATICKET_OE',
klasse: 'KLASSENLOS',
};
}
return {
art: 'KEINE_ERMAESSIGUNG',
klasse: 'KLASSENLOS',

View file

@ -26,43 +26,34 @@ const validateLocation = (loc, name = 'location') => {
}
};
const loadEnrichedStationData = (profile) => new Promise((resolve, reject) => {
import('db-hafas-stations').then(m => {
const items = {};
m.default.full()
.on('data', (station) => {
items[station.id] = station;
items[station.name] = station;
})
.once('end', () => {
if (profile.DEBUG) {
console.log('Loaded station index.');
}
resolve(items);
})
.once('error', (err) => {
reject(err);
});
});
const loadEnrichedStationData = (profile) => new Promise(async (resolve, reject) => {
const { default: readStations} = await import('db-hafas-stations');
const items = {};
readStations.full()
.on('data', (station) => {
items[station.id] = station;
items[station.name] = station;
})
.once('end', () => {
if (profile.DEBUG) {
console.log('Loaded station index.');
}
resolve(items);
})
.once('error', (err) => {
reject(err);
});
});
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 = {};
let shouldLoadEnrichedStationData = false;
if (typeof opt.enrichStations === 'function') {
profile.enrichStation = opt.enrichStations;
} else if (opt.enrichStations !== false) {
shouldLoadEnrichedStationData = true;
if (opt.enrichStations !== false) {
loadEnrichedStationData(profile)
.then(locations => {
common.locations = locations;
});
}
if ('string' !== typeof userAgent) {
@ -73,7 +64,6 @@ 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) {
@ -116,15 +106,9 @@ const createClient = (profile, userAgent, opt = {}) => {
const {res} = await profile.request({profile, opt}, userAgent, req);
const ctx = {profile, opt, common, res};
let results = (res[resultsField] || res.items || res.bahnhofstafelAbfahrtPositionen || res.bahnhofstafelAnkunftPositionen || res.entries)
const results = (res[resultsField] || res.items || res.bahnhofstafelAbfahrtPositionen || res.bahnhofstafelAnkunftPositionen || res.entries)
.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
@ -139,7 +123,6 @@ 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.');
}
@ -222,8 +205,6 @@ 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.');
}
@ -250,8 +231,6 @@ 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.');
}
@ -278,8 +257,6 @@ 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) {
@ -301,8 +278,6 @@ const createClient = (profile, userAgent, opt = {}) => {
};
const nearby = async (location, opt = {}) => {
await applyEnrichedStationData({profile, common}, shouldLoadEnrichedStationData);
validateLocation(location, 'location');
opt = Object.assign({
@ -333,8 +308,6 @@ 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.');
}
@ -362,8 +335,6 @@ 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');
};
@ -390,5 +361,4 @@ const createClient = (profile, userAgent, opt = {}) => {
export {
createClient,
loadEnrichedStationData,
};

View file

@ -1,74 +0,0 @@
import {data as cards} from '../format/loyalty-cards.js';
import {parseBoolean, parseInteger} 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 !== 'journeys') {
return parsers;
}
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),
},
};
};
export {
mapRouteParsers,
};

View file

@ -16,7 +16,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, enrichStation} from '../parse/location.js';
import {parseLocation} 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 +37,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(typeof process !== 'undefined' ? process.env.DEBUG || '' : '');
const DEBUG = (/(^|,)hafas-client(,|$)/).test((typeof process !== 'undefined') ? (process.env.DEBUG || '') : '');
const logRequest = DEBUG
? (_, req, reqId) => console.error(String(req.body))
: () => { };
@ -82,7 +82,6 @@ const defaultProfile = {
parseLine,
parseStationName: id,
parseLocation,
enrichStation,
parsePolyline,
parseOperator,
parseRemarks,

View file

@ -7,10 +7,9 @@ const randomBytesHex = (nBytes = 8) => {
const array = new Uint8Array(nBytes);
crypto.getRandomValues(array);
return Array.from(array)
.map((byte) => byte.toString(16)
.padStart(2, '0'))
.join('');
};
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('');
};
const checkIfResponseIsOk = (_) => {
const {

View file

@ -1,7 +1,4 @@
# ISC License
- Copyright © 2024 Jannis R
- Copyright © 2025 traines-source
Copyright (c) 2024, Jannis R
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.

View file

@ -1,5 +1,5 @@
import dbnavBase from '../dbnav/base.json' with { type: 'json' };
import dbregioguideBase from '../dbregioguide/base.json' with { type: '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';
// journeys()

View file

@ -1,4 +1,4 @@
import baseProfile from './base.json' with { type: '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';

View file

@ -1,4 +1,4 @@
import baseProfile from './base.json' with { type: 'json' };
import baseProfile from './base.json' with { type: "json" };
import {products} from '../../lib/products.js';
import {formatTripReq} from './trip-req.js';

View file

@ -1,4 +1,4 @@
import baseProfile from './base.json' with { type: 'json' };
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';

View file

@ -8,7 +8,7 @@ const formatStationBoardReq = (ctx, station, type) => {
ortExtId: station,
zeit: profile.formatTimeOfDay(profile, opt.when),
datum: profile.formatDate(profile, opt.when),
mitVias: opt.stopovers || Boolean(opt.direction) || undefined,
mitVias: opt.stopovers || undefined,
verkehrsmittel: profile.formatProductsFilter(ctx, opt.products || {}),
},
method: 'GET',

2732
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{
"name": "db-vendo-client",
"description": "Client for bahn.de public transport APIs.",
"version": "6.5.0",
"version": "6.4.0",
"type": "module",
"main": "index.js",
"files": [
@ -68,15 +68,12 @@
"uuid": "^11.0.5"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.20.0",
"@pollyjs/adapter-node-http": "^6.0.5",
"@pollyjs/core": "^6.0.5",
"@pollyjs/persister-fs": "^6.0.5",
"@stylistic/eslint-plugin": "^3.1.0",
"@stylistic/eslint-plugin": "^1.5.1",
"db-rest": "github:derhuerst/db-rest",
"eslint": "^9.20.1",
"globals": "^15.15.0",
"eslint": "^8.56.0",
"hafas-rest-api": "^5.1.3",
"is-coordinates": "^2.0.2",
"is-roughly-equal": "^0.1.0",
@ -86,8 +83,8 @@
"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",

View file

@ -40,7 +40,7 @@ const createParseArrOrDep = (prefix) => {
res.remarks = profile.parseRemarks(ctx, d);
}
if ((opt.stopovers || opt.direction) && Array.isArray(d.ueber)) {
if (opt.stopovers && Array.isArray(d.ueber)) {
const stopovers = d.ueber
.map(viaName => profile.parseStopover(ctx, {name: viaName}, null));

View file

@ -16,11 +16,11 @@ const parseJourneyLeg = (ctx, pt, date, fallbackLocations) => { // pt = raw leg
const stops = pt.halte?.length && pt.halte || pt.stops?.length && pt.stops || [];
const res = {
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),
|| 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),
|| pt.ankunftsOrt?.name && profile.parseLocation(ctx, pt.ankunftsOrt)
|| locationFallback(pt.ankunftsOrtExtId, pt.ankunftsOrt, fallbackLocations),
};
const cancelledDep = stops.length && profile.parseCancelled(stops[0]);

View file

@ -14,7 +14,7 @@ const parseLocation = (ctx, l) => {
}
const lid = parse(l.id || l.locationId, {delimiter: '@'});
let res = {
const res = {
type: 'location',
id: (l.extId || l.evaNr || lid.L || l.evaNumber || l.evaNo || l.bahnhofsId || '').replace(leadingZeros, '') || null,
};
@ -46,7 +46,13 @@ const parseLocation = (ctx, l) => {
stop.products = profile.parseProducts(ctx, l.products);
}
stop = profile.enrichStation(ctx, stop);
if (common && common.locations && common.locations[stop.id]) {
delete stop.type;
stop = {
...common.locations[stop.id],
...stop,
};
}
// TODO isMeta
// TODO entrances, lines
@ -64,8 +70,6 @@ const parseLocation = (ctx, l) => {
}
res.name = name;
res = enrichStation(ctx, res);
if (l.type === ADDRESS || lid.A == '2') {
res.address = name;
}
@ -76,22 +80,6 @@ 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,
};
}
return stop;
};
export {
parseLocation,
enrichStation,
};

View file

@ -37,11 +37,11 @@ const parseRemarks = (ctx, ref) => {
let res = {
code: remark.code || remark.key || remark.id,
summary: remark.nachrichtKurz || remark.value || remark.ueberschrift || remark.text || remark.shortText
|| Object.values(remark.descriptions || {})
.shift()?.textShort,
|| Object.values(remark.descriptions || {})
.shift()?.textShort,
text: remark.nachrichtLang || remark.value || remark.text || remark.caption
|| Object.values(remark.descriptions || {})
.shift()?.text,
|| Object.values(remark.descriptions || {})
.shift()?.text,
type: type,
};
if (remark.modDateTime || remark.letzteAktualisierung) {
@ -208,9 +208,9 @@ const parseCancelled = (ref) => {
|| 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',
|| r.type == 'HALT_AUSFALL'
|| r.text == 'Halt entfällt'
|| r.text == 'Stop cancelled',
),
);
};

View file

@ -5,7 +5,7 @@ import tap from 'tap';
import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbnav/index.js';
import res from './fixtures/dbnav-departures.json' with { type: 'json' };
import res from './fixtures/dbnav-departures.json' with { type: "json" };
import {dbnavDepartures as expected} from './fixtures/dbnav-departures.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -5,7 +5,7 @@ import tap from 'tap';
import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbnav/index.js';
import res from './fixtures/dbnav-refresh-journey.json' with { type: 'json' };
import res from './fixtures/dbnav-refresh-journey.json' with { type: "json" };
import {dbNavJourney as expected} from './fixtures/dbnav-refresh-journey.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -5,7 +5,7 @@ import tap from 'tap';
import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbnav/index.js';
import res from './fixtures/dbnav-stop.json' with { type: 'json' };
import res from './fixtures/dbnav-stop.json' with { type: "json" };
import {dbnavDepartures as expected} from './fixtures/dbnav-stop.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -4,7 +4,7 @@ import tap from 'tap';
import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbnav/index.js';
import res from './fixtures/dbnav-trip.json' with { type: 'json' };
import res from './fixtures/dbnav-trip.json' with { type: "json" };
import {dbTrip as expected} from './fixtures/dbnav-trip.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

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

View file

@ -4,7 +4,7 @@ import tap from 'tap';
import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbweb/index.js';
import res from './fixtures/dbris-arrivals.json' with { type: 'json' };
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});

View file

@ -4,10 +4,10 @@ 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 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 client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: true});
const {profile} = client;
const opt = {

View file

@ -4,7 +4,7 @@ import tap from 'tap';
import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbweb/index.js';
import res from './fixtures/dbweb-journey.json' with { type: 'json' };
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});

View file

@ -4,7 +4,7 @@ import tap from 'tap';
import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbweb/index.js';
import res from './fixtures/dbweb-refresh-journey.json' with { type: 'json' };
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});

View file

@ -4,7 +4,7 @@ import tap from 'tap';
import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbweb/index.js';
import res from './fixtures/dbweb-trip.json' with { type: 'json' };
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});

View file

@ -395,8 +395,17 @@ tap.test('trip details', async (t) => {
});
tap.test('departures at Berlin Schwedter Str.', async (t) => {
const res = await client.departures(blnSchwedterStr, {
duration: 5, when,
const res = await new Promise((resolve) => {
let interval = setInterval(async () => { // repeat evaluating `departures()` until stations are enriched
const res = await client.departures(blnSchwedterStr, {
duration: 5, when,
});
if (res.departures[0].stop.name !== undefined) { // ctx.common.locations have loaded
clearInterval(interval);
return resolve(res);
}
}, 4000);
});
await testDepartures({
@ -409,24 +418,42 @@ tap.test('departures at Berlin Schwedter Str.', async (t) => {
});
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});
const res = await new Promise((resolve) => {
let interval = setInterval(async () => { // repeat evaluating `departures()` until stations are enriched
const res = await client.departures({
type: 'station',
id: jungfernheide,
name: 'Berlin Jungfernheide',
location: {
type: 'location',
latitude: 1.23,
longitude: 2.34,
},
}, {when});
if (res.departures[0].stop.name !== undefined) { // ctx.common.locations have loaded
clearInterval(interval);
return resolve(res);
}
}, 4000);
});
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,
const res = await new Promise((resolve) => {
let interval = setInterval(async () => { // repeat evaluating `arrivals()` until stations are enriched
const res = await client.arrivals(blnSchwedterStr, {
duration: 5, when,
});
if (res.arrivals[0].stop.name !== undefined) { // ctx.common.locations have loaded
clearInterval(interval);
return resolve(res);
}
}, 4000);
});
await testArrivals({

View file

@ -4,7 +4,6 @@ import {parseProducts} from '../../parse/products.js';
const profile = {
parseLocation: parse,
enrichStation: (ctx, stop) => stop,
parseStationName: (_, name) => name.toLowerCase(),
parseProducts,
products: [{