From c551f8850c30060055ac624f4b61126de936fd84 Mon Sep 17 00:00:00 2001 From: candux Date: Thu, 7 Oct 2021 15:28:07 +0200 Subject: [PATCH 1/3] update ubuntu-version for cypress --- .github/workflows/cypress.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index 7a8c9e64..0439df07 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -4,7 +4,7 @@ on: [push] jobs: build: - runs-on: ubuntu-16.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 From 879b522b36250748d02f7393a9e3a9a551f9363e Mon Sep 17 00:00:00 2001 From: oterral Date: Mon, 11 Oct 2021 09:01:41 +0200 Subject: [PATCH 2/3] Allow to set tracker properties at constructor time (#142) * Adapt doc * Clean tracker doc and constructor * Clean tracker doc and constructor * v1.3.6-beta.0 * Abort previous requests * Try CODEOWNERS file * Remove CODEOWNERS file * v1.3.13-beta.1 * Unsubscribe previous call with same callback function * v1.3.13-beta.2 * MAke sure setBBox doesn't read event twice * v1.3.13-beta.3 * Fix render in the past activating on new live mode * v1.3.13-beta.4 * Test pixelRatio * Fix style with pixelRatio * Fix pixelRatio * Fix pixelRatio for tralis * v1.3.13-beta.5 * Clean TrackerLayerMixin * Remove useless property * Allow rotation on mapbox tralis layer * Allow rotation on mapbox tralis layer * Allow rotation for tracke rwith ol * Update src/common/Tracker.js * Clean TrackerLayer * v1.3.13-beta.6 * Clean TrackerLayer * v1.3.13-beta.7 * Apply live=false * Fix when live=true * v1.3.13-beta.8 --- __mocks__/mapbox-gl.js | 11 + package.json | 2 +- src/api/tralis/TralisAPI.js | 2 +- src/api/tralis/WebSocketConnector.js | 27 +- src/api/tralis/WebSocketConnector.test.js | 47 +++ src/common/Tracker.js | 162 +++------- src/common/mixins/SearchMixin.js | 5 +- src/common/mixins/TrackerLayerMixin.js | 296 +++++++++++++----- src/common/mixins/TrajservLayerMixin.js | 22 +- src/common/mixins/TralisLayerMixin.js | 8 - src/doc/App.js | 2 - src/doc/components/Example.js | 2 +- .../assets/tralis-live-map/s1kreis.svg | 8 +- src/doc/examples/mb-tracker.js | 2 - src/doc/examples/tralis-live-map.js | 5 +- src/mapbox/layers/TrackerLayer.js | 71 ++--- src/mapbox/layers/TrajservLayer.js | 17 +- src/mapbox/layers/TralisLayer.js | 13 +- src/mapbox/utils.js | 20 +- src/ol/layers/MapboxStyleLayer.js | 3 +- src/ol/layers/TrackerLayer.js | 94 +++--- 21 files changed, 484 insertions(+), 335 deletions(-) diff --git a/__mocks__/mapbox-gl.js b/__mocks__/mapbox-gl.js index 61182453..4644903a 100644 --- a/__mocks__/mapbox-gl.js +++ b/__mocks__/mapbox-gl.js @@ -32,6 +32,8 @@ class Map { getZoom() {} + getBearing() {} + once() {} on() {} @@ -41,6 +43,15 @@ class Map { loaded() {} remove() {} + + unproject() { + return [ + [0, 0], + [0, 0], + [0, 0], + [0, 0], + ]; + } } module.exports = { Map, diff --git a/package.json b/package.json index c363e541..ddc5cd31 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "mobility-toolbox-js", "license": "MIT", "description": "Toolbox for JavaScript applications in the domains of mobility and logistics.", - "version": "1.3.12", + "version": "1.3.13-beta.8", "main": "index.js", "module": "module.js", "dependencies": { diff --git a/src/api/tralis/TralisAPI.js b/src/api/tralis/TralisAPI.js index afc6c008..a6b6abcc 100644 --- a/src/api/tralis/TralisAPI.js +++ b/src/api/tralis/TralisAPI.js @@ -395,7 +395,7 @@ class TralisAPI { * @param {function(response: { content: Vehicle })} onMessage Function called on each message of the channel. */ subscribeDeletedVehicles(mode, onMessage) { - this.unsubscribeDeletedVehicles(); + this.unsubscribeDeletedVehicles(onMessage); this.subscribe( `deleted_vehicles${getModeSuffix(mode, TralisModes)}`, onMessage, diff --git a/src/api/tralis/WebSocketConnector.js b/src/api/tralis/WebSocketConnector.js index f2af9cee..5e1493bc 100644 --- a/src/api/tralis/WebSocketConnector.js +++ b/src/api/tralis/WebSocketConnector.js @@ -138,6 +138,9 @@ class WebSocketConnector { * @returns {{onMessage: function, errorCb: function}} Object with onMessage and error callbacks */ listen(params, cb, errorCb) { + // Remove the previous identical callback + this.unlisten(params, cb); + const onMessage = (e) => { const data = JSON.parse(e.data); let source = params.channel; @@ -161,6 +164,20 @@ class WebSocketConnector { return { onMessageCb: onMessage, onErrorCb: errorCb }; } + unlisten(params, cb) { + this.subscriptions + .filter((s) => { + return s.params.channel === params.channel && (!cb || s.cb === cb); + }) + .forEach(({ onMessageCb, onErrorCb }) => { + this.websocket.removeEventListener('message', onMessageCb); + if (onErrorCb) { + this.websocket.removeEventListener('error', onErrorCb); + this.websocket.removeEventListener('close', onErrorCb); + } + }); + } + /** * Subscribe to a given channel. * @private @@ -174,7 +191,15 @@ class WebSocketConnector { const reqStr = WebSocketConnector.getRequestString('', params); if (!quiet) { - this.subscriptions.push({ params, cb, errorCb, onMessageCb, onErrorCb }); + const index = this.subscriptions.findIndex((subcr) => { + return params.channel === subcr.params.channel && cb === subcr.cb; + }); + const newSubscr = { params, cb, errorCb, onMessageCb, onErrorCb }; + if (index > -1) { + this.subscriptions[index] = newSubscr; + } else { + this.subscriptions.push(newSubscr); + } } if (!this.subscribed[reqStr]) { diff --git a/src/api/tralis/WebSocketConnector.test.js b/src/api/tralis/WebSocketConnector.test.js index 2f794f05..f113922d 100644 --- a/src/api/tralis/WebSocketConnector.test.js +++ b/src/api/tralis/WebSocketConnector.test.js @@ -55,19 +55,66 @@ describe('WebSocketConnector', () => { test('should unsubscribe all subscriptions related to a channel', () => { // eslint-disable-next-line no-unused-vars const client = new Connector(`ws://foo:1234`); + client.websocket.removeEventListener = jest.fn(); + client.websocket.addEventListener = jest.fn(); const params = { channel: 'foo' }; const params2 = { channel: 'bar' }; const cb = jest.fn(); const cb2 = jest.fn(); client.subscribe(params, cb); + client.subscribe(params, cb); + client.subscribe(params, cb); + client.subscribe(params, cb2); + client.subscribe(params2, cb2); + expect(client.subscriptions.length).toBe(3); + expect(client.websocket.removeEventListener).toBeCalledTimes(2); + expect(client.websocket.addEventListener).toBeCalledTimes(9); + + client.unsubscribe('foo'); + expect(client.subscriptions.length).toBe(1); + expect(client.subscriptions[0].params).toBe(params2); + expect(client.subscriptions[0].cb).toBe(cb2); + }); + }); + + describe('#setBbox', () => { + test.only('should remove subscriptions before re adding it ', () => { + // eslint-disable-next-line no-unused-vars + const client = new Connector(`ws://foo:1234`); + client.websocket.removeEventListener = jest.fn(); + client.websocket.addEventListener = jest.fn(); + client.send = jest.fn(); + const params = { channel: 'foo' }; + const params2 = { channel: 'bar' }; + const cb = jest.fn(); + const cb2 = jest.fn(); + client.subscribe(params, cb); + client.subscribe(params, cb); + client.subscribe(params, cb); client.subscribe(params, cb2); client.subscribe(params2, cb2); expect(client.subscriptions.length).toBe(3); + expect(client.websocket.removeEventListener).toBeCalledTimes(2); + expect(client.websocket.addEventListener).toBeCalledTimes(5); + + client.websocket.removeEventListener.mockReset(); + client.websocket.addEventListener.mockReset(); + + client.setBbox([0, 0, 0, 0]); + + expect(client.subscriptions.length).toBe(3); + expect(client.websocket.removeEventListener).toBeCalledTimes(3); + expect(client.websocket.addEventListener).toBeCalledTimes(3); client.unsubscribe('foo'); expect(client.subscriptions.length).toBe(1); expect(client.subscriptions[0].params).toBe(params2); expect(client.subscriptions[0].cb).toBe(cb2); + client.unsubscribe('bar'); + expect(client.subscriptions.length).toBe(0); + client.send.mockRestore(); + client.websocket.removeEventListener.mockRestore(); + client.websocket.addEventListener.mockRestore(); }); }); }); diff --git a/src/common/Tracker.js b/src/common/Tracker.js index ecfa198a..df6fca6a 100644 --- a/src/common/Tracker.js +++ b/src/common/Tracker.js @@ -17,6 +17,11 @@ export default class Tracker { ...options, }; + /** + * Pixel ratio to use to draw the canvas. Default to window.devicePixelRatio + * @type {Array} + */ + this.pixelRatio = options.pixelRatio || window.devicePixelRatio || 1; /** * Array of trajectories. * @type {Array} @@ -30,8 +35,8 @@ export default class Tracker { this.renderedTrajectories = []; /** - * Array of ol events key, returned by on() or once(). - * @type {Array} + * Active interpolation calculation or not. If false, the train will not move until we receive the next message for the websocket. + * @type {boolean} */ this.interpolate = !!opts.interpolate; @@ -45,19 +50,31 @@ export default class Tracker { * Id of the trajectory which is hovered. * @type {string} */ - this.hoverVehicleId = null; + this.hoverVehicleId = opts.hoverVehicleId; /** * Id of the trajectory which is selected. * @type {string} */ - this.selectedVehicleId = null; + this.selectedVehicleId = opts.selectedVehicleId; /** - * Scale the vehicle icons with this value. - * @type {number} + * Function use to filter the features displayed. + * @type {function} + */ + this.filter = opts.filter; + + /** + * Function use to sort the features displayed. + * @type {function} + */ + this.sort = opts.sort; + + /** + * Function use to style the features displayed. + * @type {function} */ - this.iconScale = opts.iconScale; + this.style = opts.style; // we draw directly on the canvas since openlayers is too slow. /** @@ -65,16 +82,16 @@ export default class Tracker { * @type {Canvas} */ this.canvas = opts.canvas || document.createElement('canvas'); - this.canvas.width = opts.width; - this.canvas.height = opts.height; + this.canvas.width = opts.width * this.pixelRatio; + this.canvas.height = opts.height * this.pixelRatio; this.canvas.setAttribute( 'style', [ 'position: absolute', 'top: 0', 'bottom: 0', - 'width: 100%', - 'height: 100%', + `width: ${opts.width}px`, + `height: ${opts.height}px`, 'pointer-events: none', 'visibility: visible', 'margin-top: inherit', // for scrolling behavior. @@ -117,15 +134,6 @@ export default class Tracker { return this.trajectories || []; } - /** - * Return rendered trajectories. - * Use this to avoid race conditions while rendering. - * @returns {array} trajectories - */ - getRenderedTrajectories() { - return this.renderedTrajectories; - } - /** * Clear the canvas. * @private @@ -136,85 +144,6 @@ export default class Tracker { } } - /** - * Set the filter for tracker features. - * @param {function} filter Filter function. - */ - setFilter(filter) { - /** - * Current filter function. - * @type {function} - */ - this.filter = filter; - } - - /** - * Set the sort for tracker features. - * @param {function} sort Sort function. - */ - setSort(sort) { - /** - * The sort function for tracker features. - * @type {function} - */ - this.sort = sort; - } - - /** - * Set the id of the trajectory which is hovered. - * @param {string} id Id of a vehicle. - * @private - */ - setHoverVehicleId(id) { - if (id !== this.hoverVehicleId) { - this.hoverVehicleId = id; - } - } - - /** - * Set the id of the trajectory which is selected. - * @param {string} id Id of a vehicle. - * @private - */ - setSelectedVehicleId(id) { - if (id !== this.selectedVehicleId) { - this.selectedVehicleId = id; - } - } - - /** - * set the scale of the vehicle icons. - * @param {number} iconScale Scale value. - */ - setIconScale(iconScale) { - this.iconScale = iconScale; - } - - /** - * Set the tracker style. - * @param {function} s OpenLayers style function. - */ - setStyle(s) { - /** - * Style function. - * @type {function} - */ - this.style = s; - } - - /** - * Move the canvas. - * @param {number} offsetX Offset X. - * @param {number} offsetY Offset Y. - * @private - */ - moveCanvas(offsetX, offsetY) { - const oldLeft = parseFloat(this.canvas.style.left); - const oldTop = parseFloat(this.canvas.style.top); - this.canvas.style.left = `${oldLeft - offsetX}px`; - this.canvas.style.top = `${oldTop - offsetY}px`; - } - /** * Draw all the trajectories available to the canvas. * @param {Date} currTime The date to render. @@ -232,8 +161,6 @@ export default class Tracker { noInterpolate = false, ) { this.clear(); - this.canvas.style.left = '0px'; - this.canvas.style.top = '0px'; const [width, height] = size; if ( @@ -241,8 +168,17 @@ export default class Tracker { height && (this.canvas.width !== width || this.canvas.height !== height) ) { - [this.canvas.width, this.canvas.height] = [width, height]; + [this.canvas.width, this.canvas.height] = [ + width * this.pixelRatio, + height * this.pixelRatio, + ]; } + + this.canvas.style.left = '0px'; + this.canvas.style.top = '0px'; + this.canvas.style.transform = ``; + this.canvas.style.width = `${this.canvas.width / this.pixelRatio}px`; + this.canvas.style.height = `${this.canvas.height / this.pixelRatio}px`; /** * Current resolution. * @type {number} @@ -342,29 +278,33 @@ export default class Tracker { if (coord) { // We set the rotation of the trajectory (used by tralis). this.trajectories[i].coordinate = coord; - const px = this.getPixelFromCoordinate(coord); + let px = this.getPixelFromCoordinate(coord); if (!px) { // eslint-disable-next-line no-continue continue; } + + px = px.map((p) => { + return p * this.pixelRatio; + }); + // Trajectory with pixel (i.e. within map extent) will be in renderedTrajectories. this.trajectories[i].rendered = true; this.renderedTrajectories.push(this.trajectories[i]); - const vehicleImg = this.style(traj, this.currResolution); + const vehicleImg = this.style( + traj, + this.currResolution, + this.pixelRatio, + ); if (!vehicleImg) { // eslint-disable-next-line no-continue continue; } - let imgWidth = vehicleImg.width; - let imgHeight = vehicleImg.height; - - if (this.iconScale) { - imgHeight = Math.floor(imgHeight * this.iconScale); - imgWidth = Math.floor(imgWidth * this.iconScale); - } + const imgWidth = vehicleImg.width; + const imgHeight = vehicleImg.height; if ( this.hoverVehicleId !== traj.id && diff --git a/src/common/mixins/SearchMixin.js b/src/common/mixins/SearchMixin.js index 7ae409df..cf5fb56b 100644 --- a/src/common/mixins/SearchMixin.js +++ b/src/common/mixins/SearchMixin.js @@ -54,6 +54,7 @@ const SearchMixin = (Base) => apiOptions.url = url; } this.api = new StopsAPI(apiOptions); + this.abortController = new AbortController(); } render(suggestions = []) { @@ -101,7 +102,9 @@ const SearchMixin = (Base) => this.inputElt.placeholder = this.placeholder; this.inputElt.autoComplete = 'off'; this.inputElt.onkeyup = (evt) => { - this.search(evt.target.value); + this.abortController.abort(); + this.abortController = new AbortController(); + this.search(evt.target.value, this.abortController); }; Object.assign(this.inputElt.style, { padding: '10px 30px 10px 10px', diff --git a/src/common/mixins/TrackerLayerMixin.js b/src/common/mixins/TrackerLayerMixin.js index 2429a5fa..c274ab31 100644 --- a/src/common/mixins/TrackerLayerMixin.js +++ b/src/common/mixins/TrackerLayerMixin.js @@ -13,6 +13,7 @@ import { timeSteps } from '../trackerConfig'; * @classproperty {function} style - Style of the vehicle. * @classproperty {FilterFunction} filter - Time speed. * @classproperty {function} sort - Set the filter for tracker features. + * @classproperty {boolean} live - If true, the layer will always use Date.now() to render trajectories. Default to true. * @classproperty {boolean} useRequestAnimationFrame - If true, encapsulates the renderTrajectories calls in a requestAnimationFrame. Experimental. */ export class TrackerLayerInterface { @@ -34,16 +35,11 @@ export class TrackerLayerInterface { /** * Start the clock. - * - * @param {Array} size Map's size: [width, height]. - * @param {number} zoom Map's zoom level. - * @param {number} resolution Map's resolution. */ - // eslint-disable-next-line no-unused-vars - start(size, zoom, resolution) {} + start() {} /** - * Stop the time. + * Start the timeout for the next update. * @private * @param {number} zoom */ @@ -55,17 +51,6 @@ export class TrackerLayerInterface { */ stop() {} - /** - * Set the current time, it triggers a rendering of the trajectories. - * - * @param {Date} time The date to render. - * @param {number[2]} size Size of the canvas to render. - * @param {number} resolution Map's resolution to render. - * @param {boolean} [mustRender=true] If false bypass the rendering of vehicles. - */ - // eslint-disable-next-line no-unused-vars - setCurrTime(time, size, resolution, mustRender = true) {} - /** * Get vehicle. * @param {function} filterFc A function use to filter results. @@ -124,37 +109,96 @@ const TrackerLayerMixin = (Base) => isHoverActive: true, ...options, }; - let cuurSpeed = speed || 1; + + // Tracker options use to build the tracker. + const { + pixelRatio, + interpolate, + hoverVehicleId, + selectedVehicleId, + filter, + sort, + time, + live, + } = options; + + const initTrackerOptions = { + pixelRatio: pixelRatio || window.devicePixelRatio || 1, + interpolate, + hoverVehicleId, + selectedVehicleId, + filter, + sort, + style, + }; + + Object.keys(initTrackerOptions).forEach( + (key) => + initTrackerOptions[key] === undefined && + delete initTrackerOptions[key], + ); + + let currSpeed = speed || 1; + let currTime = time || new Date(); + super.defineProperties(options); + Object.defineProperties(this, { isTrackerLayer: { value: true }, + + /** + * Active on hover effect. + */ isHoverActive: { value: !!isHoverActive, writable: true, }, + + /** + * Style function used to render a vehicle. + */ style: { value: style || this.defaultStyle, }, + + /** + * Speed of the wheel of time. + * If live property is true. The speed is ignored. + */ speed: { - get: () => cuurSpeed, + get: () => currSpeed, set: (newSpeed) => { - cuurSpeed = newSpeed; + currSpeed = newSpeed; this.start(); }, }, + + /** + * Function to filter which vehicles to display. + */ filter: { - get: () => this.tracker.filter, - set: (filter) => { + get: () => + this.tracker ? this.tracker.filter : this.initTrackerOptions.filter, + set: (newFilter) => { if (this.tracker) { - this.tracker.setFilter(filter); + this.tracker.filter = newFilter; + } else { + this.initTrackerOptions.filter = newFilter; } }, }, + + /** + * Function to sort the vehicles to display. + */ sort: { - get: () => this.tracker.sort, - set: (sort) => { - if (this.sort) { - this.tracker.setSort(sort); + get: () => + this.tracker ? this.tracker.sort : this.initTrackerOptions.sort, + set: (newSort) => { + if (this.tracker) { + this.tracker.sort = newSort; + } else { + this.initTrackerOptions.sort = newSort; } }, }, @@ -170,27 +214,31 @@ const TrackerLayerMixin = (Base) => styleCache: { value: {} }, /** - * Time used to display the trajectories. + * If true. The layer will always use Date.now() on the next tick to render the trajectories. + * When true, setting the time property has no effect. */ - currTime: { - value: new Date(), + live: { + value: live === false ? live : true, writable: true, }, /** - * Keep track of the last time used to render trajectories. - * Useful when the speed increase. + * Time used to display the trajectories. Can be a Date or a number in ms representing a Date. + * If live property is true. The setter does nothing. */ - lastUpdateTime: { - value: new Date(), - writable: true, + time: { + get: () => currTime, + set: (newTime) => { + currTime = newTime && newTime.getTime ? newTime : new Date(newTime); + this.renderTrajectories(); + }, }, /** * Keep track of which trajectories are currently drawn. */ renderedTrajectories: { - get: () => this.tracker.renderedTrajectories, + get: () => this.tracker?.renderedTrajectories || [], }, /** @@ -198,10 +246,16 @@ const TrackerLayerMixin = (Base) => */ hoverVehicleId: { get: () => { - return this.tracker.hoverVehicleId; + return this.tracker + ? this.tracker.hoverVehicleId + : this.initTrackerOptions.hoverVehicleId; }, - set: (hoverVehicleId) => { - this.tracker.hoverVehicleId = hoverVehicleId; + set: (newHoverVehicleId) => { + if (this.tracker) { + this.tracker.hoverVehicleId = newHoverVehicleId; + } else { + this.initTrackerOptions.hoverVehicleId = newHoverVehicleId; + } }, }, @@ -209,16 +263,46 @@ const TrackerLayerMixin = (Base) => * Id of the selected vehicle. */ selectedVehicleId: { - get: () => { - return this.tracker.selectedVehicleId; + get: () => + this.tracker + ? this.tracker.selectedVehicleId + : this.initTrackerOptions.selectedVehicleId, + set: (newSelectedVehicleId) => { + if (this.tracker) { + this.tracker.selectedVehicleId = newSelectedVehicleId; + } else { + this.initTrackerOptions.selectedVehicleId = newSelectedVehicleId; + } }, - set: (selectedVehicleId) => { - this.tracker.selectedVehicleId = selectedVehicleId; + }, + + /** + * Pixel ratio use for the rendering. Default to window.devicePixelRatio. + */ + pixelRatio: { + get: () => + this.tracker + ? this.tracker.pixelRatio + : this.initTrackerOptions.pixelRatio, + set: (newPixelRatio) => { + if (this.tracker) { + this.tracker.pixelRatio = newPixelRatio; + } else { + this.initTrackerOptions.pixelRatio = newPixelRatio; + } }, }, /** - * If true, encapsulates the renderTrajectories calls in a requestAnimationFrame + * Options used by the constructor of the Tracker class. + */ + initTrackerOptions: { + value: initTrackerOptions, + writable: false, + }, + + /** + * If true, encapsulates the renderTrajectories calls in a requestAnimationFrame. */ useRequestAnimationFrame: { default: false, @@ -231,14 +315,24 @@ const TrackerLayerMixin = (Base) => * Initalize the Tracker. * @param {ol/Map~Map} map * @param {Object} options - * @param {Number} [options.width] Canvas's width. - * @param {Number} [options.height] Canvas's height. + * @param {number} [options.width] Canvas's width. + * @param {number} [options.height] Canvas's height. + * @param {bool} [options.interpolate] Convert an EPSG:3857 coordinate to a canvas pixel (origin top-left). + * @param {string} [options.hoverVehicleId] Id of the trajectory which is hovered. + * @param {string} [options.selectedVehicleId] Id of the trajectory which is selected. * @param {function} [options.getPixelFromCoordinate] Convert an EPSG:3857 coordinate to a canvas pixel (origin top-left). + * @param {function} [options.filter] Function use to filter the features displayed. + * @param {function} [options.sort] Function use to sort the features displayed. + * @param {function} [options.style] Function use to style the features displayed. */ - init(map, options) { + init(map, options = {}) { super.init(map); - this.tracker = new Tracker(options); - this.tracker.setStyle((props, r) => this.style(props, r)); + + this.tracker = new Tracker({ + style: (props, r) => this.style(props, r), + ...this.initTrackerOptions, + ...options, + }); if (this.visible) { this.start(); @@ -267,36 +361,37 @@ const TrackerLayerMixin = (Base) => } /** - * Start the clock. + * Start the trajectories rendering. * * @param {Array} size Map's size: [width, height]. - * @param {Number} zoom Map's zoom level. - * @param {Number} resolution Map's resolution. + * @param {number} zoom Map's zoom level. + * @param {number} resolution Map's resolution. + * @param {number} rotation Map's rotation. */ - start(size, zoom, resolution) { + start() { this.stop(); this.tracker.setVisible(true); - this.renderTrajectories(this.currTime, size, resolution); - this.startUpdateTime(zoom); + this.renderTrajectories(); + this.startUpdateTime(); } /** - * Start the time. + * Start the clock. * @private - * @param {number} zoom */ - startUpdateTime(zoom) { + startUpdateTime() { this.stopUpdateTime(); + this.updateTimeDelay = this.getRefreshTimeInMs(); this.updateTimeInterval = setInterval(() => { - const newTime = - this.currTime.getTime() + - (new Date() - this.lastUpdateTime) * this.speed; - this.setCurrTime(newTime); - }, this.getRefreshTimeInMs(zoom)); + // When live=true, we update the time with new Date(); + this.time = this.live + ? new Date() + : this.time.getTime() + this.updateTimeDelay * this.speed; + }, this.updateTimeDelay); } /** - * Stop the clock. + * Stop the trajectories rendering. */ stop() { this.stopUpdateTime(); @@ -307,7 +402,7 @@ const TrackerLayerMixin = (Base) => } /** - * Stop the time. + * Stop the clock. * @private */ stopUpdateTime() { @@ -317,35 +412,64 @@ const TrackerLayerMixin = (Base) => } /** - * Render the trajectories requesting an animation frame and cancelling the previous one + * Launch renderTrajectories. it avoids duplicating code in renderTrajectories methhod. * @private */ - renderTrajectories(time, size, resolution) { + renderTrajectoriesInternal(size, resolution, rotation, noInterpolate) { + if (!this.tracker) { + return; + } + + const renderTime = this.live ? Date.now() : this.time; + + // Avoid useless render before the next tick. + if ( + this.live && + resolution === this.lastRenderResolution && + rotation === this.lastRenderRotation && + renderTime - this.lastRenderTime < this.updateTimeDelay + ) { + return; + } + + this.lastRenderTime = renderTime; + this.lastRenderResolution = resolution; + this.lastRenderRotation = rotation; + + this.tracker.renderTrajectories( + renderTime, + size, + resolution, + noInterpolate, + ); + } + + /** + * Render the trajectories requesting an animation frame and cancelling the previous one. + * This function must be overrided by children to provide the correct parameters. + * @private + */ + renderTrajectories(size, resolution, rotation, noInterpolate) { if (this.requestId) { cancelAnimationFrame(this.requestId); } + if (this.useRequestAnimationFrame) { this.requestId = requestAnimationFrame(() => { - this.tracker.renderTrajectories(time, size, resolution); + this.renderTrajectoriesInternal( + size, + resolution, + rotation, + noInterpolate, + ); }); } else { - this.tracker.renderTrajectories(time, size, resolution); - } - } - - /** - * Set the current time, it triggers a rendering of the trajectories. - * @param {dateString | value} time - * @param {Array} size - * @param {number} resolution - * @param {boolean} [mustRender=true] - */ - setCurrTime(time, size, resolution, mustRender = true) { - const newTime = new Date(time); - this.currTime = newTime; - this.lastUpdateTime = new Date(); - if (mustRender) { - this.renderTrajectories(this.currTime, size, resolution); + this.renderTrajectoriesInternal( + size, + resolution, + rotation, + noInterpolate, + ); } } diff --git a/src/common/mixins/TrajservLayerMixin.js b/src/common/mixins/TrajservLayerMixin.js index 3401ef98..522fcdb5 100644 --- a/src/common/mixins/TrajservLayerMixin.js +++ b/src/common/mixins/TrajservLayerMixin.js @@ -337,7 +337,7 @@ const TrajservLayerMixin = (TrackerLayer) => // The 5 seconds more are used as a buffer if the request takes too long. const requestIntervalInMs = (this.requestIntervalSeconds + 5) * 1000; const intervalMs = this.speed * requestIntervalInMs; - const now = this.currTime; + const now = this.time; let diff = true; @@ -367,7 +367,7 @@ const TrajservLayerMixin = (TrackerLayer) => cd: 1, nm: 1, fl: 1, - // toff: this.currTime.getTime() / 1000, + // toff: this.time.getTime() / 1000, }; // Allow to load only differences between the last request, @@ -435,8 +435,8 @@ const TrajservLayerMixin = (TrackerLayer) => const key = `${z}${type}${name}${operatorProvidesRealtime}${delay}${hover}${selected}${cancelled}`; if (!this.styleCache[key]) { - let radius = getRadius(type, z); - const isDisplayStrokeAndDelay = radius >= 7; + let radius = getRadius(type, z) * this.pixelRatio; + const isDisplayStrokeAndDelay = radius >= 7 * this.pixelRatio; if (radius === 0) { this.styleCache[key] = null; @@ -444,16 +444,18 @@ const TrajservLayerMixin = (TrackerLayer) => } if (hover || selected) { - radius = isDisplayStrokeAndDelay ? radius + 5 : 14; + radius = isDisplayStrokeAndDelay + ? radius + 5 * this.pixelRatio + : 14 * this.pixelRatio; } - const margin = 1; + const margin = 1 * this.pixelRatio; const radiusDelay = radius + 2; const markerSize = radius * 2; const canvas = document.createElement('canvas'); // add space for delay information - canvas.width = radiusDelay * 2 + margin * 2 + 100; - canvas.height = radiusDelay * 2 + margin * 2 + 100; + canvas.width = radiusDelay * 2 + margin * 2 + 100 * this.pixelRatio; + canvas.height = radiusDelay * 2 + margin * 2 + 100 * this.pixelRatio; const ctx = canvas.getContext('2d'); const origin = canvas.width / 2; @@ -484,7 +486,7 @@ const TrajservLayerMixin = (TrackerLayer) => ctx.fillStyle = getDelayColor(delay, cancelled, true); ctx.strokeStyle = this.delayOutlineColor; - ctx.lineWidth = 1.5; + ctx.lineWidth = 1.5 * this.pixelRatio; const delayText = getDelayText(delay, cancelled); ctx.strokeText(delayText, origin + radiusDelay + margin, origin); ctx.fillText(delayText, origin + radiusDelay + margin, origin); @@ -501,7 +503,7 @@ const TrajservLayerMixin = (TrackerLayer) => ctx.save(); if (isDisplayStrokeAndDelay || hover || selected) { - ctx.lineWidth = 1; + ctx.lineWidth = 1 * this.pixelRatio; ctx.strokeStyle = '#000000'; } ctx.fillStyle = circleFillColor; diff --git a/src/common/mixins/TralisLayerMixin.js b/src/common/mixins/TralisLayerMixin.js index 07319463..229e1b23 100644 --- a/src/common/mixins/TralisLayerMixin.js +++ b/src/common/mixins/TralisLayerMixin.js @@ -19,8 +19,6 @@ export class TralisLayerInterface { * @param {string} options.apiKey Access key for [geOps services](https://developer.geops.io/). * @param {boolean} [options.debug=false] Display additional debug informations. * @param {TralisMode} [options.mode=TralisMode.TOPOGRAPHIC] - Mode. - * @param {number} [options.dfltIconScale=0.6] - Scale of vehicle icons. - * @param {number} [options.dfltIconHighlightScale=0.8] - Scale of vehicle icons when they are highlighted. */ constructor(options = {}) {} @@ -89,12 +87,6 @@ const TralisLayerMixin = (TrackerLayer) => this.onDeleteMessage = this.onDeleteMessage.bind(this); this.api = options.api || new TralisAPI(options); this.format = new GeoJSON(); - - // These scales depends from the size specifed in the svgs. - // For some reason the size must be specified in the svg (../img/lines) for firefox. - this.dfltIconScale = options.dfltIconScale || 0.6; - this.dfltIconHighlightScale = options.dfltIconHighlightScale || 0.8; - this.minIconScale = this.dfltIconScale * 0.75; } init(map) { diff --git a/src/doc/App.js b/src/doc/App.js index 08d7bb5d..1b859287 100644 --- a/src/doc/App.js +++ b/src/doc/App.js @@ -16,8 +16,6 @@ const useStyles = makeStyles({ flexGrow: 1, overflowY: 'auto', paddingBottom: 115, - minHeight: 'calc(100vh - 528px)', - maxWidth: 'calc(85vw + 48px)', margin: 'auto', marginTop: 30, }, diff --git a/src/doc/components/Example.js b/src/doc/components/Example.js index 48ffc256..a7145e7a 100644 --- a/src/doc/components/Example.js +++ b/src/doc/components/Example.js @@ -89,7 +89,7 @@ const Example = () => { }, [example, htmlFileName, jsFileName, readmeFileName]); return ( -
+
diff --git a/src/doc/examples/assets/tralis-live-map/s1kreis.svg b/src/doc/examples/assets/tralis-live-map/s1kreis.svg index a0ab07a4..8a588815 100644 --- a/src/doc/examples/assets/tralis-live-map/s1kreis.svg +++ b/src/doc/examples/assets/tralis-live-map/s1kreis.svg @@ -73,7 +73,7 @@ cy="48.799999" r="12.499999" fill="#00b6e2" - style="stroke-width:0.322997" /> + style="stroke-width: 0.322997;" /> + style="stroke-width: 0.32299;" /> + style="stroke-width: 0.32299;" /> + style="stroke-width: 0.32299;" /> diff --git a/src/doc/examples/mb-tracker.js b/src/doc/examples/mb-tracker.js index 6798d79f..10852312 100644 --- a/src/doc/examples/mb-tracker.js +++ b/src/doc/examples/mb-tracker.js @@ -10,8 +10,6 @@ export default () => { zoom: 12, touchPitch: false, pitchWithRotate: false, - dragRotate: false, - touchZoomRotate: false, }); const tracker = new TrajservLayer({ diff --git a/src/doc/examples/tralis-live-map.js b/src/doc/examples/tralis-live-map.js index 490724f5..4496afe1 100644 --- a/src/doc/examples/tralis-live-map.js +++ b/src/doc/examples/tralis-live-map.js @@ -11,16 +11,17 @@ export default () => { zoom: 10, touchPitch: false, pitchWithRotate: false, - dragRotate: false, - touchZoomRotate: false, }); const tracker = new TralisLayer({ url: 'wss://api.geops.io/realtime-ws/v1/', apiKey: window.apiKey, + bbox: [1152072, 6048052, 1433666, 6205578], style: (props) => { const img = new Image(); img.src = LINE_IMAGES[(props.line || {}).name || 'unknown']; + img.width = 25 * window.devicePixelRatio; + img.height = 25 * window.devicePixelRatio; return img; }, }); diff --git a/src/mapbox/layers/TrackerLayer.js b/src/mapbox/layers/TrackerLayer.js index fb02a88f..2e6d5b29 100644 --- a/src/mapbox/layers/TrackerLayer.js +++ b/src/mapbox/layers/TrackerLayer.js @@ -21,14 +21,6 @@ class TrackerLayer extends mixin(Layer) { this.onMapMouseMove = this.onMapMouseMove.bind(this); } - /** - * Update the icon scale if the window (and probably the canvas) is resized. - * @private - */ - updateIconScale(canvas) { - this.tracker.setIconScale(canvas.width / canvas.clientWidth); - } - /** * Initialize the layer. * @@ -41,44 +33,18 @@ class TrackerLayer extends mixin(Layer) { } const canvas = map.getCanvas(); - const iconScale = canvas.width / canvas.clientWidth; - map.on('resize', this.updateIconScale.bind(this, canvas)); super.init(map, { - width: canvas.width, - height: canvas.height, - iconScale, + width: canvas.width / this.pixelRatio, + height: canvas.height / this.pixelRatio, getPixelFromCoordinate: (coord) => { - const pixelRatio = window.devicePixelRatio || 1; const [lng, lat] = toLonLat(coord); const { x, y } = this.map.project({ lng, lat }); - return [x * pixelRatio, y * pixelRatio]; + return [x, y]; }, }); } - terminate() { - if (this.map) { - this.map.off('resize', this.updateIconScale); - } - return super.terminate(); - } - - /** - * Set the current time, it triggers a rendering of the trajectories. - * - * @param {Date} time The current time. - */ - setCurrTime(time) { - const canvas = this.map.getCanvas(); - super.setCurrTime( - time, - [canvas.width, canvas.height], - getResolution(this.map), - !this.map.isMoving() && !this.map.isRotating() && !this.map.isZooming(), - ); - } - /** * Start updating vehicles position. * @@ -87,12 +53,7 @@ class TrackerLayer extends mixin(Layer) { * @override */ start() { - const canvas = this.map.getCanvas(); - super.start( - [canvas.width, canvas.height], - this.map.getZoom(), - getResolution(this.map), - ); + super.start(); this.map.on('zoomend', this.onMapZoomEnd); @@ -114,6 +75,28 @@ class TrackerLayer extends mixin(Layer) { } } + /** + * Render the trajectories using current map's size, resolution and rotation. + * @param {boolean} noInterpolate if true, renders the vehicles without interpolating theirs positions. + * @overrides + */ + renderTrajectories(noInterpolate) { + const canvas = this.map.getCanvas(); + super.renderTrajectories( + [canvas.width / this.pixelRatio, canvas.height / this.pixelRatio], + getResolution(this.map), + this.map.getBearing(), + noInterpolate, + ); + } + + /** + * Return the delay in ms before the next rendering. + */ + getRefreshTimeInMs() { + return super.getRefreshTimeInMs(this.map.getZoom()); + } + /** * Returns an array of vehicles located at the given coordinate. * @@ -162,7 +145,7 @@ class TrackerLayer extends mixin(Layer) { this.map.getContainer().style.cursor = vehicle ? 'pointer' : 'auto'; this.hoverVehicleId = id; // We doesn´t wait the next render, we force it. - this.renderTrajectories(this.currTime); + this.renderTrajectories(); } } } diff --git a/src/mapbox/layers/TrajservLayer.js b/src/mapbox/layers/TrajservLayer.js index 88654d12..1ca39071 100644 --- a/src/mapbox/layers/TrajservLayer.js +++ b/src/mapbox/layers/TrajservLayer.js @@ -45,14 +45,10 @@ class TrajservLayer extends mixin(TrackerLayer) { super.init(map); - const { width, height } = map.getCanvas(); - this.tracker.canvas.width = width; - this.tracker.canvas.height = height; - const source = { type: 'canvas', canvas: this.tracker.canvas, - coordinates: getSourceCoordinates(map), + coordinates: getSourceCoordinates(map, this.pixelRatio), // 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, attribution: this.copyrights, @@ -132,14 +128,13 @@ class TrajservLayer extends mixin(TrackerLayer) { * @private */ onMove() { - this.map.getSource(this.key).setCoordinates(getSourceCoordinates(this.map)); - - const { width, height } = this.map.getCanvas(); - + this.map + .getSource(this.key) + .setCoordinates(getSourceCoordinates(this.map, this.pixelRatio)); this.renderTrajectories( - this.currTime, - [width, height], + undefined, getResolution(this.map), + this.map.getBearing(), ); } diff --git a/src/mapbox/layers/TralisLayer.js b/src/mapbox/layers/TralisLayer.js index 1686669d..7a2b3300 100644 --- a/src/mapbox/layers/TralisLayer.js +++ b/src/mapbox/layers/TralisLayer.js @@ -46,14 +46,10 @@ class TralisLayer extends mixin(TrackerLayer) { this.map.on('move', this.onMove); this.map.on('moveend', this.onMoveEnd); - const { width, height } = this.map.getCanvas(); - this.tracker.canvas.width = width; - this.tracker.canvas.height = height; - this.map.addSource('canvas-source', { type: 'canvas', canvas: this.tracker.canvas, - coordinates: getSourceCoordinates(this.map), + coordinates: getSourceCoordinates(this.map, this.pixelRatio), // 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, }); @@ -91,12 +87,13 @@ class TralisLayer extends mixin(TrackerLayer) { onMove() { this.map .getSource('canvas-source') - .setCoordinates(getSourceCoordinates(this.map)); + .setCoordinates(getSourceCoordinates(this.map, this.pixelRatio)); + const { width, height } = this.map.getCanvas(); this.renderTrajectories( - this.currTime, - [width, height], + [width / this.pixelRatio, height / this.pixelRatio], getResolution(this.map), + this.map.getBearing(), ); } diff --git a/src/mapbox/utils.js b/src/mapbox/utils.js index adf1f9dc..53c80426 100644 --- a/src/mapbox/utils.js +++ b/src/mapbox/utils.js @@ -20,13 +20,21 @@ export const getResolution = (map) => { * @param {mapboxgl.Map} map A map object. * @private */ -export const getSourceCoordinates = (map) => { - const bounds = map.getBounds().toArray(); +export const getSourceCoordinates = (map, pixelRatio) => { + // Requesting getBounds is not enough when we rotate the map, so we request manually each corner. + const { width, height } = map.getCanvas(); + const leftTop = map.unproject({ x: 0, y: 0 }); + const leftBottom = map.unproject({ x: 0, y: height / pixelRatio }); // southWest + const rightBottom = map.unproject({ + x: width / pixelRatio, + y: height / pixelRatio, + }); + const rightTop = map.unproject({ x: width / pixelRatio, y: 0 }); // north east return [ - [bounds[0][0], bounds[1][1]], - [...bounds[1]], - [bounds[1][0], bounds[0][1]], - [...bounds[0]], + [leftTop.lng, leftTop.lat], + [rightTop.lng, rightTop.lat], + [rightBottom.lng, rightBottom.lat], + [leftBottom.lng, leftBottom.lat], ]; }; diff --git a/src/ol/layers/MapboxStyleLayer.js b/src/ol/layers/MapboxStyleLayer.js index 4bdaab13..ab8d0a0e 100644 --- a/src/ol/layers/MapboxStyleLayer.js +++ b/src/ol/layers/MapboxStyleLayer.js @@ -396,7 +396,8 @@ class MapboxStyleLayer extends Layer { * @param {Event} evt Layer's event that has called the function. * @private */ - applyLayoutVisibility() { + // eslint-disable-next-line no-unused-vars + applyLayoutVisibility(evt) { const { visible } = this; const { mbMap } = this.mapboxLayer; const filterFunc = this.styleLayersFilter; diff --git a/src/ol/layers/TrackerLayer.js b/src/ol/layers/TrackerLayer.js index b57aed34..f1165a29 100644 --- a/src/ol/layers/TrackerLayer.js +++ b/src/ol/layers/TrackerLayer.js @@ -27,11 +27,18 @@ class TrackerLayer extends mixin(Layer) { /** * Function to define when allowing the render of trajectories depending on the zoom level. Default the fundtion return true. - * It's useful to avoid rendering the map when the map is animating or interacting + * It's useful to avoid rendering the map when the map is animating or interacting. * @type {function} */ this.renderWhenZooming = options.renderWhenZooming || (() => true); + /** + * Function to define when allowing the render of trajectories depending on the rotation. Default the fundtion return true. + * It's useful to avoid rendering the map when the map is animating or interacting. + * @type {function} + */ + this.renderWhenRotating = options.renderWhenRotating || (() => true); + this.olLayer = options.olLayer || new Group({ @@ -42,23 +49,27 @@ class TrackerLayer extends mixin(Layer) { if (!this.tracker || !this.tracker.canvas) { return null; } - const { zoom, center, resolution } = frameState.viewState; + const { zoom, center, rotation } = frameState.viewState; - if (zoom !== this.renderState.zoom) { + if ( + zoom !== this.renderState.zoom || + rotation !== this.renderState.rotation + ) { this.renderState.zoom = zoom; this.renderState.center = center; - - if (!this.renderWhenZooming(zoom)) { + this.renderState.rotation = rotation; + + if ( + (zoom !== this.renderState.zoom && + !this.renderWhenZooming(zoom)) || + (rotation !== this.renderState.rotation && + !this.renderWhenRotating(rotation)) + ) { this.tracker.clear(); return this.tracker.canvas; } - this.renderTrajectories( - this.currTime, - frameState.size, - resolution, - true, - ); + this.renderTrajectories(true); } else if ( this.renderState.center[0] !== center[0] || this.renderState.center[1] !== center[1] @@ -67,8 +78,19 @@ class TrackerLayer extends mixin(Layer) { const oldPx = this.map.getPixelFromCoordinate( this.renderState.center, ); - this.tracker.moveCanvas(px[0] - oldPx[0], px[1] - oldPx[1]); + + // We move the canvas to avoid re render the trajectories + const oldLeft = parseFloat(this.tracker.canvas.style.left); + const oldTop = parseFloat(this.tracker.canvas.style.top); + this.tracker.canvas.style.left = `${ + oldLeft - (px[0] - oldPx[0]) + }px`; + this.tracker.canvas.style.top = `${ + oldTop - (px[1] - oldPx[1]) + }px`; + this.renderState.center = center; + this.renderState.rotation = rotation; } return this.tracker.canvas; @@ -83,6 +105,7 @@ class TrackerLayer extends mixin(Layer) { this.renderState = { center: [0, 0], zoom: null, + rotation: 0, }; /** @@ -108,31 +131,12 @@ class TrackerLayer extends mixin(Layer) { }); } - /** - * Set the current time, it triggers a rendering of the trajectories. - * @param {dateString | value} time - */ - setCurrTime(time) { - const view = this.map.getView(); - super.setCurrTime( - time, - this.map.getSize(), - view.getResolution(), - !this.map.getView().getAnimating() && - !this.map.getView().getInteracting(), - ); - } - /** * Trackerlayer is started. * @private */ start() { - super.start( - this.map.getSize(), - this.currentZoom || this.map.getView().getZoom(), - this.map.getView().getResolution(), - ); + super.start(); this.olEventsKeys = [ this.map.on('moveend', () => { @@ -162,9 +166,7 @@ class TrackerLayer extends mixin(Layer) { ? 'pointer' : 'auto'; this.hoverVehicleId = id; - - // We doesn´t wait the next render, we force it. - this.renderTrajectories(this.currTime); + this.renderTrajectories(); } }), ]; @@ -180,6 +182,28 @@ class TrackerLayer extends mixin(Layer) { this.olEventsKeys = []; } + /** + * Render the trajectories using current map's size, resolution and rotation. + * @param {boolean} noInterpolate if true, renders the vehicles without interpolating theirs positions. + * @overrides + */ + renderTrajectories(noInterpolate) { + const view = this.map.getView(); + super.renderTrajectories( + this.map.getSize(), + view.getResolution(), + view.getRotation(), + noInterpolate, + ); + } + + /** + * Return the delay in ms before the next rendering. + */ + getRefreshTimeInMs() { + return super.getRefreshTimeInMs(this.map.getView().getZoom()); + } + /** * Returns the vehicle which are at the given coordinates. * Returns null when no vehicle is located at the given coordinates. From e1b5acd46c3f777eafa36e0bc92fffb9c9068386 Mon Sep 17 00:00:00 2001 From: Olivier Terral Date: Mon, 11 Oct 2021 09:02:25 +0200 Subject: [PATCH 3/3] v1.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ddc5cd31..737435f1 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "mobility-toolbox-js", "license": "MIT", "description": "Toolbox for JavaScript applications in the domains of mobility and logistics.", - "version": "1.3.13-beta.8", + "version": "1.4.0", "main": "index.js", "module": "module.js", "dependencies": {