UNPKG

mobility-toolbox-js

Version:

Toolbox for JavaScript applications in the domains of mobility and logistics.

580 lines (579 loc) 23.5 kB
import debounceWebsocketMessages from '../common/utils/debounceWebsocketMessages'; import getModeSuffix from '../common/utils/getRealtimeModeSuffix'; import WebSocketAPI from './WebSocketAPI'; /** * Enum for Realtime modes. * @readonly * @typedef {string} RealtimeMode * @property {string} RAW "raw" * @property {string} SCHEMATIC "schematic" * @property {string} TOPOGRAPHIC "topographic" * @enum {RealtimeMode} * @public */ export const RealtimeModes = { RAW: 'raw', SCHEMATIC: 'schematic', TOPOGRAPHIC: 'topographic', }; /** * This class provides convenience methods to use to the [geOps Realtime API](https://developer.geops.io/apis/realtime/). * * @example * import { RealtimeAPI } from 'mobility-toolbox-js/api'; * * const api = new RealtimeAPI({ * apiKey: "yourApiKey", * bbox: [782001, 5888803, 923410, 5923660, 11, "mots=rail"], * // url: "wss://api.geops.io/tracker-ws/v1/", * }); * * // Open the websocket connection * api.open(); * * // Subscribe to channel * api.subscribeTrajectory('topographic', (data) => { * console.log('Log trajectories:', JSON.stringify(data.content)); * }); * * // Close the websocket connection * api.close(); * * @public */ class RealtimeAPI { /** * This callback type is called `requestCallback` and is displayed as a global symbol. * * @callback onFullTrajectoryMessageCallback * @param {number} responseCode * @param {string} responseMessage */ /** * The bounding box to receive data from.\ * Example: [minX, minY, maxX, maxY, zoom, mots , gen_level, tenant, ...]\ * &nbsp;\ * Where: * - **minX**: a string representation of an integer (not a float) representing the minimal X coordinate (in EPSG:3857) of a bounding box\ * &nbsp; * - **minY**: a string representation of an integer (not a float) representing the minimal Y coordinate (in EPSG:3857) of a bounding box\ * &nbsp; * - **maxX**: a string representation of an integer (not a float) representing the maximal X coordinate (in EPSG:3857) of a bounding box\ * &nbsp; * - **maxY**: a string representation of an integer (not a float) representing the maximal Y coordinate (in EPSG:3857) of a bounding box\ * &nbsp; * - **zoom**: a string representation of an integer representing the zoom level (from 4 to 22). When zoom < 8 only the trains are displayed for performance reasons.\ * &nbsp; * - **mots**: A comma separated list of modes of transport. **Optional**.\ * Example: "mots=rail,subway".\ * &nbsp; * - **gen_level**: An integer representing the generalization level. **Optional**.\ * Example: "gen_level=5"\ * &nbsp; * - **tenant**: A string representing the tenant. **Optional**.\ * Example: "tenant=sbb"\ * &nbsp; * - ...: Any other values added to the bbox will be send to the server * * @type {string[]} * * @public */ get bbox() { return this._bbox; } set bbox(newBbox) { if (JSON.stringify(newBbox) !== JSON.stringify(this._bbox)) { this._bbox = newBbox; if (this.wsApi && this._bbox) { this.wsApi.send(`BBOX ${this._bbox.join(' ')}`); } } } get buffer() { return this._buffer; } set buffer(newBuffer) { if (JSON.stringify(newBuffer) !== JSON.stringify(this._buffer)) { this._buffer = newBuffer; if (this.wsApi && this._buffer) { this.wsApi.send(`BUFFER ${this._buffer.join(' ')}`); } } } get url() { return this._url; } set url(newUrl) { if (this._url !== newUrl) { this._url = newUrl; // Update the websocket only if the url has changed and the websocket is already open or is opening. if (this.wsApi.open || this.wsApi.connecting) { this.open(); } } } /** * Constructor * * @param {Object} options Options. * @param {string} options.apiKey Access key for [geOps apis](https://developer.geops.io/). * @param {string[]} options.bbox The bounding box to receive data from. * @param {string} [options.url='wss://api.geops.io/tracker-ws/v1/'] Url of the [geOps Realtime API](https://developer.geops.io/apis/realtime/). * @public */ constructor(options = {}) { this.version = '2'; let opt = options; if (typeof options === 'string') { opt = { url: options }; } const { apiKey } = opt; const { url } = opt; const wsApi = new WebSocketAPI(); let suffix = ''; if (apiKey && !(url === null || url === void 0 ? void 0 : url.includes('key='))) { suffix = `?key=${apiKey}`; } this._url = (url || 'wss://api.geops.io/tracker-ws/v1/') + suffix; this._buffer = opt.buffer || [100, 100]; this._bbox = opt.bbox; this.version = opt.version || '2'; /** * Interval between PING request in ms. * If equal to 0, no PING request are sent. * @type {number} */ this.pingIntervalMs = opt.pingIntervalMs || 10000; /** * Timeout in ms after an automatic reconnection when the websoscket has been closed by the server. * @type {number} */ this.reconnectTimeoutMs = opt.reconnectTimeoutMs || 100; /** * The websocket helper class to connect the websocket. */ this.wsApi = wsApi; } /** * Close the websocket connection without reconnection. * * @public */ close() { this.wsApi.close(); } /** * Send GET to a channel. * * @param {string | WebSocketAPIParameters} channelOrParams Name of the websocket channel to send GET or an object representing parameters to send * @return {Promise<WebSocketAPIMessageEventData<?>>} A websocket response. * @public */ get(channelOrParams) { let params = channelOrParams; if (typeof channelOrParams === 'string') { params = { channel: channelOrParams }; } return new Promise((resolve, reject) => { this.wsApi.get(params, resolve, reject); }); } /** * Get a full trajectory of a vehicule . * * @param {string} id A vehicle id. * @param {RealtimeMode} mode Realtime mode. * @param {string} generalizationLevel The generalization level to request. Can be one of 5 (more generalized), 10, 30, 100, undefined (less generalized). * @return {Promise<{data: { content: RealtimeFullTrajectoryCollection }}>} Return a full trajectory. * @public */ getFullTrajectory(id, mode, generalizationLevel) { let suffix = ''; if (this.version === '1') { suffix = getModeSuffix(mode, RealtimeModes); } const channel = [`full_trajectory${suffix}`]; if (id) { channel.push(id); } if ((!mode || mode === RealtimeModes.TOPOGRAPHIC) && generalizationLevel) { channel.push(`gen${generalizationLevel}`); } return this.get(channel.join('_')); } /** * Return a station with a given uic number and a mode. * * @param {number} uic UIC of the station. * @param {RealtimeMode} mode Realtime mode. * @return {Promise<{data: { content: RealtimeStation }}>} A station. * @public */ getStation(uic, mode) { const params = { args: uic, channel: `station${getModeSuffix(mode, RealtimeModes)}`, }; return this.get(params); } /** * Get the list of ststions available for a specifc mode. The promise is resolved every 100ms * @param {RealtimeMode} mode Realtime mode. * @param {number} timeout = 100 Duration in ms between each promise resolve calls. * @return {Promise<RealtimeStation[]>} An array of stations. * @public */ getStations(mode, timeout = 100) { return new Promise((resolve, reject) => { this.get(`station${getModeSuffix(mode, RealtimeModes)}`) // @ts-expect-error check this .then(debounceWebsocketMessages(resolve, undefined, timeout)) .catch(reject); }); } /** * Get the list of stops for this vehicle. * * @param {string} id A vehicle id. * @return {Promise<{data: { content: RealtimeStopSequence[] }}>} Returns a stop sequence object. * @public */ getStopSequence(id) { return this.get(`stopsequence_${id}`); } /** * Return a partial trajectory with a given id and a mode. * * @param {number} id The identifier of a trajectory. * @param {RealtimeMode} mode Realtime mode. * @return {Promise<{data: { content: RealtimeTrajectory }}>} A trajectory. * @public */ getTrajectory(id, mode) { return this.get(`partial_trajectory${getModeSuffix(mode, RealtimeModes)}_${id}`); } /** * Callback when the websocket is closed by the server. * It auto reconnects after a timeout. * @private */ onClose() { window.clearTimeout(this.pingInterval); window.clearTimeout(this.reconnectTimeout); if (this.reconnectTimeoutMs) { this.reconnectTimeout = window.setTimeout(() => { return this.open(); }, this.reconnectTimeoutMs); } } /** * Callback when the websocket is opened and ready. * It applies the bbox and the projection. * @private */ onOpen() { if (this.bbox) { this.wsApi.send(`BBOX ${this.bbox.join(' ')}`); } if (this.buffer) { this.wsApi.send(`BUFFER ${this.buffer.join(' ')}`); } /** * Keep websocket alive */ if (this.pingIntervalMs) { window.clearInterval(this.pingInterval); this.pingInterval = window.setInterval(() => { this.wsApi.send('PING'); }, this.pingIntervalMs); } } /** * Open the websocket connection. * * @public */ open() { this.wsApi.connect(this.url, this.onOpen.bind(this)); // Register reconnection on close. if (this.wsApi.websocket) { this.wsApi.websocket.onclose = () => { this.onClose(); }; } } /** * Unsubscribe trajectory and deleted_vehicles channels. To resubscribe you have to set a new BBOX. */ reset() { this.wsApi.send('RESET'); } /** * Subscribe to a channel. * * @param {string} channel Name of the websocket channel to subscribe. * @param {function} onSuccess Callback when the subscription succeeds. * @param {function} onError Callback when the subscription fails. * @param {boolean} [quiet=false] If true avoid to store the subscription in the subscriptions list. * @public */ subscribe(channel, onSuccess, onError = () => { }, quiet = false) { if (!channel || !onSuccess) { return; } this.wsApi.subscribe({ channel }, onSuccess, onError, quiet); } /** * Subscribe to deleted_vhicles channel. * * @param {RealtimeMode} mode Realtime mode. * @param {function(data: { content: RealtimeTrainId })} onMessage Callback function to unsubscribe. If null all subscriptions for the channel will be unsubscribed. * @param {function} onError Callback when the subscription fails. * @param {boolean} [quiet=false] If true avoid to store the subscription in the subscriptions list. * @public */ subscribeDeletedVehicles(mode, onMessage, onError = () => { }, quiet = false) { this.unsubscribeDeletedVehicles(onMessage); let suffix = ''; if (this.version === '1') { suffix = getModeSuffix(mode, RealtimeModes); } this.subscribe(`deleted_vehicles${suffix}`, onMessage, onError, quiet); } /** * Subscribe to departures channel of a given station. * * @param {number} stationId UIC of the station. * @param {function(departures: RealtimeDeparture[])} onMessage Function called on each message of the channel. * @param {function} onError Callback when the subscription fails. * @param {boolean} [quiet=false] If true avoid to store the subscription in the subscriptions list. * @deprecated Use subscribeTimetable instead. */ subscribeDepartures(stationId, onMessage, onError = () => { }, quiet = false) { this.subscribeTimetable(stationId, onMessage, onError, quiet); } /** * Subscribe to the disruptions channel for tenant. * * @param {RealtimeTenant} tenant Tenant's id * @param {function(data: { content: RealtimeNews[] })} onMessage Function called on each message of the channel. * @param {function} onError Callback when the subscription fails. * @param {boolean} [quiet=false] If true avoid to store the subscription in the subscriptions list. * @deprecated Use subscribeNewsticker instead. */ subscribeDisruptions(tenant, onMessage, onError = () => { }, quiet = false) { this.subscribeNewsticker(tenant, onMessage, onError, quiet); } /** * Subscribe to extra_geoms channel. * * @param {function(data: { content: RealtimeExtraGeom })} onMessage Function called on each message of the channel. * @param {function} onError Callback when the subscription fails. * @param {boolean} [quiet=false] If true avoid to store the subscription in the subscriptions list. */ subscribeExtraGeoms(onMessage, onError = () => { }, quiet = false) { this.subscribe('extra_geoms', onMessage, onError, quiet); } /** * Subscribe to full_trajectory channel of a given vehicle. * * @param {string} id A vehicle id. * @param {RealtimeMode} mode Realtime mode. * @param {function(data:{content: RealtimeFullTrajectoryCollection})} onMessage Function called on each message of the channel. * @param {function} onError Callback when the subscription fails. * @param {boolean} [quiet=false] If true avoid to store the subscription in the subscriptions list. * @public */ subscribeFullTrajectory(id, mode, onMessage, onError = () => { }, quiet = false) { let suffix = ''; if (this.version === '1') { suffix = getModeSuffix(mode, RealtimeModes); } this.subscribe(`full_trajectory${suffix}_${id}`, onMessage, onError, quiet); } /** * Subscribe to healthcheck channel. * @param {function(data: { content: string })} onMessage Callback when the subscribe to healthcheck channel succeeds. * @param {function} onError Callback when the subscription fails. * @param {boolean} [quiet=false] If true avoid to store the subscription in the subscriptions list. */ subscribeHealthCheck(onMessage, onError = () => { }, quiet = false) { this.subscribe('healthcheck', onMessage, onError, quiet); } /** * Subscribe to the newsticker channel for tenant. * * @param {RealtimeTenant} tenant Tenant's id * @param {function(data: { content: RealtimeNews[] })} onMessage Function called on each message of the channel. * @param {function} onError Callback when the subscription fails. * @param {boolean} [quiet=false] If true avoid to store the subscription in the subscriptions list. * @public */ subscribeNewsticker(tenant, onMessage, onError = () => { }, quiet = false) { this.subscribe(`${tenant}_newsticker`, onMessage, onError, quiet); } /** * Subscribe to stations channel. * One message pro station. * * @param {RealtimeMode} mode Realtime mode. * @param {function(data: { content: RealtimeStation })} onMessage Function called on each message of the channel. * @param {function} onError Callback when the subscription fails. * @param {boolean} [quiet=false] If true avoid to store the subscription in the subscriptions list. * @public */ subscribeStations(mode, onMessage, onError = () => { }, quiet = false) { this.subscribe(`station${getModeSuffix(mode, RealtimeModes)}`, onMessage, onError, quiet); } /** * Subscribe to stopsequence channel of a given vehicle. * * @param {string} id A vehicle id. * @param {function(data: { content: RealtimeStopSequence[] })} onMessage Function called on each message of the channel. * @param {function} onError Callback when the subscription fails. * @param {boolean} [quiet=false] If true avoid to store the subscription in the subscriptions list. * @public */ subscribeStopSequence(id, onMessage, onError = () => { }, quiet = false) { this.subscribe(`stopsequence_${id}`, onMessage, onError, quiet); } /** * Subscribe to timetable channel of a given station. * * @param {number} stationId UIC of the station. * @param {function(departures: RealtimeDeparture[])} onMessage Function called on each message of the channel. * @param {function} onError Callback when the subscription fails. * @param {boolean} [quiet=false] If true avoid to store the subscription in the subscriptions list. * @public */ subscribeTimetable(stationId, onMessage, onError = () => { }, quiet = false) { this.subscribe(`timetable_${stationId}`, onMessage, onError, quiet); } /** * Subscribe to trajectory channel. * * @param {RealtimeMode} mode Realtime mode. * @param {function(data: { content: RealtimeTrajectory })} onMessage Function called on each message of the channel. * @param {function} onError Callback when the subscription fails. * @param {boolean} [quiet=false] If true avoid to store the subscription in the subscriptions list. * @public */ subscribeTrajectory(mode, onMessage, onError = () => { }, quiet = false) { this.unsubscribeTrajectory(onMessage); let suffix = ''; if (this.version === '1') { suffix = getModeSuffix(mode, RealtimeModes); } this.subscribe(`trajectory${suffix}`, onMessage, onError, quiet); } /** * Unsubscribe both modes of a channel. * * @param {string} channel Name of the websocket channel to unsubscribe. * @param {string} suffix Suffix to add to the channel name. * @param {function} onMessage Callback function to unsubscribe. If null all subscriptions for the channel will be unsubscribed. * @public */ unsubscribe(channel, suffix = '', onMessage) { const suffixSchenatic = getModeSuffix(RealtimeModes.SCHEMATIC, RealtimeModes); const suffixTopographic = getModeSuffix(RealtimeModes.TOPOGRAPHIC, RealtimeModes); this.wsApi.unsubscribe(`${channel}${suffixSchenatic}${suffix || ''}`, onMessage); this.wsApi.unsubscribe(`${channel}${suffixTopographic}${suffix || ''}`, onMessage); } /** * Unsubscribe to deleted_vhicles channels. * @param {function(data: { content: RealtimeTrainId })} onMessage Callback function to unsubscribe. If null all subscriptions for the channel will be unsubscribed. * @public */ unsubscribeDeletedVehicles(onMessage) { this.unsubscribe('deleted_vehicles', '', onMessage); } /** * Unsubscribe from current departures channel. * @param {number} stationId UIC of the station. * @param {function(data: { content: RealtimeDeparture[] })} onMessage Callback function to unsubscribe. If null all subscriptions for the channel will be unsubscribed. * @deprecated Use RealtimeAPI.unsubscribeTimetabe instead. */ unsubscribeDepartures(stationId, onMessage) { this.unsubscribeTimetable(stationId, onMessage); } /** * Unsubscribe disruptions. * @param {RealtimeTenant} tenant Tenant's id * @param {Function(data: { content: RealtimeNews[] })} onMessage Callback function to unsubscribe. If null all subscriptions for the channel will be unsubscribed. * @deprecated Use unsubscribeNewsticker instead. */ unsubscribeDisruptions(tenant, onMessage) { this.unsubscribeNewsticker(tenant, onMessage); } /** * Unsubscribe to extra_geoms channel. * @param {function(data: { content: RealtimeExtraGeom })} onMessage Callback function to unsubscribe. If null all subscriptions for the channel will be unsubscribed. */ unsubscribeExtraGeoms(onMessage) { this.unsubscribe('extra_geoms', '', onMessage); } /** * Unsubscribe from full_trajectory channel * * @param {string} id A vehicle id. * @param {onFullTrajectoryMessageCallback} onMessage Callback function to unsubscribe. If null all subscriptions for the channel will be unsubscribed. * @public */ unsubscribeFullTrajectory(id, onMessage) { this.unsubscribe('full_trajectory', `_${id}`, onMessage); } /** * Unsubscribe to healthcheck channel. * @param {function(data: { content: string })} onMessage Callback function to unsubscribe. If null all subscriptions for the channel will be unsubscribed. */ unsubscribeHealthCheck(onMessage) { this.unsubscribe('healthcheck', '', onMessage); } /** * Unsubscribe disruptions. * @param {RealtimeTenant} tenant Tenant's id * @param {Function(data: { content: RealtimeNews[] })} onMessage Callback function to unsubscribe. If null all subscriptions for the channel will be unsubscribed. * @public */ unsubscribeNewsticker(tenant, onMessage) { this.unsubscribe(`${tenant}_newsticker`, '', onMessage); } /** * Unsubscribe to stations channel. * @param {function(data: { content: RealtimeStation })} onMessage The listener callback function to unsubscribe. If null all subscriptions for the channel will be unsubscribe. * @public */ unsubscribeStations(onMessage) { this.unsubscribe('station', '', onMessage); } /** * Unsubscribe from stopsequence channel * * @param {string} id A vehicle id. * @param {function(data: { content: RealtimeStopSequence[] })} onMessage Callback function to unsubscribe. If null all subscriptions for the channel will be unsubscribed. * @public */ unsubscribeStopSequence(id, onMessage) { this.unsubscribe(`stopsequence`, `_${id}`, onMessage); } /** * Unsubscribe from current departures channel. * @param {number} stationId UIC of the station. * @param {function(data: { content: RealtimeDeparture[] })} onMessage Callback function to unsubscribe. If null all subscriptions for the channel will be unsubscribed. * @public */ unsubscribeTimetable(stationId, onMessage) { this.unsubscribe(`timetable_${stationId}`, '', onMessage); } /** * Unsubscribe to trajectory channels. * @param {function(data: { content: RealtimeTrajectory })} onMessage Callback function to unsubscribe. If null all subscriptions for the channel will be unsubscribed. * @public */ unsubscribeTrajectory(onMessage) { this.unsubscribe(`trajectory`, '', onMessage); } } export default RealtimeAPI;