mobility-toolbox-js
Version:
Toolbox for JavaScript applications in the domains of mobility and logistics.
263 lines (262 loc) • 11.1 kB
JavaScript
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _RealtimeLayer_internalId;
import { point } from '@turf/helpers';
import transformRotate from '@turf/transform-rotate';
import { getHeight, getWidth } from 'ol/extent';
import { fromLonLat } from 'ol/proj';
import RealtimeEngine from '../../common/utils/RealtimeEngine';
import { getSourceCoordinates } from '../utils';
import Layer from './Layer';
/**
* A Maplibre layer able to display data from the [geOps Realtime API](https://developer.geops.io/apis/realtime/).
*
* @example
* import { Map } from 'maplibre-gl';
* import { RealtimeLayer } from 'mobility-toolbox-js/maplibre';
*
* // Define the map
* const map = new Map({ ... });
*
* // Define your layer map
* const layer = new RealtimeLayer({
* apiKey: "yourApiKey"
* // url: "wss://api.geops.io/tracker-ws/v1/",
* });
*
* // Add the layer to your map *
* map.on('load', () => {
* map.addLayer(layer);
* });
*
*
* @see <a href="/api/class/src/api/RealtimeAPI%20js~RealtimeAPI%20html">RealtimeAPI</a>
* @see <a href="/example/mb-realtime>MapLibre Realtime layer example</a>
*
* @implements {maplibregl.CustomLayerInterface}
* @extends {maplibregl.Evented}
* @classproperty {function} filter - Filter out a train. This function must be fast, it is executed for every trajectory on every render frame.
* @classproperty {RealtimeMode} mode - The realtime mode to use.
* @classproperty {RealtimeMot[]} mots - Filter trains by its mode of transportation. It filters trains on backend side.
* @classproperty {RealtimeTenant} tenant - Filter trains by its tenant. It filters trains on backend side.
* @classproperty {function} sort - Sort trains. This function must be fast, it is executed on every render frame.
* @classproperty {function} style - Function to style the vehicles.
s
* @public
*/
class RealtimeLayer extends Layer {
get canvas() {
return this.engine.canvas;
}
get pixelRatio() {
return this.engine.pixelRatio || 1;
}
set pixelRatio(pixelRatio) {
this.engine.pixelRatio = pixelRatio || 1;
}
/**
* Constructor.
*
* @param {RealtimeLayerOptions} options
* @param {string} options.apiKey Access key for [geOps apis](https://developer.geops.io/).
* @param {FilterFunction} options.filter Filter out a train. This function must be fast, it is executed for every trajectory on every render frame.
* @param {getMotsByZoomFunction} options.getMotsByZoom Returns for each zoom level the list of MOTs to display. It filters trains on backend side.
* @param {number} [options.minZoomInterpolation=8] Minimal zoom level where to start to interpolate train positions.
* @param {RealtimeMode} [options.mode='topographic'] The realtime mode to use.
* @param {SortFunction} options.sort Sort trains. This function must be fast, it is executed on every render frame.
* @param {RealtimeStyleFunction} options.style Function to style the vehicles.
* @param {RealtimeTenant} options.tenant Filter trains by its tenant. It filters trains on backend side.
* @param {string} [options.url="wss://api.geops.io/tracker-ws/v1/"] The geOps Realtime API url.
*/
constructor(options = {}) {
var _a;
const id = (options === null || options === void 0 ? void 0 : options.id) || 'realtime';
super(Object.assign(Object.assign({}, options), { id: 'realtime-custom-' + id }));
_RealtimeLayer_internalId.set(this, void 0);
__classPrivateFieldSet(this, _RealtimeLayer_internalId, id, "f");
this.engine = new RealtimeEngine(Object.assign({ getViewState: this.getViewState.bind(this), onRender: this.onRealtimeEngineRender.bind(this) }, options));
this.sourceId = __classPrivateFieldGet(this, _RealtimeLayer_internalId, "f");
this.source = {
// Set to true if the canvas source is animated. If the canvas is static, animate should be set to false to improve performance.
animate: true,
// @ts-expect-error bad type definition
attribution: (_a = options.attribution) === null || _a === void 0 ? void 0 : _a.join(', '),
canvas: this.canvas,
// Set a default coordinates, it will be overrides on next data update
coordinates: [
[0, 0],
[1, 1],
[2, 2],
[0, 0],
],
loaded: true,
type: 'canvas',
};
this.layer = {
id: __classPrivateFieldGet(this, _RealtimeLayer_internalId, "f"),
layout: {
visibility: 'visible',
},
paint: {
'raster-fade-duration': 0,
'raster-opacity': 1,
'raster-resampling': 'nearest', // important otherwise it looks blurry
},
source: this.sourceId,
type: 'raster',
};
this.onLoad = this.onLoad.bind(this);
this.onMove = this.onMove.bind(this);
this.onMoveEnd = this.onMoveEnd.bind(this);
this.onZoomEnd = this.onZoomEnd.bind(this);
}
/**
* Return the current view state. Used by the RealtimeEngine.
* @private
*/
getViewState() {
if (!this.map) {
return {};
}
if (!this.pixelRatio) {
this.pixelRatio = 1;
}
const { height, width } = this.map.getCanvas();
const center = this.map.getCenter();
// We use turf here to have good transform.
// @ts-expect-error bad type definition
const leftBottom = this.map.unproject({
x: 0,
y: height / this.pixelRatio,
}); // southWest
// @ts-expect-error bad type definition
const rightTop = this.map.unproject({
x: width / this.pixelRatio,
y: 0,
}); // north east
const coord0 = transformRotate(point([leftBottom.lng, leftBottom.lat]), -this.map.getBearing(), {
pivot: [center.lng, center.lat],
}).geometry.coordinates;
const coord1 = transformRotate(point([rightTop.lng, rightTop.lat]), -this.map.getBearing(), {
pivot: [center.lng, center.lat],
}).geometry.coordinates;
const bounds = [...fromLonLat(coord0), ...fromLonLat(coord1)];
const xResolution = getWidth(bounds) / (width / this.pixelRatio);
const yResolution = getHeight(bounds) / (height / this.pixelRatio);
const res = Math.max(xResolution, yResolution);
// Coordinate of trajectories are in mercator so we have to pass the proper resolution and center in mercator.
return {
center: fromLonLat([center.lng, center.lat]),
extent: bounds,
pixelRatio: this.pixelRatio,
resolution: res,
rotation: -(this.map.getBearing() * Math.PI) / 180,
size: [width / this.pixelRatio, height / this.pixelRatio],
visible: true,
zoom: this.map.getZoom() - 1,
};
}
/**
* Add sources, layers and listeners to the map.
*/
onAdd(map, gl) {
super.onAdd(map, gl);
this.engine.attachToMap();
if (map.isStyleLoaded()) {
this.onLoad();
}
map.on('load', this.onLoad);
}
onLoad() {
var _a, _b, _c, _d;
if (!((_a = this.map) === null || _a === void 0 ? void 0 : _a.getSource(this.sourceId))) {
(_b = this.map) === null || _b === void 0 ? void 0 : _b.addSource(this.sourceId, this.source);
}
if (!((_c = this.map) === null || _c === void 0 ? void 0 : _c.getLayer(this.layer.id))) {
(_d = this.map) === null || _d === void 0 ? void 0 : _d.addLayer(this.layer, this.id);
}
this.start();
}
/**
* Callback on 'move' event.
*/
onMove() {
this.engine.renderTrajectories();
}
/**
* Callback on 'moveend' event.
*/
onMoveEnd() {
this.engine.renderTrajectories();
if (this.engine.isUpdateBboxOnMoveEnd) {
this.engine.setBbox();
}
}
/**
* Callback when the RealtimeEngine has rendered successfully.
*/
onRealtimeEngineRender() {
var _a;
if ((_a = this.map) === null || _a === void 0 ? void 0 : _a.style) {
const extent = getSourceCoordinates(this.map, this.pixelRatio);
const source = this.map.getSource(this.sourceId);
if (source) {
// @ts-expect-error bad type definition
source.setCoordinates(extent);
}
}
}
/**
* Remove source, layers and listeners from the map.
*/
onRemove(map, gl) {
this.engine.detachFromMap();
this.stop();
map.off('load', this.onLoad);
if (map.getLayer(this.layer.id)) {
map.removeLayer(this.layer.id);
}
if (map.getSource(this.sourceId)) {
map.removeSource(this.sourceId);
}
super.onRemove(map, gl);
}
onZoomEnd() {
this.engine.onZoomEnd();
}
/**
* Start updating vehicles position.
*
* @public
*/
start() {
var _a, _b, _c;
this.engine.start();
(_a = this.map) === null || _a === void 0 ? void 0 : _a.on('move', this.onMove);
(_b = this.map) === null || _b === void 0 ? void 0 : _b.on('moveend', this.onMoveEnd);
(_c = this.map) === null || _c === void 0 ? void 0 : _c.on('zoomend', this.onZoomEnd);
}
/**
* Stop updating vehicles position.
*
* @public
*/
stop() {
var _a, _b, _c;
this.engine.stop();
(_a = this.map) === null || _a === void 0 ? void 0 : _a.off('move', this.onMove);
(_b = this.map) === null || _b === void 0 ? void 0 : _b.off('moveend', this.onMoveEnd);
(_c = this.map) === null || _c === void 0 ? void 0 : _c.off('zoomend', this.onZoomEnd);
}
}
_RealtimeLayer_internalId = new WeakMap();
export default RealtimeLayer;