dbnav profile: locations, nearby

This commit is contained in:
Traines 2024-12-21 23:04:05 +00:00
parent ad6c356552
commit debc1ee150
16 changed files with 167 additions and 34 deletions

1
api.js
View file

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

View file

@ -34,6 +34,9 @@ const formatProductsFilter = (ctx, filter, key = 'vendo') => {
if (!foundDeselected && key == 'ris') {
return undefined;
}
if (!foundDeselected && key == 'dbnav') {
return ['ALL'];
}
return products;
};

View file

@ -15,25 +15,6 @@ const formatStationBoardReq = (ctx, station, type) => {
};
};
/*
TODO separate DB Nav profile?
const formatStationBoardReq = (ctx, station, type) => {
const { profile, opt } = ctx;
return {
endpoint: profile.boardEndpoint,
path: type == 'departures' ? 'abfahrt' : 'ankunft',
body: { "anfragezeit": profile.formatTime(profile, opt.when), "datum": profile.formatDate(profile, opt.when), "ursprungsBahnhofId": profile.formatStation(station).lid, "verkehrsmittel": profile.formatProductsFilter(ctx, opt.products || {}, 'dbnav') },
method: 'POST',
header: {
'Accept': 'application/x.db.vendo.mob.bahnhofstafeln.v2+json',
'X-Correlation-ID': 'null',
'Content-Type': 'application/x.db.vendo.mob.bahnhofstafeln.v2+json'
}
};
};
*/
/*
TODO separate RIS::Boards profile?
const formatRisStationBoardReq = (ctx, station, type) => {

View file

@ -104,7 +104,8 @@ 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).map(res => parse(ctx, res)); // todo sort?
const results = (res[resultsField] || res.items || res.bahnhofstafelAbfahrtPositionen || res.bahnhofstafelAnkunftPositionen)
.map(res => parse(ctx, res)); // TODO sort?, slice
return {
[resultsField]: results,
@ -189,7 +190,7 @@ 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 = opt.results ? res.verbindungen.slice(0, opt.results) : res.verbindungen;
const verbindungen = Number.isInteger(opt.results) ? res.verbindungen.slice(0, opt.results) : res.verbindungen;
const journeys = verbindungen
.map(j => profile.parseJourney(ctx, j));
@ -246,7 +247,11 @@ const createClient = (profile, userAgent, opt = {}) => {
const {res} = await profile.request({profile, opt}, userAgent, req);
const ctx = {profile, opt, common, res};
return res.map(loc => profile.parseLocation(ctx, loc));
const results = res.map(loc => profile.parseLocation(ctx, loc));
return Number.isInteger(opt.results)
? results.slice(0, opt.results)
: results;
};
const stop = async (stop, opt = {}) => { // TODO

View file

@ -100,7 +100,7 @@ const request = async (ctx, userAgent, reqData) => {
const endpoint = reqData.endpoint;
delete reqData.endpoint;
const rawReqBody = profile.transformReqBody(ctx, reqData.body);
// console.log(rawReqBody, JSON.stringify(rawReqBody.req.reisende));
const req = profile.transformReq(ctx, {
agent: getAgent(),
method: reqData.method,
@ -121,7 +121,10 @@ const request = async (ctx, userAgent, reqData) => {
query: reqData.query,
});
const url = endpoint + (reqData.path || '') + '?' + stringify(req.query, {arrayFormat: 'brackets', encodeValuesOnly: true});
const url = endpoint + (reqData.path || '');
if (query) {
url += '?' + stringify(req.query, {arrayFormat: 'brackets', encodeValuesOnly: true});
}
const reqId = randomBytes(3)
.toString('hex');
const fetchReq = new Request(url, req);
@ -147,7 +150,7 @@ const request = async (ctx, userAgent, reqData) => {
let cType = res.headers.get('content-type');
if (cType) {
const {type} = parseContentType(cType);
if (type !== 'application/json' && type !== 'application/vnd.de.db.ris+json') {
if (type !== req.headers['Accept']) {
throw new HafasError('invalid/unsupported response content-type: ' + cType, null, errProps);
}
}

View file

@ -5,7 +5,7 @@ const formatJourneysReq = (ctx, from, to, when, outFrwd, journeysRef) => {
to = profile.formatLocation(profile, to, 'to');
const filters = profile.formatProductsFilter({profile}, opt.products || {});
// TODO opt.accessibility
// TODO routingMode
let query = {
maxUmstiege: opt.transfers,
minUmstiegszeit: opt.transferTime,

10
p/dbnav/base.json Normal file
View file

@ -0,0 +1,10 @@
{
"journeysEndpoint": "https://app.vendo.noncd.db.de/mob/angebote/fahrplan",
"refreshJourneysEndpointPrice": "https://app.vendo.noncd.db.de/mob/angebote/recon/autonomereservierung",
"refreshJourneysEndpointPolyline": "https://app.vendo.noncd.db.de/mob/trip/recon",
"locationsEndpoint": "https://app.vendo.noncd.db.de/mob/location/search",
"nearbyEndpoint": "https://app.vendo.noncd.db.de/mob/location/nearby",
"tripEndpoint": "https://app.vendo.noncd.db.de/mob/zuglauf",
"boardEndpoint": "https://app.vendo.noncd.db.de/mob/bahnhofstafel/abfahrt",
"defaultLanguage": "en"
}

11
p/dbnav/header.js Normal file
View file

@ -0,0 +1,11 @@
const getHeaders = (contentType) => {
return {
'X-Correlation-ID': 'null',
'Accept': contentType,
'Content-Type': contentType,
};
};
export {
getHeaders,
};

33
p/dbnav/index.js Normal file
View file

@ -0,0 +1,33 @@
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
const baseProfile = require('./base.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 {formatNearbyReq} from './nearby-req.js';
import {formatStationBoardReq} from './station-board-req.js';
// import {formatTravellers} from './travellers.js';
// import {parseTickets, parsePrice} from './tickets.js';
const profile = {
...baseProfile,
locale: 'de-DE',
timezone: 'Europe/Berlin',
products,
// formatJourneysReq,
// formatRefreshJourneyReq,
formatNearbyReq,
formatLocationsReq,
formatStationBoardReq,
formatLocationFilter,
// parsePrice,
// parseTickets,
// formatTravellers,
};
export {
profile,
};

View file

@ -0,0 +1,20 @@
const formatLocationFilter = (stops, addresses, poi) => {
if (stops && addresses && poi) {
return ['ALL'];
}
const types = [];
if (stops) {
types.push('ST');
}
if (addresses) {
types.push('ADR');
}
if (poi) {
types.push('POI');
}
return types;
};
export {
formatLocationFilter,
};

20
p/dbnav/locations-req.js Normal file
View file

@ -0,0 +1,20 @@
import {getHeaders} from './header.js';
const formatLocationsReq = (ctx, query) => {
const {profile, opt} = ctx;
return {
endpoint: profile.locationsEndpoint,
body: {
locationTypes: profile.formatLocationFilter(opt.stops, opt.addresses, opt.poi),
searchTerm: query,
maxResults: opt.results,
},
headers: getHeaders('application/x.db.vendo.mob.location.v3+json'),
method: 'post',
};
};
export {
formatLocationsReq,
};

29
p/dbnav/nearby-req.js Normal file
View file

@ -0,0 +1,29 @@
import {getHeaders} from './header.js';
const formatNearbyReq = (ctx, location) => {
const {profile, opt} = ctx;
if (opt.distance > 10000) {
throw new Error('maximum supported distance by this endpoint is 10000');
}
// TODO location types
return {
endpoint: profile.nearbyEndpoint,
body: {
area: {
coordinates: {
longitude: location.longitude,
latitude: location.latitude,
},
radius: opt.distance || 10000,
},
maxResults: opt.results,
products: profile.formatProductsFilter(ctx, opt.products || {}, 'dbnav'),
},
headers: getHeaders('application/x.db.vendo.mob.location.v3+json'),
method: 'post',
};
};
export {
formatNearbyReq,
};

View file

@ -0,0 +1,17 @@
import {getHeaders} from './header.js';
const formatStationBoardReq = (ctx, station, type) => {
const {profile, opt} = ctx;
return {
endpoint: profile.boardEndpoint,
path: type == 'departures' ? 'abfahrt' : 'ankunft',
body: {anfragezeit: profile.formatTimeOfDay(profile, opt.when), datum: profile.formatDate(profile, opt.when), ursprungsBahnhofId: profile.formatStation(station).lid, verkehrsmittel: profile.formatProductsFilter(ctx, opt.products || {}, 'dbnav')},
method: 'POST',
header: getHeaders('application/x.db.vendo.mob.bahnhofstafeln.v2+json'),
};
};
export {
formatStationBoardReq,
};

View file

@ -13,16 +13,16 @@ const parseLocation = (ctx, l) => {
return null;
}
const lid = parse(l.id, {delimiter: '@'});
const lid = parse(l.id || l.locationId, {delimiter: '@'});
const res = {
type: 'location',
id: (l.extId || lid.L || l.evaNumber || l.evaNo || '').replace(leadingZeros, '') || null,
id: (l.extId || l.evaNr || lid.L || l.evaNumber || l.evaNo || '').replace(leadingZeros, '') || null,
};
const name = l.name || lid.O;
if (l.lat && l.lon) {
res.latitude = l.lat;
res.longitude = l.lon;
if (l.lat && l.lon || l.coordinates || l.position) {
res.latitude = l.lat || l.coordinates?.latitude || l.position?.latitude;
res.longitude = l.lon || l.coordinates?.longitude || l.position?.longitude;
} else if ('X' in lid && 'Y' in lid) {
res.latitude = lid.Y / 1000000;
res.longitude = lid.X / 1000000;

View file

@ -1,7 +1,7 @@
const parseProducts = ({profile}, bitmask) => {
const parseProducts = ({profile}, products) => {
const res = {};
for (let product of profile.products) {
res[product.id] = Boolean(bitmask.find(p => p == product.vendo));
res[product.id] = Boolean(products.find(p => p == product.vendo || p == product.dbnav));
}
return res;
};