mobility-toolbox-js
Version:
Toolbox for JavaScript applications in the domains of mobility and logistics.
392 lines (391 loc) • 15.5 kB
JavaScript
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;