UNPKG

mobility-toolbox-js

Version:

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

392 lines (391 loc) 15.5 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import debounce from 'lodash.debounce'; import { getIntersection, isEmpty } from 'ol/extent'; import GeoJSON from 'ol/format/GeoJSON'; import Layer from 'ol/layer/Layer'; import VectorLayer from 'ol/layer/Vector'; import { unByKey } from 'ol/Observable'; import { Vector as VectorSource } from 'ol/source'; import Source from 'ol/source/Source'; import RealtimeEngine from '../../common/utils/RealtimeEngine'; import RealtimeLayerRenderer from '../renderers/RealtimeLayerRenderer'; import { fullTrajectoryStyle } from '../styles'; import defineDeprecatedProperties from '../utils/defineDeprecatedProperties'; import { deprecated } from './MaplibreLayer'; const format = new GeoJSON(); /** * An OpenLayers layer able to display data from the [geOps Realtime API](https://developer.geops.io/apis/realtime/). * * @example * import { RealtimeLayer } from 'mobility-toolbox-js/ol'; * * const layer = new RealtimeLayer({ * apiKey: "yourApiKey" * // allowRenderWhenAnimating: false, * // url: "wss://api.geops.io/tracker-ws/v1/", * }); * * * @see <a href="/api/class/src/api/RealtimeAPI%20js~RealtimeAPI%20html">RealtimeAPI</a> * @see <a href="/example/ol-realtime">OpenLayers Realtime layer example</a> * * * @extends {ol/layer/Layer~Layer} * * * @classproperty {boolean} allowRenderWhenAnimating - Allow rendering of the layer when the map is animating. * @public */ class RealtimeLayer extends Layer { get api() { return this.engine.api; } set api(api) { this.engine.api = api; } get canvas() { return this.engine.canvas; } get filter() { return this.engine.filter; } set filter(filter) { this.engine.filter = filter; } get hoverVehicleId() { return this.engine.hoverVehicleId; } set hoverVehicleId(id) { this.engine.hoverVehicleId = id; } get mode() { return this.engine.mode; } set mode(mode) { this.engine.mode = mode; } get pixelRatio() { return this.engine.pixelRatio; } get selectedVehicleId() { return this.engine.selectedVehicleId; } set selectedVehicleId(id) { this.engine.selectedVehicleId = id; } get sort() { return this.engine.sort; } set sort(sort) { this.engine.sort = sort; } get trajectories() { return this.engine.trajectories; } /** * Constructor. * * @param {RealtimeLayerOptions} options * @param {boolean} [options.allowRenderWhenAnimating=false] Allow rendering of the layer when the map is animating. * @param {string} options.apiKey Access key for [geOps APIs](https://developer.geops.io/). * @param {string} [options.url="wss://api.geops.io/tracker-ws/v1/"] The [geOps Realtime API](https://developer.geops.io/apis/realtime/) url. * @public */ constructor(options) { // We use a group to be able to add custom vector layer in extended class. // For example TrajservLayer use a vectorLayer to display the complete trajectory. super(Object.assign({ source: new Source({}) }, options)); this.allowRenderWhenAnimating = false; this.maxNbFeaturesRequested = 100; this.olEventsKeys = []; // For backward compatibility with v2 defineDeprecatedProperties(this, options); this.engine = new RealtimeEngine(Object.assign({ getViewState: this.getViewState.bind(this), onIdle: this.onRealtimeEngineIdle.bind(this), onRender: this.onRealtimeEngineRender.bind(this) }, options)); this.allowRenderWhenAnimating = !!options.allowRenderWhenAnimating; // We store the layer used to highlight the full Trajectory this.vectorLayer = new VectorLayer({ source: new VectorSource({ features: [] }), style: (feature, resolution) => { return (options.fullTrajectoryStyle || fullTrajectoryStyle)(feature, resolution, this.engine.styleOptions); }, updateWhileAnimating: this.allowRenderWhenAnimating, updateWhileInteracting: true, }); this.onZoomEndDebounced = debounce(this.onZoomEnd, 100); this.onMoveEndDebounced = debounce(this.onMoveEnd, 100); } attachToMap() { this.engine.attachToMap(); const mapInternal = this.getMapInternal(); if (mapInternal) { // If the layer is visible we start the rendering clock if (this.getVisible()) { this.engine.start(); } this.olEventsKeys.push(mapInternal.on('movestart', () => { if (this.engine.isUpdateBboxOnMoveEnd) { this.engine.updateIdleState(); } }), ...mapInternal.on(['moveend', 'change:target'], // @ts-expect-error - bad ol definitions (evt) => { const view = (evt.map || evt.target).getView(); if (!view || (view === null || view === void 0 ? void 0 : view.getAnimating()) || (view === null || view === void 0 ? void 0 : view.getInteracting())) { return; } const zoom = view.getZoom(); // Update the interval between render updates if (this.currentZoom !== zoom) { this.onZoomEndDebounced(evt); } this.currentZoom = zoom; this.onMoveEndDebounced(evt); }), this.on('change:visible', (evt) => { if (evt.target.getVisible()) { this.engine.start(); } else { this.engine.stop(); } }), this.on('propertychange', (evt) => { // We apply every property change event related to visiblity to the vectorlayer if (/(opacity|visible|zIndex|minResolution|maxResolution|minZoom|maxZoom)/.test(evt.key)) { this.vectorLayer.set(evt.key, evt.target.get(evt.key)); } })); } } cleanVectorLayer() { var _a, _b, _c; (_b = (_a = this.vectorLayer) === null || _a === void 0 ? void 0 : _a.getSource()) === null || _b === void 0 ? void 0 : _b.clear(true); (_c = this.vectorLayer.getMapInternal()) === null || _c === void 0 ? void 0 : _c.removeLayer(this.vectorLayer); } /** * Create a copy of the RealtimeLayer. * * @param {Object} newOptions Options to override. See constructor. * @return {RealtimeLayer} A RealtimeLayer * @public */ clone(newOptions) { return new RealtimeLayer(Object.assign(Object.assign({}, this.get('options')), newOptions)); } createRenderer() { return new RealtimeLayerRenderer(this); } /** * Destroy the container of the tracker. */ detachFromMap() { var _a; unByKey(this.olEventsKeys); (_a = this.getMapInternal()) === null || _a === void 0 ? void 0 : _a.removeLayer(this.vectorLayer); this.engine.detachFromMap(); } /** * Get the full trajectory of a vehicle as features. * * @param {string} id A vehicle's id. * @returns {Promise<Feature[]>} A list of features representing a full trajectory. * @public */ getFullTrajectory(id) { return __awaiter(this, void 0, void 0, function* () { var _a, _b, _c, _d; const data = yield this.engine.api.getFullTrajectory(id, this.engine.mode, this.engine.getGeneralizationLevelByZoom(Math.floor(((_b = (_a = this.getMapInternal()) === null || _a === void 0 ? void 0 : _a.getView()) === null || _b === void 0 ? void 0 : _b.getZoom()) || 0))); if ((_d = (_c = data === null || data === void 0 ? void 0 : data.content) === null || _c === void 0 ? void 0 : _c.features) === null || _d === void 0 ? void 0 : _d.length) { return format.readFeatures(data === null || data === void 0 ? void 0 : data.content); } return []; }); } /** * Get the stop sequences of a vehicle. * * @param {string} id A vehicle's id. * @returns {Promise<RealtimeStopSequence[]>} An array of stop sequences. * @public */ getStopSequences(id) { return __awaiter(this, void 0, void 0, function* () { const data = yield this.engine.api.getStopSequence(id); return data === null || data === void 0 ? void 0 : data.content; }); } /** * Get full trajectory and stop sequences of a vehicle. * * @param {RealtimeTrainId} id A vehicle's id. * @returns {Promise<{fullTrajectory: Feature[], stopSequences: RealtimeStopSequence[]}>} An object containing the full trajectory and the stop sequences. */ getTrajectoryInfos(id) { return __awaiter(this, void 0, void 0, function* () { // When a vehicle is selected, we request the complete stop sequence and the complete full trajectory. // Then we combine them in one response. const promises = [this.getStopSequences(id), this.getFullTrajectory(id)]; const [stopSequences, fullTrajectory] = yield Promise.all(promises); return { fullTrajectory: fullTrajectory, stopSequences: stopSequences, }; }); } getVehicles(filterFunc) { return this.engine.getVehicles(filterFunc); } getViewState() { const mapInternal = this.getMapInternal(); if (!(mapInternal === null || mapInternal === void 0 ? void 0 : mapInternal.getView())) { return {}; } const view = mapInternal.getView(); let extent = view.calculateExtent(); const layerExtent = this.getExtent(); if (layerExtent) { extent = getIntersection(extent, layerExtent); // If there is no intersection we use the layer extent if (isEmpty(extent)) { extent = layerExtent; } } return { center: view.getCenter(), extent: extent, pixelRatio: this.engine.pixelRatio, resolution: view.getResolution(), rotation: view.getRotation(), size: mapInternal.getSize(), visible: this.getVisible(), zoom: view.getZoom(), }; } highlight(features) { const feat = Array.isArray(features) ? features[0] : features; const id = feat === null || feat === void 0 ? void 0 : feat.get('train_id'); if (this.hoverVehicleId !== id) { this.hoverVehicleId = id; this.engine.renderTrajectories(true); } } /** * Highlight the trajectory of journey. */ highlightTrajectory(id) { return __awaiter(this, void 0, void 0, function* () { var _a, _b, _c, _d, _e, _f; const features = yield this.getFullTrajectory(id); this.cleanVectorLayer(); if (features.length) { (_b = (_a = this.vectorLayer) === null || _a === void 0 ? void 0 : _a.getSource()) === null || _b === void 0 ? void 0 : _b.addFeatures(features); } if (this.vectorLayer.getMapInternal() && this.vectorLayer.getMapInternal() !== this.getMapInternal()) { (_c = this.vectorLayer.getMapInternal()) === null || _c === void 0 ? void 0 : _c.removeLayer(this.vectorLayer); } // Add the vector layer to the map const zIndex = this.getZIndex(); if (zIndex !== undefined) { this.vectorLayer.setZIndex(zIndex - 1); if (!this.vectorLayer.getMapInternal()) { (_d = this.getMapInternal()) === null || _d === void 0 ? void 0 : _d.addLayer(this.vectorLayer); } } else if (!this.vectorLayer.getMapInternal()) { const index = ((_e = this.getMapInternal()) === null || _e === void 0 ? void 0 : _e.getLayers().getArray().indexOf(this)) || 0; if (index) { (_f = this.getMapInternal()) === null || _f === void 0 ? void 0 : _f.getLayers().insertAt(index, this.vectorLayer); } } return features; }); } onMoveEnd() { if (!this.engine.isUpdateBboxOnMoveEnd || !this.getVisible()) { return; } this.engine.setBbox(); } onRealtimeEngineIdle() { this.changed(); } /** * Callback when the RealtimeEngine has rendered successfully. */ onRealtimeEngineRender(renderState, viewState) { this.renderedViewState = Object.assign({}, viewState); // @ts-expect-error - we are in the same class const { container } = this.getRenderer(); if (container) { container.style.transform = ''; } } onZoomEnd() { this.engine.onZoomEnd(); if (!this.engine.isUpdateBboxOnMoveEnd || !this.getVisible()) { return; } if (this.selectedVehicleId) { void this.highlightTrajectory(this.selectedVehicleId); } } /** * Render the trajectories of the vehicles. * @deprecated Use this.engine.renderTrajectories instead. */ renderTrajectories(noInterpolate) { deprecated('RealtimeLayer.renderTrajectories is deprecated. Use RealtimeLayer.engine.renderTrajectories instead.'); this.engine.renderTrajectories(noInterpolate); } select(features) { const feat = Array.isArray(features) ? features[0] : features; const id = feat === null || feat === void 0 ? void 0 : feat.get('train_id'); if (this.selectedVehicleId !== id) { this.cleanVectorLayer(); this.selectedVehicleId = id; this.engine.renderTrajectories(true); } void this.highlightTrajectory(id); } setMapInternal(map) { if (map) { super.setMapInternal(map); this.attachToMap(); } else { this.detachFromMap(); super.setMapInternal(map); } } shouldRender() { var _a, _b; return this.allowRenderWhenAnimating ? false : ((_a = this.getMapInternal()) === null || _a === void 0 ? void 0 : _a.getView().getAnimating()) || ((_b = this.getMapInternal()) === null || _b === void 0 ? void 0 : _b.getView().getInteracting()); } /** * Start the rendering. * * @public */ start() { this.engine.start(); } /** * Stop the rendering. * * @public */ stop() { this.engine.stop(); } } export default RealtimeLayer;