From debc1ee150977c6062f1190f843b3669474c74f5 Mon Sep 17 00:00:00 2001 From: Traines Date: Sat, 21 Dec 2024 23:04:05 +0000 Subject: [PATCH] dbnav profile: locations, nearby --- api.js | 1 + format/products-filter.js | 3 +++ format/station-board-req.js | 19 ------------------ index.js | 11 ++++++++--- lib/request.js | 9 ++++++--- p/db/journeys-req.js | 2 +- p/dbnav/base.json | 10 ++++++++++ p/dbnav/header.js | 11 +++++++++++ p/dbnav/index.js | 33 ++++++++++++++++++++++++++++++++ p/dbnav/location-filter.js | 20 +++++++++++++++++++ p/dbnav/locations-req.js | 20 +++++++++++++++++++ p/dbnav/nearby-req.js | 29 ++++++++++++++++++++++++++++ p/dbnav/station-board-req.js | 17 ++++++++++++++++ parse/location.js | 10 +++++----- parse/products.js | 4 ++-- test/format/db-journeys-query.js | 2 +- 16 files changed, 167 insertions(+), 34 deletions(-) create mode 100644 p/dbnav/base.json create mode 100644 p/dbnav/header.js create mode 100644 p/dbnav/index.js create mode 100644 p/dbnav/location-filter.js create mode 100644 p/dbnav/locations-req.js create mode 100644 p/dbnav/nearby-req.js create mode 100644 p/dbnav/station-board-req.js diff --git a/api.js b/api.js index 25b77b34..0001bfd5 100644 --- a/api.js +++ b/api.js @@ -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; diff --git a/format/products-filter.js b/format/products-filter.js index 1894b94f..e3362e77 100644 --- a/format/products-filter.js +++ b/format/products-filter.js @@ -34,6 +34,9 @@ const formatProductsFilter = (ctx, filter, key = 'vendo') => { if (!foundDeselected && key == 'ris') { return undefined; } + if (!foundDeselected && key == 'dbnav') { + return ['ALL']; + } return products; }; diff --git a/format/station-board-req.js b/format/station-board-req.js index 604bb557..acc93fb5 100644 --- a/format/station-board-req.js +++ b/format/station-board-req.js @@ -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) => { diff --git a/index.js b/index.js index d55492b1..d49de882 100644 --- a/index.js +++ b/index.js @@ -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 diff --git a/lib/request.js b/lib/request.js index c6ba6d1c..188e0b4f 100644 --- a/lib/request.js +++ b/lib/request.js @@ -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); } } diff --git a/p/db/journeys-req.js b/p/db/journeys-req.js index 05ffc55d..453646e2 100644 --- a/p/db/journeys-req.js +++ b/p/db/journeys-req.js @@ -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, diff --git a/p/dbnav/base.json b/p/dbnav/base.json new file mode 100644 index 00000000..bd4f9cc5 --- /dev/null +++ b/p/dbnav/base.json @@ -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" +} diff --git a/p/dbnav/header.js b/p/dbnav/header.js new file mode 100644 index 00000000..1df515a3 --- /dev/null +++ b/p/dbnav/header.js @@ -0,0 +1,11 @@ +const getHeaders = (contentType) => { + return { + 'X-Correlation-ID': 'null', + 'Accept': contentType, + 'Content-Type': contentType, + }; +}; + +export { + getHeaders, +}; diff --git a/p/dbnav/index.js b/p/dbnav/index.js new file mode 100644 index 00000000..45fafa7b --- /dev/null +++ b/p/dbnav/index.js @@ -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, +}; diff --git a/p/dbnav/location-filter.js b/p/dbnav/location-filter.js new file mode 100644 index 00000000..c8c18ee5 --- /dev/null +++ b/p/dbnav/location-filter.js @@ -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, +}; diff --git a/p/dbnav/locations-req.js b/p/dbnav/locations-req.js new file mode 100644 index 00000000..068fa0b8 --- /dev/null +++ b/p/dbnav/locations-req.js @@ -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, +}; diff --git a/p/dbnav/nearby-req.js b/p/dbnav/nearby-req.js new file mode 100644 index 00000000..0d3b5ffb --- /dev/null +++ b/p/dbnav/nearby-req.js @@ -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, +}; diff --git a/p/dbnav/station-board-req.js b/p/dbnav/station-board-req.js new file mode 100644 index 00000000..f99744c0 --- /dev/null +++ b/p/dbnav/station-board-req.js @@ -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, +}; diff --git a/parse/location.js b/parse/location.js index ad88f8fe..8c2073c3 100644 --- a/parse/location.js +++ b/parse/location.js @@ -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; diff --git a/parse/products.js b/parse/products.js index 1f198359..ee9106a0 100644 --- a/parse/products.js +++ b/parse/products.js @@ -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; }; diff --git a/test/format/db-journeys-query.js b/test/format/db-journeys-query.js index e478b322..7ad0367b 100644 --- a/test/format/db-journeys-query.js +++ b/test/format/db-journeys-query.js @@ -12,7 +12,7 @@ const opt = { via: null, stopovers: false, transfers: null, - transferTime: 0, + transferTime: 0, accessibility: 'none', bike: false, walkingSpeed: 'normal',