From 3944a5a0387e4a686aac2033c13153d8cc340c33 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Wed, 4 Mar 2020 15:59:52 +0100 Subject: [PATCH 01/25] Make DEVICE_PIXEL_RATIO work in non-window context --- src/ol/has.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ol/has.js b/src/ol/has.js index bfc8aabef3..9bf3e354bf 100644 --- a/src/ol/has.js +++ b/src/ol/has.js @@ -37,7 +37,13 @@ export const MAC = ua.indexOf('macintosh') !== -1; * @type {number} * @api */ -export const DEVICE_PIXEL_RATIO = window.devicePixelRatio || 1; +export const DEVICE_PIXEL_RATIO = (function() { + try { + return self.devicePixelRatio; + } catch (e) { + return window.devicePixelRatio || 1; + } +})(); /** * Image.prototype.decode() is supported. From f896d9fb03d06a7743a194ad55897026002f934e Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Wed, 4 Mar 2020 16:01:43 +0100 Subject: [PATCH 02/25] Move tile priority function to the TileQueue module --- src/ol/PluggableMap.js | 24 ++---------------------- src/ol/TileQueue.js | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/src/ol/PluggableMap.js b/src/ol/PluggableMap.js index eb6c7646e4..792215e1cc 100644 --- a/src/ol/PluggableMap.js +++ b/src/ol/PluggableMap.js @@ -12,7 +12,7 @@ import MapProperty from './MapProperty.js'; import RenderEventType from './render/EventType.js'; import BaseObject, {getChangeEventType} from './Object.js'; import ObjectEventType from './ObjectEventType.js'; -import TileQueue from './TileQueue.js'; +import TileQueue, {getTilePriority} from './TileQueue.js'; import View from './View.js'; import ViewHint from './ViewHint.js'; import {assert} from './asserts.js'; @@ -24,7 +24,6 @@ import {TRUE} from './functions.js'; import {DEVICE_PIXEL_RATIO, IMAGE_DECODE, PASSIVE_EVENT_LISTENERS} from './has.js'; import LayerGroup from './layer/Group.js'; import {hasArea} from './size.js'; -import {DROP} from './structs/PriorityQueue.js'; import {create as createTransform, apply as applyTransform} from './transform.js'; import {toUserCoordinate, fromUserCoordinate} from './proj.js'; @@ -891,26 +890,7 @@ class PluggableMap extends BaseObject { * @return {number} Tile priority. */ getTilePriority(tile, tileSourceKey, tileCenter, tileResolution) { - // Filter out tiles at higher zoom levels than the current zoom level, or that - // are outside the visible extent. - const frameState = this.frameState_; - if (!frameState || !(tileSourceKey in frameState.wantedTiles)) { - return DROP; - } - if (!frameState.wantedTiles[tileSourceKey][tile.getKey()]) { - return DROP; - } - // Prioritize the highest zoom level tiles closest to the focus. - // Tiles at higher zoom levels are prioritized using Math.log(tileResolution). - // Within a zoom level, tiles are prioritized by the distance in pixels between - // the center of the tile and the center of the viewport. The factor of 65536 - // means that the prioritization should behave as desired for tiles up to - // 65536 * Math.log(2) = 45426 pixels from the focus. - const center = frameState.viewState.center; - const deltaX = tileCenter[0] - center[0]; - const deltaY = tileCenter[1] - center[1]; - return 65536 * Math.log(tileResolution) + - Math.sqrt(deltaX * deltaX + deltaY * deltaY) / tileResolution; + return getTilePriority(this.frameState_, tile, tileSourceKey, tileCenter, tileResolution); } /** diff --git a/src/ol/TileQueue.js b/src/ol/TileQueue.js index 994a8eeb1d..eb39b119d8 100644 --- a/src/ol/TileQueue.js +++ b/src/ol/TileQueue.js @@ -3,7 +3,7 @@ */ import TileState from './TileState.js'; import EventType from './events/EventType.js'; -import PriorityQueue from './structs/PriorityQueue.js'; +import PriorityQueue, {DROP} from './structs/PriorityQueue.js'; /** @@ -119,3 +119,34 @@ class TileQueue extends PriorityQueue { export default TileQueue; + + +/** + * @param {import('./PluggableMap.js').FrameState} frameState Frame state. + * @param {import("./Tile.js").default} tile Tile. + * @param {string} tileSourceKey Tile source key. + * @param {import("./coordinate.js").Coordinate} tileCenter Tile center. + * @param {number} tileResolution Tile resolution. + * @return {number} Tile priority. + */ +export function getTilePriority(frameState, tile, tileSourceKey, tileCenter, tileResolution) { + // Filter out tiles at higher zoom levels than the current zoom level, or that + // are outside the visible extent. + if (!frameState || !(tileSourceKey in frameState.wantedTiles)) { + return DROP; + } + if (!frameState.wantedTiles[tileSourceKey][tile.getKey()]) { + return DROP; + } + // Prioritize the highest zoom level tiles closest to the focus. + // Tiles at higher zoom levels are prioritized using Math.log(tileResolution). + // Within a zoom level, tiles are prioritized by the distance in pixels between + // the center of the tile and the center of the viewport. The factor of 65536 + // means that the prioritization should behave as desired for tiles up to + // 65536 * Math.log(2) = 45426 pixels from the focus. + const center = frameState.viewState.center; + const deltaX = tileCenter[0] - center[0]; + const deltaY = tileCenter[1] - center[1]; + return 65536 * Math.log(tileResolution) + + Math.sqrt(deltaX * deltaX + deltaY * deltaY) / tileResolution; +} From 717b8ad0cf90f5505fb238d1f00366725e6ded28 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Wed, 4 Mar 2020 16:02:07 +0100 Subject: [PATCH 03/25] Make createCanvasContext2D work in non-window context --- src/ol/dom.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/ol/dom.js b/src/ol/dom.js index ad10c14671..37d188541e 100644 --- a/src/ol/dom.js +++ b/src/ol/dom.js @@ -3,6 +3,7 @@ */ +//FIXME Move this function to the canvas module /** * Create an html canvas element and returns its 2d context. * @param {number=} opt_width Canvas width. @@ -12,14 +13,18 @@ */ export function createCanvasContext2D(opt_width, opt_height, opt_canvasPool) { const canvas = opt_canvasPool && opt_canvasPool.length ? - opt_canvasPool.shift() : document.createElement('canvas'); + opt_canvasPool.shift() : + 'document' in self ? + document.createElement('canvas') : + new OffscreenCanvas(opt_width || 300, opt_height || 300); if (opt_width) { canvas.width = opt_width; } if (opt_height) { canvas.height = opt_height; } - return canvas.getContext('2d'); + //FIXME Allow OffscreenCanvasRenderingContext2D as return type + return /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d')); } From 8b76f52652f9a6bf8fb2433fd6d41d213a65e34f Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Wed, 4 Mar 2020 16:03:09 +0100 Subject: [PATCH 04/25] Make createTransformString work in non-window context --- src/ol/renderer/canvas/Layer.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/ol/renderer/canvas/Layer.js b/src/ol/renderer/canvas/Layer.js index d082f13923..5be2536e07 100644 --- a/src/ol/renderer/canvas/Layer.js +++ b/src/ol/renderer/canvas/Layer.js @@ -73,7 +73,7 @@ class CanvasLayerRenderer extends LayerRenderer { * @type {HTMLCanvasElement} * @private */ - this.createTransformStringCanvas_ = createCanvasContext2D(1, 1).canvas; + this.createTransformStringCanvas_ = 'document' in self ? createCanvasContext2D(1, 1).canvas : null; } @@ -274,8 +274,12 @@ class CanvasLayerRenderer extends LayerRenderer { * @return {string} CSS transform. */ createTransformString(transform) { - this.createTransformStringCanvas_.style.transform = toString(transform); - return this.createTransformStringCanvas_.style.transform; + if (this.createTransformStringCanvas_) { + this.createTransformStringCanvas_.style.transform = toString(transform); + return this.createTransformStringCanvas_.style.transform; + } else { + return toString(transform); + } } } From 3f5022630b72ed0185d15fa96231285cfd263134 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Wed, 4 Mar 2020 16:06:18 +0100 Subject: [PATCH 05/25] Create a basic example for OffscreenCanvas rendering --- examples/mvtlayer.worker.js | 75 ++++++++++++++++++++++++++++ examples/offscreen-canvas-tiles.html | 9 ++++ examples/offscreen-canvas-tiles.js | 63 +++++++++++++++++++++++ package-lock.json | 22 ++++++++ package.json | 1 + 5 files changed, 170 insertions(+) create mode 100644 examples/mvtlayer.worker.js create mode 100644 examples/offscreen-canvas-tiles.html create mode 100644 examples/offscreen-canvas-tiles.js diff --git a/examples/mvtlayer.worker.js b/examples/mvtlayer.worker.js new file mode 100644 index 0000000000..6de3774f1d --- /dev/null +++ b/examples/mvtlayer.worker.js @@ -0,0 +1,75 @@ +import VectorTileLayer from '../src/ol/layer/VectorTile.js'; +import VectorTileSource from '../src/ol/source/VectorTile.js'; +import MVT from '../src/ol/format/MVT.js'; +import {Projection} from '../src/ol/proj.js'; +import TileQueue from '../src/ol/TileQueue.js'; +import {getTilePriority as tilePriorityFunction} from '../src/ol/TileQueue.js'; + +const key = 'pk.eyJ1IjoiYWhvY2V2YXIiLCJhIjoiY2pzbmg0Nmk5MGF5NzQzbzRnbDNoeHJrbiJ9.7_-_gL8ur7ZtEiNwRfCy7Q'; + +/** @type {any} */ +const worker = self; + +let frameState; + +function getTilePriority(tile, tileSourceKey, tileCenter, tileResolution) { + return tilePriorityFunction(frameState, tile, tileSourceKey, tileCenter, tileResolution); +} + +const layer = new VectorTileLayer({ + declutter: true, + source: new VectorTileSource({ + format: new MVT(), + url: 'https://{a-d}.tiles.mapbox.com/v4/mapbox.mapbox-streets-v6/' + + '{z}/{x}/{y}.vector.pbf?access_token=' + key + }) +}); +const tileQueue = new TileQueue(getTilePriority, function() { + worker.postMessage({type: 'render'}); +}); +const maxTotalLoading = 8; +const maxNewLoads = 2; + +const renderer = layer.getRenderer(); + +renderer.useContainer = function(target, transform, opacity) { + target.style = {}; + this.canvas = target; + this.context = target.getContext('2d'); + this.container = { + firstElementChild: target + }; + worker.postMessage({ + type: 'transform-opacity', + transform: transform, + opacity: opacity + }); +}; + +let canvas; +let rendering = false; + +worker.onmessage = function(event) { + if (rendering) { + // drop this frame + return; + } + if (event.data.canvas) { + canvas = event.data.canvas; + } else { + frameState = event.data.frameState; + frameState.tileQueue = tileQueue; + frameState.viewState.projection.__proto__ = Projection.prototype; + rendering = true; + requestAnimationFrame(function() { + renderer.renderFrame(frameState, canvas); + if (tileQueue.getTilesLoading() < maxTotalLoading) { + tileQueue.reprioritize(); // FIXME only call if view has changed + tileQueue.loadMoreTiles(maxTotalLoading, maxNewLoads); + } + rendering = false; + }); + } +}; + +export let create; diff --git a/examples/offscreen-canvas-tiles.html b/examples/offscreen-canvas-tiles.html new file mode 100644 index 0000000000..231da2f14f --- /dev/null +++ b/examples/offscreen-canvas-tiles.html @@ -0,0 +1,9 @@ +--- +layout: example.html +title: Vector tiles rendered in an offscreen canvas +shortdesc: Example of a map that delegates rendering to a worker. +docs: > + The map in this example is rendered in a web worker, using `OffscreenCanvas`. **Note:** This is currently only supported in Chrome and Edge. +tags: "worker, offscreencanvas, vector-tiles" +--- +
diff --git a/examples/offscreen-canvas-tiles.js b/examples/offscreen-canvas-tiles.js new file mode 100644 index 0000000000..af9f43e03f --- /dev/null +++ b/examples/offscreen-canvas-tiles.js @@ -0,0 +1,63 @@ +import Map from '../src/ol/Map.js'; +import View from '../src/ol/View.js'; +import Layer from '../src/ol/layer/Layer.js'; +//eslint-disable-next-line +import Worker from 'worker-loader!./mvtlayer.worker.js'; + +const mvtLayerWorker = new Worker(); + +function getCircularReplacer() { + const seen = new WeakSet(); + return function(key, value) { + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) { + return '[circular]'; + } + seen.add(value); + } + return value; + }; +} + +let container, canvas; + +const map = new Map({ + layers: [ + new Layer({ + render: function(frameState) { + if (!container) { + container = document.createElement('div'); + container.style.position = 'absolute'; + container.style.width = '100%'; + container.style.height = '100%'; + canvas = document.createElement('canvas'); + canvas.style.position = 'absolute'; + canvas.style.left = '0'; + canvas.style.transformOrigin = 'top left'; + container.appendChild(canvas); + const offscreen = canvas.transferControlToOffscreen(); + mvtLayerWorker.postMessage({ + canvas: offscreen + }, [offscreen]); + } + mvtLayerWorker.postMessage({ + frameState: JSON.parse(JSON.stringify(frameState, getCircularReplacer())) + }); + return container; + } + }) + ], + target: 'map', + view: new View({ + center: [0, 0], + zoom: 2 + }) +}); +mvtLayerWorker.addEventListener('message', message => { + if (message.data.type === 'render') { + map.render(); + } else if (canvas && message.data.type === 'transform-opacity') { + canvas.style.transform = message.data.transform; + canvas.style.opacity = message.data.opacity; + } +}); diff --git a/package-lock.json b/package-lock.json index 9bced339b2..450a9667f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12693,6 +12693,28 @@ "errno": "~0.1.7" } }, + "worker-loader": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-2.0.0.tgz", + "integrity": "sha512-tnvNp4K3KQOpfRnD20m8xltE3eWh89Ye+5oj7wXEEHKac1P4oZ6p9oTj8/8ExqoSBnk9nu5Pr4nKfQ1hn2APJw==", + "dev": true, + "requires": { + "loader-utils": "^1.0.0", + "schema-utils": "^0.4.0" + }, + "dependencies": { + "schema-utils": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", + "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, "wrap-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", diff --git a/package.json b/package.json index 2fa01fd358..d9adcfa006 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ "webpack-cli": "^3.3.2", "webpack-dev-middleware": "^3.6.2", "webpack-dev-server": "^3.3.1", + "worker-loader": "^2.0.0", "yargs": "^15.0.2" }, "eslintConfig": { From 56edbb2d736d1f8aad6b03075d57d939d9f16513 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Thu, 12 Mar 2020 11:46:40 +0100 Subject: [PATCH 06/25] Make createTransformToString a standalone function --- src/ol/has.js | 14 ++++++++++++++ src/ol/render/canvas.js | 24 ++++++++++++++++++++++++ src/ol/renderer/canvas/ImageLayer.js | 3 ++- src/ol/renderer/canvas/Layer.js | 22 ++-------------------- src/ol/renderer/canvas/TileLayer.js | 3 ++- 5 files changed, 44 insertions(+), 22 deletions(-) diff --git a/src/ol/has.js b/src/ol/has.js index 9bf3e354bf..24b053a2fe 100644 --- a/src/ol/has.js +++ b/src/ol/has.js @@ -45,6 +45,20 @@ export const DEVICE_PIXEL_RATIO = (function() { } })(); +/** + * The execution context is a window. + * @const + * @type {boolean} + */ +export const WINDOW = (function() { + try { + return 'document' in self; + } catch (e) { + // ancient browsers don't have `self` + return true; + } +})(); + /** * Image.prototype.decode() is supported. * @type {boolean} diff --git a/src/ol/render/canvas.js b/src/ol/render/canvas.js index adb892dd34..04781b9bc1 100644 --- a/src/ol/render/canvas.js +++ b/src/ol/render/canvas.js @@ -6,6 +6,8 @@ import {createCanvasContext2D} from '../dom.js'; import {clear} from '../obj.js'; import BaseObject from '../Object.js'; import EventTarget from '../events/Target.js'; +import {WINDOW} from '../has.js'; +import {toString} from '../transform.js'; /** @@ -438,3 +440,25 @@ function executeLabelInstructions(label, context) { } } } + +/** + * @type {HTMLCanvasElement} + * @private + */ +let createTransformStringCanvas = null; + +/** + * @param {import("../transform.js").Transform} transform Transform. + * @return {string} CSS transform. + */ +export function createTransformString(transform) { + if (WINDOW) { + if (!createTransformStringCanvas) { + createTransformStringCanvas = createCanvasContext2D(1, 1).canvas; + } + createTransformStringCanvas.style.transform = toString(transform); + return createTransformStringCanvas.style.transform; + } else { + return toString(transform); + } +} diff --git a/src/ol/renderer/canvas/ImageLayer.js b/src/ol/renderer/canvas/ImageLayer.js index 7403890219..bd11ac74b3 100644 --- a/src/ol/renderer/canvas/ImageLayer.js +++ b/src/ol/renderer/canvas/ImageLayer.js @@ -8,6 +8,7 @@ import {fromUserExtent} from '../../proj.js'; import {getIntersection, isEmpty} from '../../extent.js'; import CanvasLayerRenderer from './Layer.js'; import {compose as composeTransform, makeInverse} from '../../transform.js'; +import {createTransformString} from '../../render/canvas.js'; /** * @classdesc @@ -109,7 +110,7 @@ class CanvasImageLayerRenderer extends CanvasLayerRenderer { ); makeInverse(this.inversePixelTransform, this.pixelTransform); - const canvasTransform = this.createTransformString(this.pixelTransform); + const canvasTransform = createTransformString(this.pixelTransform); this.useContainer(target, canvasTransform, layerState.opacity); diff --git a/src/ol/renderer/canvas/Layer.js b/src/ol/renderer/canvas/Layer.js index 5be2536e07..57338822b1 100644 --- a/src/ol/renderer/canvas/Layer.js +++ b/src/ol/renderer/canvas/Layer.js @@ -7,7 +7,7 @@ import RenderEvent from '../../render/Event.js'; import RenderEventType from '../../render/EventType.js'; import {rotateAtOffset} from '../../render/canvas.js'; import LayerRenderer from '../Layer.js'; -import {create as createTransform, apply as applyTransform, compose as composeTransform, toString} from '../../transform.js'; +import {create as createTransform, apply as applyTransform, compose as composeTransform} from '../../transform.js'; /** * @abstract @@ -69,12 +69,6 @@ class CanvasLayerRenderer extends LayerRenderer { */ this.containerReused = false; - /** - * @type {HTMLCanvasElement} - * @private - */ - this.createTransformStringCanvas_ = 'document' in self ? createCanvasContext2D(1, 1).canvas : null; - } /** @@ -269,19 +263,7 @@ class CanvasLayerRenderer extends LayerRenderer { return data; } - /** - * @param {import("../../transform.js").Transform} transform Transform. - * @return {string} CSS transform. - */ - createTransformString(transform) { - if (this.createTransformStringCanvas_) { - this.createTransformStringCanvas_.style.transform = toString(transform); - return this.createTransformStringCanvas_.style.transform; - } else { - return toString(transform); - } - } - } export default CanvasLayerRenderer; + diff --git a/src/ol/renderer/canvas/TileLayer.js b/src/ol/renderer/canvas/TileLayer.js index 51fb12939f..51a4d8a372 100644 --- a/src/ol/renderer/canvas/TileLayer.js +++ b/src/ol/renderer/canvas/TileLayer.js @@ -9,6 +9,7 @@ import {createEmpty, equals, getIntersection, getTopLeft} from '../../extent.js' import CanvasLayerRenderer from './Layer.js'; import {apply as applyTransform, compose as composeTransform, makeInverse} from '../../transform.js'; import {numberSafeCompareFunction} from '../../array.js'; +import {createTransformString} from '../../render/canvas.js'; /** * @classdesc @@ -243,7 +244,7 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer { -width / 2, -height / 2 ); - const canvasTransform = this.createTransformString(this.pixelTransform); + const canvasTransform = createTransformString(this.pixelTransform); this.useContainer(target, canvasTransform, layerState.opacity); const context = this.context; From a93edb338b85eba13128f122039e6bdf9a173d5d Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Fri, 13 Mar 2020 15:52:46 +0100 Subject: [PATCH 07/25] Instant UI feedback --- examples/mvtlayer.worker.js | 61 +++++++++++++++++++----------- examples/offscreen-canvas-tiles.js | 53 +++++++++++++++++++++----- 2 files changed, 81 insertions(+), 33 deletions(-) diff --git a/examples/mvtlayer.worker.js b/examples/mvtlayer.worker.js index 6de3774f1d..bc2ab5a20b 100644 --- a/examples/mvtlayer.worker.js +++ b/examples/mvtlayer.worker.js @@ -11,6 +11,20 @@ const key = 'pk.eyJ1IjoiYWhvY2V2YXIiLCJhIjoiY2pzbmg0Nmk5MGF5NzQzbzRnbDNoeHJrbiJ9 const worker = self; let frameState; +const canvas = new OffscreenCanvas(1, 1); + +function getCircularReplacer() { + const seen = new WeakSet(); + return function(key, value) { + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) { + return '[circular]'; + } + seen.add(value); + } + return value; + }; +} function getTilePriority(tile, tileSourceKey, tileCenter, tileResolution) { return tilePriorityFunction(frameState, tile, tileSourceKey, tileCenter, tileResolution); @@ -24,13 +38,13 @@ const layer = new VectorTileLayer({ '{z}/{x}/{y}.vector.pbf?access_token=' + key }) }); +const renderer = layer.getRenderer(); const tileQueue = new TileQueue(getTilePriority, function() { - worker.postMessage({type: 'render'}); + worker.postMessage({type: 'request-render'}); }); const maxTotalLoading = 8; const maxNewLoads = 2; -const renderer = layer.getRenderer(); renderer.useContainer = function(target, transform, opacity) { target.style = {}; @@ -39,37 +53,38 @@ renderer.useContainer = function(target, transform, opacity) { this.container = { firstElementChild: target }; - worker.postMessage({ - type: 'transform-opacity', - transform: transform, - opacity: opacity + layer.once('postrender', function() { + const imageData = canvas.transferToImageBitmap(); + worker.postMessage({ + type: 'rendered', + imageData: imageData, + transform: transform, + opacity: opacity, + frameState: JSON.parse(JSON.stringify(frameState, getCircularReplacer())) + }, [imageData]); }); }; -let canvas; let rendering = false; worker.onmessage = function(event) { if (rendering) { // drop this frame + worker.postMessage({type: 'request-render'}); return; } - if (event.data.canvas) { - canvas = event.data.canvas; - } else { - frameState = event.data.frameState; - frameState.tileQueue = tileQueue; - frameState.viewState.projection.__proto__ = Projection.prototype; - rendering = true; - requestAnimationFrame(function() { - renderer.renderFrame(frameState, canvas); - if (tileQueue.getTilesLoading() < maxTotalLoading) { - tileQueue.reprioritize(); // FIXME only call if view has changed - tileQueue.loadMoreTiles(maxTotalLoading, maxNewLoads); - } - rendering = false; - }); - } + frameState = event.data.frameState; + frameState.tileQueue = tileQueue; + frameState.viewState.projection.__proto__ = Projection.prototype; + rendering = true; + requestAnimationFrame(function() { + renderer.renderFrame(frameState, canvas); + if (tileQueue.getTilesLoading() < maxTotalLoading) { + tileQueue.reprioritize(); // FIXME only call if view has changed + tileQueue.loadMoreTiles(maxTotalLoading, maxNewLoads); + } + rendering = false; + }); }; export let create; diff --git a/examples/offscreen-canvas-tiles.js b/examples/offscreen-canvas-tiles.js index af9f43e03f..0e619526aa 100644 --- a/examples/offscreen-canvas-tiles.js +++ b/examples/offscreen-canvas-tiles.js @@ -3,6 +3,8 @@ import View from '../src/ol/View.js'; import Layer from '../src/ol/layer/Layer.js'; //eslint-disable-next-line import Worker from 'worker-loader!./mvtlayer.worker.js'; +import {compose, create} from '../src/ol/transform.js'; +import {createTransformString} from '../src/ol/render/canvas.js'; const mvtLayerWorker = new Worker(); @@ -19,7 +21,28 @@ function getCircularReplacer() { }; } -let container, canvas; +let container, transformContainer, canvas, workerFrameState, mainThreadFrameState; + +function updateContainerTransform() { + if (workerFrameState) { + const viewState = mainThreadFrameState.viewState; + const renderedViewState = workerFrameState.viewState; + const center = viewState.center; + const resolution = viewState.resolution; + const rotation = viewState.rotation; + const renderedCenter = renderedViewState.center; + const renderedResolution = renderedViewState.resolution; + const renderedRotation = renderedViewState.rotation; + const transform = compose(create(), + (renderedCenter[0] - center[0]) / resolution, + (center[1] - renderedCenter[1]) / resolution, + renderedResolution / resolution, renderedResolution / resolution, + rotation - renderedRotation, + 0, 0); + transformContainer.style.transform = createTransformString(transform); + } + +} const map = new Map({ layers: [ @@ -30,16 +53,19 @@ const map = new Map({ container.style.position = 'absolute'; container.style.width = '100%'; container.style.height = '100%'; + transformContainer = document.createElement('div'); + transformContainer.style.position = 'absolute'; + transformContainer.style.width = '100%'; + transformContainer.style.height = '100%'; + container.appendChild(transformContainer); canvas = document.createElement('canvas'); canvas.style.position = 'absolute'; canvas.style.left = '0'; canvas.style.transformOrigin = 'top left'; - container.appendChild(canvas); - const offscreen = canvas.transferControlToOffscreen(); - mvtLayerWorker.postMessage({ - canvas: offscreen - }, [offscreen]); + transformContainer.appendChild(canvas); } + mainThreadFrameState = frameState; + updateContainerTransform(); mvtLayerWorker.postMessage({ frameState: JSON.parse(JSON.stringify(frameState, getCircularReplacer())) }); @@ -53,11 +79,18 @@ const map = new Map({ zoom: 2 }) }); -mvtLayerWorker.addEventListener('message', message => { - if (message.data.type === 'render') { +mvtLayerWorker.addEventListener('message', function(message) { + if (message.data.type === 'request-render') { map.render(); - } else if (canvas && message.data.type === 'transform-opacity') { - canvas.style.transform = message.data.transform; + } else if (canvas && message.data.type === 'rendered') { + transformContainer.style.transform = ''; + const imageData = message.data.imageData; + canvas.width = imageData.width; + canvas.height = imageData.height; + canvas.getContext('2d').drawImage(imageData, 0, 0); canvas.style.opacity = message.data.opacity; + canvas.style.transform = message.data.transform; + workerFrameState = message.data.frameState; + updateContainerTransform(); } }); From bb1ca76bcce6052f732fe117cd854d82479d9b40 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Mon, 16 Mar 2020 23:22:49 +0100 Subject: [PATCH 08/25] Make Executor work in workers --- src/ol/render/canvas/Executor.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ol/render/canvas/Executor.js b/src/ol/render/canvas/Executor.js index ccdf260033..c9217a7ac4 100644 --- a/src/ol/render/canvas/Executor.js +++ b/src/ol/render/canvas/Executor.js @@ -18,6 +18,7 @@ import { } from '../../transform.js'; import {defaultTextAlign, measureTextHeight, measureAndCacheTextWidth, measureTextWidths} from '../canvas.js'; import RBush from 'rbush/rbush.js'; +import {WINDOW} from '../../has.js'; /** @@ -204,7 +205,9 @@ class Executor { contextInstructions.push('lineCap', strokeState.lineCap); contextInstructions.push('lineJoin', strokeState.lineJoin); contextInstructions.push('miterLimit', strokeState.miterLimit); - if (CanvasRenderingContext2D.prototype.setLineDash) { + // eslint-disable-next-line + const Context = WINDOW ? CanvasRenderingContext2D : OffscreenCanvasRenderingContext2D; + if (Context.prototype.setLineDash) { contextInstructions.push('setLineDash', [strokeState.lineDash]); contextInstructions.push('lineDashOffset', strokeState.lineDashOffset); } From 06f6ba13c888242168c23c3ca1c97b8962e5a055 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Mon, 16 Mar 2020 23:23:34 +0100 Subject: [PATCH 09/25] Make font loading work in workers --- src/ol/css.js | 27 ++++++++++++------------ src/ol/render/canvas.js | 46 ++++++++++++++++++++++++++++++++++------- 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/src/ol/css.js b/src/ol/css.js index c855fae7ea..f4eb09eb59 100644 --- a/src/ol/css.js +++ b/src/ol/css.js @@ -7,6 +7,7 @@ * @property {Array} families * @property {string} style * @property {string} weight + * @property {string} lineHeight */ @@ -68,8 +69,9 @@ export const CLASS_COLLAPSED = 'ol-collapsed'; /** * Get the list of font families from a font spec. Note that this doesn't work * for font families that have commas in them. - * @param {string} The CSS font property. - * @return {FontParameters} The font families (or null if the input spec is invalid). + * @param {string} fontSpec The CSS font property. + * @param {function(FontParameters):void} callback Called with the font families + * (or null if the input spec is invalid). */ export const getFontParameters = (function() { /** @@ -80,26 +82,25 @@ export const getFontParameters = (function() { * @type {Object} */ const cache = {}; - return function(font) { + return function(fontSpec, callback) { if (!style) { style = document.createElement('div').style; } - if (!(font in cache)) { - style.font = font; + if (!(fontSpec in cache)) { + style.font = fontSpec; const family = style.fontFamily; - const fontWeight = style.fontWeight; - const fontStyle = style.fontStyle; - style.font = ''; if (!family) { - return null; + callback(null); } const families = family.split(/,\s?/); - cache[font] = { + cache[fontSpec] = { families: families, - weight: fontWeight, - style: fontStyle + weight: style.fontWeight, + style: style.fontStyle, + lineHeight: style.lineHeight }; + style.font = ''; } - return cache[font]; + callback(cache[fontSpec]); }; })(); diff --git a/src/ol/render/canvas.js b/src/ol/render/canvas.js index 04781b9bc1..1a5293bd2e 100644 --- a/src/ol/render/canvas.js +++ b/src/ol/render/canvas.js @@ -266,8 +266,7 @@ export const registerFont = (function() { } } - return function(fontSpec) { - const font = getFontParameters(fontSpec); + function fontCallback(font) { if (!font) { return; } @@ -285,6 +284,31 @@ export const registerFont = (function() { } } } + } + + return function(fontSpec) { + if (WINDOW) { + getFontParameters(fontSpec, fontCallback); + } else { + /** @type {any} */ + const worker = self; + worker.postMessage({ + type: 'getFontParameters', + font: fontSpec + }); + worker.addEventListener('message', function handler(event) { + if (event.data.type === 'getFontParameters') { + worker.removeEventListener('message', handler); + const font = event.data.font; + fontCallback(font); + if (!textHeights[fontSpec]) { + const metrics = measureText(fontSpec, 'Žg'); + const lineHeight = isNaN(font.lineHeight) ? 1.2 : Number(font.lineHeight); + textHeights[fontSpec] = lineHeight * (metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent); + } + } + }); + } }; })(); @@ -320,13 +344,12 @@ export const measureTextHeight = (function() { }; })(); - /** * @param {string} font Font. * @param {string} text Text. - * @return {number} Width. + * @return {TextMetrics} Text metrics. */ -export function measureTextWidth(font, text) { +function measureText(font, text) { if (!measureContext) { measureContext = createCanvasContext2D(1, 1); } @@ -334,7 +357,16 @@ export function measureTextWidth(font, text) { measureContext.font = font; measureFont = measureContext.font; } - return measureContext.measureText(text).width; + return measureContext.measureText(text); +} + +/** + * @param {string} font Font. + * @param {string} text Text. + * @return {number} Width. + */ +export function measureTextWidth(font, text) { + return measureText(font, text).width; } @@ -434,7 +466,7 @@ function executeLabelInstructions(label, context) { const contextInstructions = label.contextInstructions; for (let i = 0, ii = contextInstructions.length; i < ii; i += 2) { if (Array.isArray(contextInstructions[i + 1])) { - CanvasRenderingContext2D.prototype[contextInstructions[i]].apply(context, contextInstructions[i + 1]); + context[contextInstructions[i]].apply(context, contextInstructions[i + 1]); } else { context[contextInstructions[i]] = contextInstructions[i + 1]; } From 10c333058042ae979e476076ef0892bffb32839a Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Mon, 16 Mar 2020 23:26:09 +0100 Subject: [PATCH 10/25] Use correct transorms at the right time --- examples/mvtlayer.worker.js | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/examples/mvtlayer.worker.js b/examples/mvtlayer.worker.js index bc2ab5a20b..cf535c89ba 100644 --- a/examples/mvtlayer.worker.js +++ b/examples/mvtlayer.worker.js @@ -4,6 +4,7 @@ import MVT from '../src/ol/format/MVT.js'; import {Projection} from '../src/ol/proj.js'; import TileQueue from '../src/ol/TileQueue.js'; import {getTilePriority as tilePriorityFunction} from '../src/ol/TileQueue.js'; +import {renderDeclutterItems} from '../src/ol/render.js'; const key = 'pk.eyJ1IjoiYWhvY2V2YXIiLCJhIjoiY2pzbmg0Nmk5MGF5NzQzbzRnbDNoeHJrbiJ9.7_-_gL8ur7ZtEiNwRfCy7Q'; @@ -45,7 +46,7 @@ const tileQueue = new TileQueue(getTilePriority, function() { const maxTotalLoading = 8; const maxNewLoads = 2; - +let rendererTransform, rendererOpacity; renderer.useContainer = function(target, transform, opacity) { target.style = {}; this.canvas = target; @@ -53,21 +54,16 @@ renderer.useContainer = function(target, transform, opacity) { this.container = { firstElementChild: target }; - layer.once('postrender', function() { - const imageData = canvas.transferToImageBitmap(); - worker.postMessage({ - type: 'rendered', - imageData: imageData, - transform: transform, - opacity: opacity, - frameState: JSON.parse(JSON.stringify(frameState, getCircularReplacer())) - }, [imageData]); - }); + rendererTransform = transform; + rendererOpacity = opacity; }; let rendering = false; -worker.onmessage = function(event) { +worker.addEventListener('message', function(event) { + if (event.data.type !== 'render') { + return; + } if (rendering) { // drop this frame worker.postMessage({type: 'request-render'}); @@ -79,12 +75,21 @@ worker.onmessage = function(event) { rendering = true; requestAnimationFrame(function() { renderer.renderFrame(frameState, canvas); + renderDeclutterItems(frameState, null); if (tileQueue.getTilesLoading() < maxTotalLoading) { tileQueue.reprioritize(); // FIXME only call if view has changed tileQueue.loadMoreTiles(maxTotalLoading, maxNewLoads); } + const imageData = canvas.transferToImageBitmap(); + worker.postMessage({ + type: 'rendered', + imageData: imageData, + transform: rendererTransform, + opacity: rendererOpacity, + frameState: JSON.parse(JSON.stringify(frameState, getCircularReplacer())) + }, [imageData]); rendering = false; }); -}; +}); export let create; From 3217bf131636da5353b88b6189e358c6e08a4fd5 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Mon, 16 Mar 2020 23:26:24 +0100 Subject: [PATCH 11/25] Add style handling --- examples/mvtlayer.worker.js | 3 ++ examples/offscreen-canvas-tiles.css | 3 ++ examples/offscreen-canvas-tiles.js | 31 ++++++++++++++++ examples/resources/mapbox-streets-v6-style.js | 36 ++++++++++++++++--- 4 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 examples/offscreen-canvas-tiles.css diff --git a/examples/mvtlayer.worker.js b/examples/mvtlayer.worker.js index cf535c89ba..7dc263cf23 100644 --- a/examples/mvtlayer.worker.js +++ b/examples/mvtlayer.worker.js @@ -4,6 +4,8 @@ import MVT from '../src/ol/format/MVT.js'; import {Projection} from '../src/ol/proj.js'; import TileQueue from '../src/ol/TileQueue.js'; import {getTilePriority as tilePriorityFunction} from '../src/ol/TileQueue.js'; +import {Style, Fill, Stroke, Icon, Text} from '../src/ol/style.js'; +import createMapboxStreetsV6Style from './resources/mapbox-streets-v6-style.js'; import {renderDeclutterItems} from '../src/ol/render.js'; const key = 'pk.eyJ1IjoiYWhvY2V2YXIiLCJhIjoiY2pzbmg0Nmk5MGF5NzQzbzRnbDNoeHJrbiJ9.7_-_gL8ur7ZtEiNwRfCy7Q'; @@ -33,6 +35,7 @@ function getTilePriority(tile, tileSourceKey, tileCenter, tileResolution) { const layer = new VectorTileLayer({ declutter: true, + style: createMapboxStreetsV6Style(Style, Fill, Stroke, Icon, Text), source: new VectorTileSource({ format: new MVT(), url: 'https://{a-d}.tiles.mapbox.com/v4/mapbox.mapbox-streets-v6/' + diff --git a/examples/offscreen-canvas-tiles.css b/examples/offscreen-canvas-tiles.css new file mode 100644 index 0000000000..33e90f7301 --- /dev/null +++ b/examples/offscreen-canvas-tiles.css @@ -0,0 +1,3 @@ +.map { + background: #f8f4f0; +} diff --git a/examples/offscreen-canvas-tiles.js b/examples/offscreen-canvas-tiles.js index 0e619526aa..356e9ead4c 100644 --- a/examples/offscreen-canvas-tiles.js +++ b/examples/offscreen-canvas-tiles.js @@ -5,9 +5,39 @@ import Layer from '../src/ol/layer/Layer.js'; import Worker from 'worker-loader!./mvtlayer.worker.js'; import {compose, create} from '../src/ol/transform.js'; import {createTransformString} from '../src/ol/render/canvas.js'; +import {getFontParameters} from '../src/ol/css.js'; const mvtLayerWorker = new Worker(); +const loadingImages = {}; +mvtLayerWorker.addEventListener('message', event => { + if (event.data.type === 'getFontParameters') { + getFontParameters(event.data.font, font => { + mvtLayerWorker.postMessage({ + type: 'getFontParameters', + font: font + }); + }); + } else if (event.data.type === 'loadImage') { + if (!(event.data.src in loadingImages)) { + const image = new Image(); + image.crossOrigin = 'anonymous'; + image.addEventListener('load', function() { + createImageBitmap(image, 0, 0, image.width, image.height).then(imageBitmap => { + delete loadingImages[event.data.iconName]; + mvtLayerWorker.postMessage({ + type: 'imageLoaded', + image: imageBitmap, + iconName: event.data.iconName + }, [imageBitmap]); + }); + }); + image.src = 'https://unpkg.com/@mapbox/maki@4.0.0/icons/' + event.data.iconName + '-15.svg'; + loadingImages[event.data.iconName] = true; + } + } +}); + function getCircularReplacer() { const seen = new WeakSet(); return function(key, value) { @@ -67,6 +97,7 @@ const map = new Map({ mainThreadFrameState = frameState; updateContainerTransform(); mvtLayerWorker.postMessage({ + type: 'render', frameState: JSON.parse(JSON.stringify(frameState, getCircularReplacer())) }); return container; diff --git a/examples/resources/mapbox-streets-v6-style.js b/examples/resources/mapbox-streets-v6-style.js index abd5babca3..e1f0197bf8 100644 --- a/examples/resources/mapbox-streets-v6-style.js +++ b/examples/resources/mapbox-streets-v6-style.js @@ -2,6 +2,20 @@ // http://a.tiles.mapbox.com/v4/mapbox.mapbox-streets-v6.json function createMapboxStreetsV6Style(Style, Fill, Stroke, Icon, Text) { + + let worker; + try { + worker = self.document ? null : self; + worker.addEventListener('message', message => { + if (message.data.type === 'imageLoaded') { + iconCache[message.data.iconName].setImage(new Icon({ + img: message.data.image, + imgSize: [15, 15] + })); + } + }); + } catch (e) {} + var fill = new Fill({color: ''}); var stroke = new Stroke({color: '', width: 1}); var polygon = new Style({fill: fill}); @@ -14,11 +28,19 @@ function createMapboxStreetsV6Style(Style, Fill, Stroke, Icon, Text) { function getIcon(iconName) { var icon = iconCache[iconName]; if (!icon) { - icon = new Style({image: new Icon({ - src: 'https://unpkg.com/@mapbox/maki@4.0.0/icons/' + iconName + '-15.svg', - imgSize: [15, 15], - crossOrigin: 'anonymous' - })}); + if (!worker) { + icon = new Style({image: new Icon({ + src: 'https://unpkg.com/@mapbox/maki@4.0.0/icons/' + iconName + '-15.svg', + imgSize: [15, 15], + crossOrigin: 'anonymous' + })}); + } else { + icon = new Style({}); + worker.postMessage({ + type: 'loadImage', + iconName: iconName + }); + } iconCache[iconName] = icon; } return icon; @@ -310,3 +332,7 @@ function createMapboxStreetsV6Style(Style, Fill, Stroke, Icon, Text) { return styles; }; } + +try { + module.exports = createMapboxStreetsV6Style; +} catch (e) {} From f80c175263be43b0e4c8110ae090701d42d1d039 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Tue, 17 Mar 2020 16:43:50 +0100 Subject: [PATCH 12/25] Do not transform rotated views --- examples/offscreen-canvas-tiles.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/examples/offscreen-canvas-tiles.js b/examples/offscreen-canvas-tiles.js index 356e9ead4c..13899f2949 100644 --- a/examples/offscreen-canvas-tiles.js +++ b/examples/offscreen-canvas-tiles.js @@ -63,12 +63,15 @@ function updateContainerTransform() { const renderedCenter = renderedViewState.center; const renderedResolution = renderedViewState.resolution; const renderedRotation = renderedViewState.rotation; - const transform = compose(create(), - (renderedCenter[0] - center[0]) / resolution, - (center[1] - renderedCenter[1]) / resolution, - renderedResolution / resolution, renderedResolution / resolution, - rotation - renderedRotation, - 0, 0); + const transform = create(); + if (!rotation) { + compose(transform, + (renderedCenter[0] - center[0]) / resolution, + (center[1] - renderedCenter[1]) / resolution, + renderedResolution / resolution, renderedResolution / resolution, + rotation - renderedRotation, + 0, 0); + } transformContainer.style.transform = createTransformString(transform); } From 6dcc54bfb89f39a8c8b9576eb3a943db60121a5e Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Tue, 17 Mar 2020 17:55:54 +0100 Subject: [PATCH 13/25] 'action' instead of 'type' as message identifier --- examples/mvtlayer.worker.js | 8 ++-- examples/offscreen-canvas-tiles.js | 65 ++++++++++++++++-------------- src/ol/render/canvas.js | 6 +-- 3 files changed, 41 insertions(+), 38 deletions(-) diff --git a/examples/mvtlayer.worker.js b/examples/mvtlayer.worker.js index 7dc263cf23..16a555e052 100644 --- a/examples/mvtlayer.worker.js +++ b/examples/mvtlayer.worker.js @@ -44,7 +44,7 @@ const layer = new VectorTileLayer({ }); const renderer = layer.getRenderer(); const tileQueue = new TileQueue(getTilePriority, function() { - worker.postMessage({type: 'request-render'}); + worker.postMessage({action: 'request-render'}); }); const maxTotalLoading = 8; const maxNewLoads = 2; @@ -64,12 +64,12 @@ renderer.useContainer = function(target, transform, opacity) { let rendering = false; worker.addEventListener('message', function(event) { - if (event.data.type !== 'render') { + if (event.data.action !== 'render') { return; } if (rendering) { // drop this frame - worker.postMessage({type: 'request-render'}); + worker.postMessage({action: 'request-render'}); return; } frameState = event.data.frameState; @@ -85,7 +85,7 @@ worker.addEventListener('message', function(event) { } const imageData = canvas.transferToImageBitmap(); worker.postMessage({ - type: 'rendered', + action: 'rendered', imageData: imageData, transform: rendererTransform, opacity: rendererOpacity, diff --git a/examples/offscreen-canvas-tiles.js b/examples/offscreen-canvas-tiles.js index 13899f2949..60ee70480c 100644 --- a/examples/offscreen-canvas-tiles.js +++ b/examples/offscreen-canvas-tiles.js @@ -11,14 +11,14 @@ const mvtLayerWorker = new Worker(); const loadingImages = {}; mvtLayerWorker.addEventListener('message', event => { - if (event.data.type === 'getFontParameters') { + if (event.data.action === 'getFontParameters') { getFontParameters(event.data.font, font => { mvtLayerWorker.postMessage({ - type: 'getFontParameters', + action: 'getFontParameters', font: font }); }); - } else if (event.data.type === 'loadImage') { + } else if (event.data.action === 'loadImage') { if (!(event.data.src in loadingImages)) { const image = new Image(); image.crossOrigin = 'anonymous'; @@ -26,7 +26,7 @@ mvtLayerWorker.addEventListener('message', event => { createImageBitmap(image, 0, 0, image.width, image.height).then(imageBitmap => { delete loadingImages[event.data.iconName]; mvtLayerWorker.postMessage({ - type: 'imageLoaded', + action: 'imageLoaded', image: imageBitmap, iconName: event.data.iconName }, [imageBitmap]); @@ -77,34 +77,37 @@ function updateContainerTransform() { } +function render(id, frameState) { + if (!container) { + container = document.createElement('div'); + container.style.position = 'absolute'; + container.style.width = '100%'; + container.style.height = '100%'; + transformContainer = document.createElement('div'); + transformContainer.style.position = 'absolute'; + transformContainer.style.width = '100%'; + transformContainer.style.height = '100%'; + container.appendChild(transformContainer); + canvas = document.createElement('canvas'); + canvas.style.position = 'absolute'; + canvas.style.left = '0'; + canvas.style.transformOrigin = 'top left'; + transformContainer.appendChild(canvas); + } + mainThreadFrameState = frameState; + updateContainerTransform(); + mvtLayerWorker.postMessage({ + action: 'render', + id: id, + frameState: JSON.parse(JSON.stringify(frameState, getCircularReplacer())) + }); + return container; +} + const map = new Map({ layers: [ new Layer({ - render: function(frameState) { - if (!container) { - container = document.createElement('div'); - container.style.position = 'absolute'; - container.style.width = '100%'; - container.style.height = '100%'; - transformContainer = document.createElement('div'); - transformContainer.style.position = 'absolute'; - transformContainer.style.width = '100%'; - transformContainer.style.height = '100%'; - container.appendChild(transformContainer); - canvas = document.createElement('canvas'); - canvas.style.position = 'absolute'; - canvas.style.left = '0'; - canvas.style.transformOrigin = 'top left'; - transformContainer.appendChild(canvas); - } - mainThreadFrameState = frameState; - updateContainerTransform(); - mvtLayerWorker.postMessage({ - type: 'render', - frameState: JSON.parse(JSON.stringify(frameState, getCircularReplacer())) - }); - return container; - } + render: render.bind(undefined, 'mapbox') }) ], target: 'map', @@ -114,9 +117,9 @@ const map = new Map({ }) }); mvtLayerWorker.addEventListener('message', function(message) { - if (message.data.type === 'request-render') { + if (message.data.action === 'request-render') { map.render(); - } else if (canvas && message.data.type === 'rendered') { + } else if (canvas && message.data.action === 'rendered') { transformContainer.style.transform = ''; const imageData = message.data.imageData; canvas.width = imageData.width; diff --git a/src/ol/render/canvas.js b/src/ol/render/canvas.js index 1a5293bd2e..002ec4efd0 100644 --- a/src/ol/render/canvas.js +++ b/src/ol/render/canvas.js @@ -289,15 +289,15 @@ export const registerFont = (function() { return function(fontSpec) { if (WINDOW) { getFontParameters(fontSpec, fontCallback); - } else { + } else if (self.postMessage) { /** @type {any} */ const worker = self; worker.postMessage({ - type: 'getFontParameters', + action: 'getFontParameters', font: fontSpec }); worker.addEventListener('message', function handler(event) { - if (event.data.type === 'getFontParameters') { + if (event.data.action === 'getFontParameters') { worker.removeEventListener('message', handler); const font = event.data.font; fontCallback(font); From 3f7f999db02e52e2bbd27ef4aab960517e1f56f2 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sat, 21 Mar 2020 15:49:01 +0100 Subject: [PATCH 14/25] Avoid try/catch, DOM and workers --- src/ol/css.js | 79 +++++++++++++++++++------------- src/ol/dom.js | 8 ++-- src/ol/has.js | 20 ++------ src/ol/render/canvas.js | 72 +++++++++++------------------ src/ol/render/canvas/Executor.js | 4 +- tsconfig.json | 2 +- 6 files changed, 85 insertions(+), 100 deletions(-) diff --git a/src/ol/css.js b/src/ol/css.js index f4eb09eb59..88a6d5c9dc 100644 --- a/src/ol/css.js +++ b/src/ol/css.js @@ -4,10 +4,13 @@ /** * @typedef {Object} FontParameters - * @property {Array} families * @property {string} style + * @property {string} variant * @property {string} weight + * @property {string} size * @property {string} lineHeight + * @property {string} family + * @property {Array} families */ @@ -65,42 +68,52 @@ export const CLASS_CONTROL = 'ol-control'; */ export const CLASS_COLLAPSED = 'ol-collapsed'; +/** + * From http://stackoverflow.com/questions/10135697/regex-to-parse-any-css-font + * @type {RegExp} + */ +const fontRegEx = new RegExp([ + '^\\s*(?=(?:(?:[-a-z]+\\s*){0,2}(italic|oblique))?)', + '(?=(?:(?:[-a-z]+\\s*){0,2}(small-caps))?)', + '(?=(?:(?:[-a-z]+\\s*){0,2}(bold(?:er)?|lighter|[1-9]00 ))?)', + '(?:(?:normal|\\1|\\2|\\3)\\s*){0,3}((?:xx?-)?', + '(?:small|large)|medium|smaller|larger|[\\.\\d]+(?:\\%|in|[cem]m|ex|p[ctx]))', + '(?:\\s*\\/\\s*(normal|[\\.\\d]+(?:\\%|in|[cem]m|ex|p[ctx])?))', + '?\\s*([-,\\"\\\'\\sa-z]+?)\\s*$' +].join(''), 'i'); +const fontRegExMatchIndex = [ + 'style', + 'variant', + 'weight', + 'size', + 'lineHeight', + 'family' +]; /** * Get the list of font families from a font spec. Note that this doesn't work * for font families that have commas in them. * @param {string} fontSpec The CSS font property. - * @param {function(FontParameters):void} callback Called with the font families - * (or null if the input spec is invalid). + * @return {FontParameters} The font parameters (or null if the input spec is invalid). */ -export const getFontParameters = (function() { - /** - * @type {CSSStyleDeclaration} - */ - let style; - /** - * @type {Object} - */ - const cache = {}; - return function(fontSpec, callback) { - if (!style) { - style = document.createElement('div').style; +export const getFontParameters = function(fontSpec) { + const match = fontSpec.match(fontRegEx); + if (!match) { + return null; + } + const style = /** @type {FontParameters} */ ({ + lineHeight: 'normal', + size: '1.2em', + style: 'normal', + weight: 'normal', + variant: 'normal' + }); + for (let i = 0, ii = fontRegExMatchIndex.length; i < ii; ++i) { + const value = match[i + 1]; + if (value !== undefined) { + style[fontRegExMatchIndex[i]] = value; } - if (!(fontSpec in cache)) { - style.font = fontSpec; - const family = style.fontFamily; - if (!family) { - callback(null); - } - const families = family.split(/,\s?/); - cache[fontSpec] = { - families: families, - weight: style.fontWeight, - style: style.fontStyle, - lineHeight: style.lineHeight - }; - style.font = ''; - } - callback(cache[fontSpec]); - }; -})(); + } + style.families = style.family.split(/,\s?/); + return style; +}; diff --git a/src/ol/dom.js b/src/ol/dom.js index 37d188541e..e3c403cc99 100644 --- a/src/ol/dom.js +++ b/src/ol/dom.js @@ -1,3 +1,5 @@ +import {WORKER_OFFSCREEN_CANVAS} from './has.js'; + /** * @module ol/dom */ @@ -14,9 +16,9 @@ export function createCanvasContext2D(opt_width, opt_height, opt_canvasPool) { const canvas = opt_canvasPool && opt_canvasPool.length ? opt_canvasPool.shift() : - 'document' in self ? - document.createElement('canvas') : - new OffscreenCanvas(opt_width || 300, opt_height || 300); + WORKER_OFFSCREEN_CANVAS ? + new OffscreenCanvas(opt_width || 300, opt_height || 300) : + document.createElement('canvas'); if (opt_width) { canvas.width = opt_width; } diff --git a/src/ol/has.js b/src/ol/has.js index 24b053a2fe..3fdebf833c 100644 --- a/src/ol/has.js +++ b/src/ol/has.js @@ -37,27 +37,15 @@ export const MAC = ua.indexOf('macintosh') !== -1; * @type {number} * @api */ -export const DEVICE_PIXEL_RATIO = (function() { - try { - return self.devicePixelRatio; - } catch (e) { - return window.devicePixelRatio || 1; - } -})(); +export const DEVICE_PIXEL_RATIO = (typeof self !== 'undefined' ? self.devicePixelRatio : window.devicePixelRatio) || 1; /** - * The execution context is a window. + * The execution context is a worker with OffscreenCanvas available. * @const * @type {boolean} */ -export const WINDOW = (function() { - try { - return 'document' in self; - } catch (e) { - // ancient browsers don't have `self` - return true; - } -})(); +export const WORKER_OFFSCREEN_CANVAS = typeof WorkerGlobalScope !== 'undefined' && typeof OffscreenCanvas !== 'undefined' && + self instanceof WorkerGlobalScope; //eslint-disable-line /** * Image.prototype.decode() is supported. diff --git a/src/ol/render/canvas.js b/src/ol/render/canvas.js index 002ec4efd0..edd5737a07 100644 --- a/src/ol/render/canvas.js +++ b/src/ol/render/canvas.js @@ -6,7 +6,7 @@ import {createCanvasContext2D} from '../dom.js'; import {clear} from '../obj.js'; import BaseObject from '../Object.js'; import EventTarget from '../events/Target.js'; -import {WINDOW} from '../has.js'; +import {WORKER_OFFSCREEN_CANVAS} from '../has.js'; import {toString} from '../transform.js'; @@ -266,7 +266,8 @@ export const registerFont = (function() { } } - function fontCallback(font) { + return function(fontSpec) { + const font = getFontParameters(fontSpec); if (!font) { return; } @@ -284,31 +285,6 @@ export const registerFont = (function() { } } } - } - - return function(fontSpec) { - if (WINDOW) { - getFontParameters(fontSpec, fontCallback); - } else if (self.postMessage) { - /** @type {any} */ - const worker = self; - worker.postMessage({ - action: 'getFontParameters', - font: fontSpec - }); - worker.addEventListener('message', function handler(event) { - if (event.data.action === 'getFontParameters') { - worker.removeEventListener('message', handler); - const font = event.data.font; - fontCallback(font); - if (!textHeights[fontSpec]) { - const metrics = measureText(fontSpec, 'Žg'); - const lineHeight = isNaN(font.lineHeight) ? 1.2 : Number(font.lineHeight); - textHeights[fontSpec] = lineHeight * (metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent); - } - } - }); - } }; })(); @@ -323,22 +299,29 @@ export const measureTextHeight = (function() { */ let div; const heights = textHeights; - return function(font) { - let height = heights[font]; + return function(fontSpec) { + let height = heights[fontSpec]; if (height == undefined) { - if (!div) { - div = document.createElement('div'); - div.innerHTML = 'M'; - div.style.margin = '0 !important'; - div.style.padding = '0 !important'; - div.style.position = 'absolute !important'; - div.style.left = '-99999px !important'; + if (WORKER_OFFSCREEN_CANVAS) { + const font = getFontParameters(fontSpec); + const metrics = measureText(fontSpec, 'Žg'); + const lineHeight = isNaN(Number(font.lineHeight)) ? 1.2 : Number(font.lineHeight); + textHeights[fontSpec] = lineHeight * (metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent); + } else { + if (!div) { + div = document.createElement('div'); + div.innerHTML = 'M'; + div.style.margin = '0 !important'; + div.style.padding = '0 !important'; + div.style.position = 'absolute !important'; + div.style.left = '-99999px !important'; + } + div.style.font = fontSpec; + document.body.appendChild(div); + height = div.offsetHeight; + heights[fontSpec] = height; + document.body.removeChild(div); } - div.style.font = font; - document.body.appendChild(div); - height = div.offsetHeight; - heights[font] = height; - document.body.removeChild(div); } return height; }; @@ -369,7 +352,6 @@ export function measureTextWidth(font, text) { return measureText(font, text).width; } - /** * Measure text width using a cache. * @param {string} font The font. @@ -484,13 +466,13 @@ let createTransformStringCanvas = null; * @return {string} CSS transform. */ export function createTransformString(transform) { - if (WINDOW) { + if (WORKER_OFFSCREEN_CANVAS) { + return toString(transform); + } else { if (!createTransformStringCanvas) { createTransformStringCanvas = createCanvasContext2D(1, 1).canvas; } createTransformStringCanvas.style.transform = toString(transform); return createTransformStringCanvas.style.transform; - } else { - return toString(transform); } } diff --git a/src/ol/render/canvas/Executor.js b/src/ol/render/canvas/Executor.js index c9217a7ac4..5f88e0aa94 100644 --- a/src/ol/render/canvas/Executor.js +++ b/src/ol/render/canvas/Executor.js @@ -18,7 +18,7 @@ import { } from '../../transform.js'; import {defaultTextAlign, measureTextHeight, measureAndCacheTextWidth, measureTextWidths} from '../canvas.js'; import RBush from 'rbush/rbush.js'; -import {WINDOW} from '../../has.js'; +import {WORKER_OFFSCREEN_CANVAS} from '../../has.js'; /** @@ -206,7 +206,7 @@ class Executor { contextInstructions.push('lineJoin', strokeState.lineJoin); contextInstructions.push('miterLimit', strokeState.miterLimit); // eslint-disable-next-line - const Context = WINDOW ? CanvasRenderingContext2D : OffscreenCanvasRenderingContext2D; + const Context = WORKER_OFFSCREEN_CANVAS ? OffscreenCanvasRenderingContext2D : CanvasRenderingContext2D; if (Context.prototype.setLineDash) { contextInstructions.push('setLineDash', [strokeState.lineDash]); contextInstructions.push('lineDashOffset', strokeState.lineDashOffset); diff --git a/tsconfig.json b/tsconfig.json index 438785ff84..3a208514a3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ /* Basic Options */ "target": "ES2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ - "lib": ["es2017", "dom"], /* Specify library files to be included in the compilation. */ + "lib": ["es2017", "dom", "webworker"], /* Specify library files to be included in the compilation. */ "allowJs": true, /* Allow javascript files to be compiled. */ "checkJs": true, /* Report errors in .js files. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ From b9bfe45d86b0e4d70f5c0e9129ccb70915c54499 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sun, 22 Mar 2020 20:04:53 +0100 Subject: [PATCH 15/25] Update ol-mapbox-style --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d9adcfa006..67d75abf99 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "loglevelnext": "^3.0.1", "marked": "0.8.2", "mocha": "7.1.1", - "ol-mapbox-style": "^6.0.0", + "ol-mapbox-style": "^6.1.0", "pixelmatch": "^5.1.0", "pngjs": "^3.4.0", "proj4": "2.6.0", From 30ac91c4aefd0a644a7ccae19a32a9eb7499b38c Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sun, 22 Mar 2020 20:05:27 +0100 Subject: [PATCH 16/25] Simpler feature check --- src/ol/has.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ol/has.js b/src/ol/has.js index 3fdebf833c..6452b79a58 100644 --- a/src/ol/has.js +++ b/src/ol/has.js @@ -37,7 +37,7 @@ export const MAC = ua.indexOf('macintosh') !== -1; * @type {number} * @api */ -export const DEVICE_PIXEL_RATIO = (typeof self !== 'undefined' ? self.devicePixelRatio : window.devicePixelRatio) || 1; +export const DEVICE_PIXEL_RATIO = typeof devicePixelRatio !== 'undefined' ? devicePixelRatio : 1; /** * The execution context is a worker with OffscreenCanvas available. From bc1be50cbc5a3becfd698ba15963359123262766 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sun, 22 Mar 2020 20:06:03 +0100 Subject: [PATCH 17/25] Add worker support to examples --- examples/resources/common.js | 36 ++++++++++++++++++----------- examples/templates/example.html | 9 +++++++- examples/webpack/example-builder.js | 30 +++++++++++++++++++++++- 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/examples/resources/common.js b/examples/resources/common.js index 893fe51c1a..a59685d3d5 100644 --- a/examples/resources/common.js +++ b/examples/resources/common.js @@ -57,6 +57,8 @@ event.preventDefault(); const html = document.getElementById('example-html-source').innerText; const js = document.getElementById('example-js-source').innerText; + const workerContainer = document.getElementById('example-worker-source'); + const worker = workerContainer ? workerContainer.innerText : undefined; const pkgJson = document.getElementById('example-pkg-source').innerText; const form = document.getElementById('codepen-form'); @@ -68,22 +70,28 @@ Promise.all(promises) .then(results => { - const data = { - files: { - 'index.html': { - content: html - }, - 'index.js': { - content: js - }, - "package.json": { - content: pkgJson - }, - 'sandbox.config.json': { - content: '{"template": "parcel"}' - } + const files = { + 'index.html': { + content: html + }, + 'index.js': { + content: js + }, + "package.json": { + content: pkgJson + }, + 'sandbox.config.json': { + content: '{"template": "parcel"}' } }; + if (worker) { + files['worker.js'] = { + content: worker + } + } + const data = { + files: files + }; for (let i = 0; i < localResources.length; i++) { data.files[localResources[i]] = results[i]; diff --git a/examples/templates/example.html b/examples/templates/example.html index a45f88e7be..b0c5cee4b7 100644 --- a/examples/templates/example.html +++ b/examples/templates/example.html @@ -160,6 +160,14 @@
index.jsimport 'ol/ol.css';
 {{ js.source }}
+{{#if worker.source}} +
+
+ Copy +
+
worker.js{{ worker.source }}
+
+{{/if}}
Copy @@ -167,7 +175,6 @@
package.json{{ pkgJson }}
- {{{ js.tag }}} diff --git a/examples/webpack/example-builder.js b/examples/webpack/example-builder.js index 22a7867500..d25306b897 100644 --- a/examples/webpack/example-builder.js +++ b/examples/webpack/example-builder.js @@ -208,6 +208,10 @@ ExampleBuilder.prototype.render = async function(dir, chunk) { jsSource = jsSource.replace(new RegExp(entry.key, 'g'), entry.value); } } + // Remove worker loader import and modify `new Worker()` to add source + jsSource = jsSource.replace(/import Worker from 'worker-loader![^\n]*\n/g, ''); + jsSource = jsSource.replace('new Worker()', 'new Worker(\'./worker.js\')'); + data.js = { tag: ``, source: jsSource @@ -218,9 +222,33 @@ ExampleBuilder.prototype.render = async function(dir, chunk) { data.js.tag = prelude + data.js.tag; } + // check for worker js + const workerName = `${name}.worker.js`; + const workerPath = path.join(dir, workerName); + let workerSource; + try { + workerSource = await readFile(workerPath, readOptions); + } catch (err) { + // pass + } + if (workerSource) { + // remove "../src/" prefix and ".js" to have the same import syntax as the documentation + workerSource = workerSource.replace(/'\.\.\/src\//g, '\''); + workerSource = workerSource.replace(/\.js';/g, '\';'); + if (data.cloak) { + for (const entry of data.cloak) { + workerSource = workerSource.replace(new RegExp(entry.key, 'g'), entry.value); + } + } + data.worker = { + source: workerSource + }; + assets[workerName] = workerSource; + } + data.pkgJson = JSON.stringify({ name: name, - dependencies: getDependencies(jsSource), + dependencies: getDependencies(jsSource + workerSource ? `\n${workerSource}` : ''), devDependencies: { parcel: '1.11.0' }, From ade9ac8857685508edb3abf8d9cd517494a23e2f Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sun, 22 Mar 2020 20:08:24 +0100 Subject: [PATCH 18/25] Make mapbox-style example fullscreen on demand --- examples/mapbox-style.html | 27 ++------------------------- examples/mapbox-style.js | 5 ++++- 2 files changed, 6 insertions(+), 26 deletions(-) diff --git a/examples/mapbox-style.html b/examples/mapbox-style.html index 8285734955..1c88215464 100644 --- a/examples/mapbox-style.html +++ b/examples/mapbox-style.html @@ -1,5 +1,5 @@ --- -layout: example-verbatim.html +layout: example.html title: Vector tiles created from a Mapbox Style object shortdesc: Example of using ol-mapbox-style with tiles from maptiler.com. docs: > @@ -10,27 +10,4 @@ cloak: - key: get_your_own_D6rA4zTHduk6KOKTXzGB value: Get your own API key at https://www.maptiler.com/cloud/ --- - - - - - - - Mapbox Style objects with ol-mapbox-style - - - - - -
- - - - +
diff --git a/examples/mapbox-style.js b/examples/mapbox-style.js index 819177a69d..a774155526 100644 --- a/examples/mapbox-style.js +++ b/examples/mapbox-style.js @@ -1,3 +1,6 @@ import apply from 'ol-mapbox-style'; +import FullScreen from '../src/ol/control/FullScreen.js'; -apply('map', 'https://api.maptiler.com/maps/topo/style.json?key=get_your_own_D6rA4zTHduk6KOKTXzGB'); +apply('map', 'https://api.maptiler.com/maps/topo/style.json?key=get_your_own_D6rA4zTHduk6KOKTXzGB').then(function(map) { + map.addControl(new FullScreen()); +}); From 28f390828dac51b1f793e165356d846187c4178e Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sun, 22 Mar 2020 20:11:07 +0100 Subject: [PATCH 19/25] Use same data as in mapbox-style example --- examples/mvtlayer.worker.js | 98 ----------- examples/offscreen-canvas-tiles.css | 2 +- examples/offscreen-canvas-tiles.js | 16 +- examples/offscreen-canvas-tiles.worker.js | 154 ++++++++++++++++++ examples/resources/mapbox-streets-v6-style.js | 38 +---- 5 files changed, 170 insertions(+), 138 deletions(-) delete mode 100644 examples/mvtlayer.worker.js create mode 100644 examples/offscreen-canvas-tiles.worker.js diff --git a/examples/mvtlayer.worker.js b/examples/mvtlayer.worker.js deleted file mode 100644 index 16a555e052..0000000000 --- a/examples/mvtlayer.worker.js +++ /dev/null @@ -1,98 +0,0 @@ -import VectorTileLayer from '../src/ol/layer/VectorTile.js'; -import VectorTileSource from '../src/ol/source/VectorTile.js'; -import MVT from '../src/ol/format/MVT.js'; -import {Projection} from '../src/ol/proj.js'; -import TileQueue from '../src/ol/TileQueue.js'; -import {getTilePriority as tilePriorityFunction} from '../src/ol/TileQueue.js'; -import {Style, Fill, Stroke, Icon, Text} from '../src/ol/style.js'; -import createMapboxStreetsV6Style from './resources/mapbox-streets-v6-style.js'; -import {renderDeclutterItems} from '../src/ol/render.js'; - -const key = 'pk.eyJ1IjoiYWhvY2V2YXIiLCJhIjoiY2pzbmg0Nmk5MGF5NzQzbzRnbDNoeHJrbiJ9.7_-_gL8ur7ZtEiNwRfCy7Q'; - -/** @type {any} */ -const worker = self; - -let frameState; -const canvas = new OffscreenCanvas(1, 1); - -function getCircularReplacer() { - const seen = new WeakSet(); - return function(key, value) { - if (typeof value === 'object' && value !== null) { - if (seen.has(value)) { - return '[circular]'; - } - seen.add(value); - } - return value; - }; -} - -function getTilePriority(tile, tileSourceKey, tileCenter, tileResolution) { - return tilePriorityFunction(frameState, tile, tileSourceKey, tileCenter, tileResolution); -} - -const layer = new VectorTileLayer({ - declutter: true, - style: createMapboxStreetsV6Style(Style, Fill, Stroke, Icon, Text), - source: new VectorTileSource({ - format: new MVT(), - url: 'https://{a-d}.tiles.mapbox.com/v4/mapbox.mapbox-streets-v6/' + - '{z}/{x}/{y}.vector.pbf?access_token=' + key - }) -}); -const renderer = layer.getRenderer(); -const tileQueue = new TileQueue(getTilePriority, function() { - worker.postMessage({action: 'request-render'}); -}); -const maxTotalLoading = 8; -const maxNewLoads = 2; - -let rendererTransform, rendererOpacity; -renderer.useContainer = function(target, transform, opacity) { - target.style = {}; - this.canvas = target; - this.context = target.getContext('2d'); - this.container = { - firstElementChild: target - }; - rendererTransform = transform; - rendererOpacity = opacity; -}; - -let rendering = false; - -worker.addEventListener('message', function(event) { - if (event.data.action !== 'render') { - return; - } - if (rendering) { - // drop this frame - worker.postMessage({action: 'request-render'}); - return; - } - frameState = event.data.frameState; - frameState.tileQueue = tileQueue; - frameState.viewState.projection.__proto__ = Projection.prototype; - rendering = true; - requestAnimationFrame(function() { - renderer.renderFrame(frameState, canvas); - renderDeclutterItems(frameState, null); - if (tileQueue.getTilesLoading() < maxTotalLoading) { - tileQueue.reprioritize(); // FIXME only call if view has changed - tileQueue.loadMoreTiles(maxTotalLoading, maxNewLoads); - } - const imageData = canvas.transferToImageBitmap(); - worker.postMessage({ - action: 'rendered', - imageData: imageData, - transform: rendererTransform, - opacity: rendererOpacity, - frameState: JSON.parse(JSON.stringify(frameState, getCircularReplacer())) - }, [imageData]); - rendering = false; - }); -}); - -export let create; diff --git a/examples/offscreen-canvas-tiles.css b/examples/offscreen-canvas-tiles.css index 33e90f7301..9fde055492 100644 --- a/examples/offscreen-canvas-tiles.css +++ b/examples/offscreen-canvas-tiles.css @@ -1,3 +1,3 @@ .map { - background: #f8f4f0; + background: rgba(232, 230, 223, 1); } diff --git a/examples/offscreen-canvas-tiles.js b/examples/offscreen-canvas-tiles.js index 60ee70480c..29243c2d9d 100644 --- a/examples/offscreen-canvas-tiles.js +++ b/examples/offscreen-canvas-tiles.js @@ -1,11 +1,12 @@ import Map from '../src/ol/Map.js'; import View from '../src/ol/View.js'; import Layer from '../src/ol/layer/Layer.js'; -//eslint-disable-next-line -import Worker from 'worker-loader!./mvtlayer.worker.js'; +import Worker from 'worker-loader!./offscreen-canvas-tiles.worker.js'; //eslint-disable-line import {compose, create} from '../src/ol/transform.js'; import {createTransformString} from '../src/ol/render/canvas.js'; import {getFontParameters} from '../src/ol/css.js'; +import {createXYZ} from '../src/ol/tilegrid.js'; +import {FullScreen} from '../src/ol/control.js'; const mvtLayerWorker = new Worker(); @@ -14,7 +15,7 @@ mvtLayerWorker.addEventListener('message', event => { if (event.data.action === 'getFontParameters') { getFontParameters(event.data.font, font => { mvtLayerWorker.postMessage({ - action: 'getFontParameters', + action: 'gotFontParameters', font: font }); }); @@ -28,12 +29,12 @@ mvtLayerWorker.addEventListener('message', event => { mvtLayerWorker.postMessage({ action: 'imageLoaded', image: imageBitmap, - iconName: event.data.iconName + src: event.data.src }, [imageBitmap]); }); }); - image.src = 'https://unpkg.com/@mapbox/maki@4.0.0/icons/' + event.data.iconName + '-15.svg'; - loadingImages[event.data.iconName] = true; + image.src = event.data.src; + loadingImages[event.data.src] = true; } } }); @@ -112,10 +113,12 @@ const map = new Map({ ], target: 'map', view: new View({ + resolutions: createXYZ({tileSize: 512}).getResolutions89, center: [0, 0], zoom: 2 }) }); +map.addControl(new FullScreen()); mvtLayerWorker.addEventListener('message', function(message) { if (message.data.action === 'request-render') { map.render(); @@ -125,7 +128,6 @@ mvtLayerWorker.addEventListener('message', function(message) { canvas.width = imageData.width; canvas.height = imageData.height; canvas.getContext('2d').drawImage(imageData, 0, 0); - canvas.style.opacity = message.data.opacity; canvas.style.transform = message.data.transform; workerFrameState = message.data.frameState; updateContainerTransform(); diff --git a/examples/offscreen-canvas-tiles.worker.js b/examples/offscreen-canvas-tiles.worker.js new file mode 100644 index 0000000000..d801747de5 --- /dev/null +++ b/examples/offscreen-canvas-tiles.worker.js @@ -0,0 +1,154 @@ +import VectorTileLayer from '../src/ol/layer/VectorTile.js'; +import VectorTileSource from '../src/ol/source/VectorTile.js'; +import MVT from '../src/ol/format/MVT.js'; +import {Projection} from '../src/ol/proj.js'; +import TileQueue from '../src/ol/TileQueue.js'; +import {getTilePriority as tilePriorityFunction} from '../src/ol/TileQueue.js'; +import {renderDeclutterItems} from '../src/ol/render.js'; +import styleFunction from 'ol-mapbox-style/dist/stylefunction.js'; +import {inView} from '../src/ol/layer/Layer.js'; + +/** @type {any} */ +const worker = self; + +let frameState, pixelRatio; +const canvas = new OffscreenCanvas(1, 1); + +function getCircularReplacer() { + const seen = new WeakSet(); + return function(key, value) { + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) { + return '[circular]'; + } + seen.add(value); + } + return value; + }; +} + +function getTilePriority(tile, tileSourceKey, tileCenter, tileResolution) { + return tilePriorityFunction(frameState, tile, tileSourceKey, tileCenter, tileResolution); +} + +const landcover = new VectorTileLayer({ + visible: false, + declutter: true, + maxZoom: 9, + source: new VectorTileSource({ + maxZoom: 9, + format: new MVT(), + url: 'https://api.maptiler.com/tiles/landcover/{z}/{x}/{y}.pbf?key=get_your_own_D6rA4zTHduk6KOKTXzGB' + }) +}); +const contours = new VectorTileLayer({ + visible: false, + declutter: true, + minZoom: 9, + maxZoom: 14, + source: new VectorTileSource({ + minZoom: 9, + maxZoom: 14, + format: new MVT(), + url: 'https://api.maptiler.com/tiles/contours/{z}/{x}/{y}.pbf?key=get_your_own_D6rA4zTHduk6KOKTXzGB' + }) +}); +const openmaptiles = new VectorTileLayer({ + visible: false, + declutter: true, + source: new VectorTileSource({ + format: new MVT(), + maxZoom: 14, + url: 'https://api.maptiler.com/tiles/v3/{z}/{x}/{y}.pbf?key=get_your_own_D6rA4zTHduk6KOKTXzGB' + }) +}); + +const layers = [landcover, contours, openmaptiles]; +let rendererTransform; +layers.forEach(layer => { + layer.once('change', () => { + layer.setVisible(true); + worker.postMessage({action: 'request-render'}); + }); + layer.getRenderer().useContainer = function(target, transform) { + this.containerReused = this.getLayer() !== layers[0]; + target.style = {}; + this.canvas = target; + this.context = target.getContext('2d'); + this.container = { + firstElementChild: target + }; + rendererTransform = transform; + }; +}); + +function getFont(font) { + return font[0] + .replace('Noto Sans', 'serif') + .replace('Roboto', 'sans-serif'); +} + +function loadStyles() { + const styleUrl = 'https://api.maptiler.com/maps/topo/style.json?key=get_your_own_D6rA4zTHduk6KOKTXzGB'; + fetch(styleUrl).then(data => data.json()).then(styleJson => { + const spriteUrl = styleJson.sprite + (pixelRatio > 1 ? '@2x' : '') + '.json'; + const spriteImageUrl = styleJson.sprite + (pixelRatio > 1 ? '@2x' : '') + '.png'; + fetch(spriteUrl).then(data => data.json()).then(spriteJson => { + styleFunction(landcover, styleJson, 'landcover', undefined, spriteJson, spriteImageUrl, getFont); + styleFunction(contours, styleJson, 'contours', undefined, spriteJson, spriteImageUrl, getFont); + styleFunction(openmaptiles, styleJson, 'openmaptiles', undefined, spriteJson, spriteImageUrl, getFont); + }); + }); +} + +const tileQueue = new TileQueue(getTilePriority, () => { + worker.postMessage({action: 'request-render'}); +}); +const maxTotalLoading = 8; +const maxNewLoads = 2; + +let rendering = false; + +worker.addEventListener('message', event => { + if (event.data.action !== 'render') { + return; + } + frameState = event.data.frameState; + if (!pixelRatio) { + pixelRatio = frameState.pixelRatio; + loadStyles(); + } + frameState.tileQueue = tileQueue; + frameState.viewState.projection.__proto__ = Projection.prototype; + if (rendering) { + return; + } + rendering = true; + requestAnimationFrame(function() { + let rendered = false; + layers.forEach(layer => { + if (inView(layer.getLayerState(), frameState.viewState)) { + rendered = true; + const renderer = layer.getRenderer(); + renderer.renderFrame(frameState, canvas); + } + }); + rendering = false; + if (!rendered) { + return; + } + renderDeclutterItems(frameState, null); + if (tileQueue.getTilesLoading() < maxTotalLoading) { + tileQueue.reprioritize(); // FIXME only call if view has changed + tileQueue.loadMoreTiles(maxTotalLoading, maxNewLoads); + } + const imageData = canvas.transferToImageBitmap(); + worker.postMessage({ + action: 'rendered', + imageData: imageData, + transform: rendererTransform, + frameState: JSON.parse(JSON.stringify(frameState, getCircularReplacer())) + }, [imageData]); + }); +}); + diff --git a/examples/resources/mapbox-streets-v6-style.js b/examples/resources/mapbox-streets-v6-style.js index e1f0197bf8..afc1d8605d 100644 --- a/examples/resources/mapbox-streets-v6-style.js +++ b/examples/resources/mapbox-streets-v6-style.js @@ -2,20 +2,6 @@ // http://a.tiles.mapbox.com/v4/mapbox.mapbox-streets-v6.json function createMapboxStreetsV6Style(Style, Fill, Stroke, Icon, Text) { - - let worker; - try { - worker = self.document ? null : self; - worker.addEventListener('message', message => { - if (message.data.type === 'imageLoaded') { - iconCache[message.data.iconName].setImage(new Icon({ - img: message.data.image, - imgSize: [15, 15] - })); - } - }); - } catch (e) {} - var fill = new Fill({color: ''}); var stroke = new Stroke({color: '', width: 1}); var polygon = new Style({fill: fill}); @@ -28,19 +14,11 @@ function createMapboxStreetsV6Style(Style, Fill, Stroke, Icon, Text) { function getIcon(iconName) { var icon = iconCache[iconName]; if (!icon) { - if (!worker) { - icon = new Style({image: new Icon({ - src: 'https://unpkg.com/@mapbox/maki@4.0.0/icons/' + iconName + '-15.svg', - imgSize: [15, 15], - crossOrigin: 'anonymous' - })}); - } else { - icon = new Style({}); - worker.postMessage({ - type: 'loadImage', - iconName: iconName - }); - } + icon = new Style({image: new Icon({ + src: 'https://unpkg.com/@mapbox/maki@4.0.0/icons/' + iconName + '-15.svg', + imgSize: [15, 15], + crossOrigin: 'anonymous' + })}); iconCache[iconName] = icon; } return icon; @@ -331,8 +309,4 @@ function createMapboxStreetsV6Style(Style, Fill, Stroke, Icon, Text) { styles.length = length; return styles; }; -} - -try { - module.exports = createMapboxStreetsV6Style; -} catch (e) {} +} \ No newline at end of file From 941df3b270aee8d8679999c09afdc1d5fdaf6079 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sun, 22 Mar 2020 20:19:15 +0100 Subject: [PATCH 20/25] Fix issues with TypeScript's built-in webworker lib --- tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.json b/tsconfig.json index 3a208514a3..4a838e8614 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "lib": ["es2017", "dom", "webworker"], /* Specify library files to be included in the compilation. */ "allowJs": true, /* Allow javascript files to be compiled. */ "checkJs": true, /* Report errors in .js files. */ + "skipLibCheck": true, // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ // "declaration": true, /* Generates corresponding '.d.ts' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */ From 828becf68e70fa3b97b95adcdf8922489db3a3a9 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sun, 22 Mar 2020 20:49:40 +0100 Subject: [PATCH 21/25] Position rotate control in the bottom left --- examples/mapbox-style.css | 6 ++++++ examples/offscreen-canvas-tiles.css | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 examples/mapbox-style.css diff --git a/examples/mapbox-style.css b/examples/mapbox-style.css new file mode 100644 index 0000000000..c79f84a2c1 --- /dev/null +++ b/examples/mapbox-style.css @@ -0,0 +1,6 @@ +.ol-rotate { + left: .5em; + bottom: .5em; + top: unset; + right: unset; +} \ No newline at end of file diff --git a/examples/offscreen-canvas-tiles.css b/examples/offscreen-canvas-tiles.css index 9fde055492..10bacb9a63 100644 --- a/examples/offscreen-canvas-tiles.css +++ b/examples/offscreen-canvas-tiles.css @@ -1,3 +1,9 @@ .map { background: rgba(232, 230, 223, 1); } +.ol-rotate { + left: .5em; + bottom: .5em; + top: unset; + right: unset; +} \ No newline at end of file From 0e1af6836ffbf195cb5715ad6f5edaacbfdb7584 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Mon, 23 Mar 2020 12:46:06 +0100 Subject: [PATCH 22/25] Example cleanup --- examples/offscreen-canvas-tiles.html | 1 + examples/offscreen-canvas-tiles.js | 126 +++++++++------------ examples/offscreen-canvas-tiles.worker.js | 129 ++++++++++------------ package.json | 1 + 4 files changed, 114 insertions(+), 143 deletions(-) diff --git a/examples/offscreen-canvas-tiles.html b/examples/offscreen-canvas-tiles.html index 231da2f14f..741d469e23 100644 --- a/examples/offscreen-canvas-tiles.html +++ b/examples/offscreen-canvas-tiles.html @@ -5,5 +5,6 @@ shortdesc: Example of a map that delegates rendering to a worker. docs: > The map in this example is rendered in a web worker, using `OffscreenCanvas`. **Note:** This is currently only supported in Chrome and Edge. tags: "worker, offscreencanvas, vector-tiles" +experimental: true ---
diff --git a/examples/offscreen-canvas-tiles.js b/examples/offscreen-canvas-tiles.js index 29243c2d9d..53696a7414 100644 --- a/examples/offscreen-canvas-tiles.js +++ b/examples/offscreen-canvas-tiles.js @@ -4,56 +4,16 @@ import Layer from '../src/ol/layer/Layer.js'; import Worker from 'worker-loader!./offscreen-canvas-tiles.worker.js'; //eslint-disable-line import {compose, create} from '../src/ol/transform.js'; import {createTransformString} from '../src/ol/render/canvas.js'; -import {getFontParameters} from '../src/ol/css.js'; import {createXYZ} from '../src/ol/tilegrid.js'; import {FullScreen} from '../src/ol/control.js'; +import stringify from 'json-stringify-safe'; -const mvtLayerWorker = new Worker(); - -const loadingImages = {}; -mvtLayerWorker.addEventListener('message', event => { - if (event.data.action === 'getFontParameters') { - getFontParameters(event.data.font, font => { - mvtLayerWorker.postMessage({ - action: 'gotFontParameters', - font: font - }); - }); - } else if (event.data.action === 'loadImage') { - if (!(event.data.src in loadingImages)) { - const image = new Image(); - image.crossOrigin = 'anonymous'; - image.addEventListener('load', function() { - createImageBitmap(image, 0, 0, image.width, image.height).then(imageBitmap => { - delete loadingImages[event.data.iconName]; - mvtLayerWorker.postMessage({ - action: 'imageLoaded', - image: imageBitmap, - src: event.data.src - }, [imageBitmap]); - }); - }); - image.src = event.data.src; - loadingImages[event.data.src] = true; - } - } -}); - -function getCircularReplacer() { - const seen = new WeakSet(); - return function(key, value) { - if (typeof value === 'object' && value !== null) { - if (seen.has(value)) { - return '[circular]'; - } - seen.add(value); - } - return value; - }; -} +const worker = new Worker(); let container, transformContainer, canvas, workerFrameState, mainThreadFrameState; +// Transform the container to account for the differnece between the (newer) +// main thread frameState and the (older) worker frameState function updateContainerTransform() { if (workerFrameState) { const viewState = mainThreadFrameState.viewState; @@ -65,6 +25,8 @@ function updateContainerTransform() { const renderedResolution = renderedViewState.resolution; const renderedRotation = renderedViewState.rotation; const transform = create(); + // Skip the extra transform for rotated views, because it will not work + // correctly in that case if (!rotation) { compose(transform, (renderedCenter[0] - center[0]) / resolution, @@ -75,40 +37,36 @@ function updateContainerTransform() { } transformContainer.style.transform = createTransformString(transform); } - -} - -function render(id, frameState) { - if (!container) { - container = document.createElement('div'); - container.style.position = 'absolute'; - container.style.width = '100%'; - container.style.height = '100%'; - transformContainer = document.createElement('div'); - transformContainer.style.position = 'absolute'; - transformContainer.style.width = '100%'; - transformContainer.style.height = '100%'; - container.appendChild(transformContainer); - canvas = document.createElement('canvas'); - canvas.style.position = 'absolute'; - canvas.style.left = '0'; - canvas.style.transformOrigin = 'top left'; - transformContainer.appendChild(canvas); - } - mainThreadFrameState = frameState; - updateContainerTransform(); - mvtLayerWorker.postMessage({ - action: 'render', - id: id, - frameState: JSON.parse(JSON.stringify(frameState, getCircularReplacer())) - }); - return container; } const map = new Map({ layers: [ new Layer({ - render: render.bind(undefined, 'mapbox') + render: function(frameState) { + if (!container) { + container = document.createElement('div'); + container.style.position = 'absolute'; + container.style.width = '100%'; + container.style.height = '100%'; + transformContainer = document.createElement('div'); + transformContainer.style.position = 'absolute'; + transformContainer.style.width = '100%'; + transformContainer.style.height = '100%'; + container.appendChild(transformContainer); + canvas = document.createElement('canvas'); + canvas.style.position = 'absolute'; + canvas.style.left = '0'; + canvas.style.transformOrigin = 'top left'; + transformContainer.appendChild(canvas); + } + mainThreadFrameState = frameState; + updateContainerTransform(); + worker.postMessage({ + action: 'render', + frameState: JSON.parse(stringify(frameState)) + }); + return container; + } }) ], target: 'map', @@ -119,10 +77,28 @@ const map = new Map({ }) }); map.addControl(new FullScreen()); -mvtLayerWorker.addEventListener('message', function(message) { - if (message.data.action === 'request-render') { + +// Worker messaging and actions +worker.addEventListener('message', message => { + if (message.data.action === 'loadImage') { + // Image loader for ol-mapbox-style + const image = new Image(); + image.crossOrigin = 'anonymous'; + image.addEventListener('load', function() { + createImageBitmap(image, 0, 0, image.width, image.height).then(imageBitmap => { + worker.postMessage({ + action: 'imageLoaded', + image: imageBitmap, + src: event.data.src + }, [imageBitmap]); + }); + }); + image.src = event.data.src; + } else if (message.data.action === 'request-render') { + // Worker requested a new render frame map.render(); } else if (canvas && message.data.action === 'rendered') { + // Worker provies a new render frame transformContainer.style.transform = ''; const imageData = message.data.imageData; canvas.width = imageData.width; diff --git a/examples/offscreen-canvas-tiles.worker.js b/examples/offscreen-canvas-tiles.worker.js index d801747de5..e80187e7af 100644 --- a/examples/offscreen-canvas-tiles.worker.js +++ b/examples/offscreen-canvas-tiles.worker.js @@ -7,81 +7,35 @@ import {getTilePriority as tilePriorityFunction} from '../src/ol/TileQueue.js'; import {renderDeclutterItems} from '../src/ol/render.js'; import styleFunction from 'ol-mapbox-style/dist/stylefunction.js'; import {inView} from '../src/ol/layer/Layer.js'; +import stringify from 'json-stringify-safe'; /** @type {any} */ const worker = self; -let frameState, pixelRatio; +let frameState, pixelRatio, rendererTransform; const canvas = new OffscreenCanvas(1, 1); -function getCircularReplacer() { - const seen = new WeakSet(); - return function(key, value) { - if (typeof value === 'object' && value !== null) { - if (seen.has(value)) { - return '[circular]'; - } - seen.add(value); - } - return value; - }; -} - -function getTilePriority(tile, tileSourceKey, tileCenter, tileResolution) { - return tilePriorityFunction(frameState, tile, tileSourceKey, tileCenter, tileResolution); -} - -const landcover = new VectorTileLayer({ - visible: false, - declutter: true, - maxZoom: 9, - source: new VectorTileSource({ +const sources = { + landcover: new VectorTileSource({ maxZoom: 9, format: new MVT(), url: 'https://api.maptiler.com/tiles/landcover/{z}/{x}/{y}.pbf?key=get_your_own_D6rA4zTHduk6KOKTXzGB' - }) -}); -const contours = new VectorTileLayer({ - visible: false, - declutter: true, - minZoom: 9, - maxZoom: 14, - source: new VectorTileSource({ + }), + contours: new VectorTileSource({ minZoom: 9, maxZoom: 14, format: new MVT(), url: 'https://api.maptiler.com/tiles/contours/{z}/{x}/{y}.pbf?key=get_your_own_D6rA4zTHduk6KOKTXzGB' - }) -}); -const openmaptiles = new VectorTileLayer({ - visible: false, - declutter: true, - source: new VectorTileSource({ + }), + openmaptiles: new VectorTileSource({ format: new MVT(), maxZoom: 14, url: 'https://api.maptiler.com/tiles/v3/{z}/{x}/{y}.pbf?key=get_your_own_D6rA4zTHduk6KOKTXzGB' }) -}); - -const layers = [landcover, contours, openmaptiles]; -let rendererTransform; -layers.forEach(layer => { - layer.once('change', () => { - layer.setVisible(true); - worker.postMessage({action: 'request-render'}); - }); - layer.getRenderer().useContainer = function(target, transform) { - this.containerReused = this.getLayer() !== layers[0]; - target.style = {}; - this.canvas = target; - this.context = target.getContext('2d'); - this.container = { - firstElementChild: target - }; - rendererTransform = transform; - }; -}); +}; +const layers = []; +// Font replacement so we do not need to load web fonts in the worker function getFont(font) { return font[0] .replace('Noto Sans', 'serif') @@ -90,23 +44,63 @@ function getFont(font) { function loadStyles() { const styleUrl = 'https://api.maptiler.com/maps/topo/style.json?key=get_your_own_D6rA4zTHduk6KOKTXzGB'; + fetch(styleUrl).then(data => data.json()).then(styleJson => { + const buckets = []; + let currentSource; + styleJson.layers.forEach(layer => { + if (!layer.source) { + return; + } + if (currentSource !== layer.source) { + currentSource = layer.source; + buckets.push({ + source: layer.source, + layers: [] + }); + } + buckets[buckets.length - 1].layers.push(layer.id); + }); + const spriteUrl = styleJson.sprite + (pixelRatio > 1 ? '@2x' : '') + '.json'; const spriteImageUrl = styleJson.sprite + (pixelRatio > 1 ? '@2x' : '') + '.png'; fetch(spriteUrl).then(data => data.json()).then(spriteJson => { - styleFunction(landcover, styleJson, 'landcover', undefined, spriteJson, spriteImageUrl, getFont); - styleFunction(contours, styleJson, 'contours', undefined, spriteJson, spriteImageUrl, getFont); - styleFunction(openmaptiles, styleJson, 'openmaptiles', undefined, spriteJson, spriteImageUrl, getFont); + buckets.forEach(bucket => { + const source = sources[bucket.source]; + if (!source) { + return; + } + const layer = new VectorTileLayer({ + declutter: true, + source, + minZoom: source.getTileGrid().getMinZoom() + }); + layer.getRenderer().useContainer = function(target, transform) { + this.containerReused = this.getLayer() !== layers[0]; + target.style = {}; + this.canvas = target; + this.context = target.getContext('2d'); + this.container = { + firstElementChild: target + }; + rendererTransform = transform; + }; + styleFunction(layer, styleJson, bucket.layers, undefined, spriteJson, spriteImageUrl, getFont); + layers.push(layer); + }); + worker.postMessage({action: 'request-render'}); }); }); } -const tileQueue = new TileQueue(getTilePriority, () => { - worker.postMessage({action: 'request-render'}); -}); +// Minimal map-like functionality for rendering + +const tileQueue = new TileQueue( + (tile, tileSourceKey, tileCenter, tileResolution) => tilePriorityFunction(frameState, tile, tileSourceKey, tileCenter, tileResolution), + () => worker.postMessage({action: 'request-render'})); + const maxTotalLoading = 8; const maxNewLoads = 2; - let rendering = false; worker.addEventListener('message', event => { @@ -124,7 +118,7 @@ worker.addEventListener('message', event => { return; } rendering = true; - requestAnimationFrame(function() { + requestAnimationFrame(() => { let rendered = false; layers.forEach(layer => { if (inView(layer.getLayerState(), frameState.viewState)) { @@ -139,7 +133,7 @@ worker.addEventListener('message', event => { } renderDeclutterItems(frameState, null); if (tileQueue.getTilesLoading() < maxTotalLoading) { - tileQueue.reprioritize(); // FIXME only call if view has changed + tileQueue.reprioritize(); tileQueue.loadMoreTiles(maxTotalLoading, maxNewLoads); } const imageData = canvas.transferToImageBitmap(); @@ -147,8 +141,7 @@ worker.addEventListener('message', event => { action: 'rendered', imageData: imageData, transform: rendererTransform, - frameState: JSON.parse(JSON.stringify(frameState, getCircularReplacer())) + frameState: JSON.parse(stringify(frameState)) }, [imageData]); }); }); - diff --git a/package.json b/package.json index 67d75abf99..2eca898191 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "jquery": "3.4.1", "jsdoc": "3.6.3", "jsdoc-plugin-typescript": "^2.0.5", + "json-stringify-safe": "^5.0.1", "karma": "^4.4.1", "karma-chrome-launcher": "3.1.0", "karma-coverage-istanbul-reporter": "^2.1.1", From 5113d7070149186ac9d1615da8fbd244dd7f1647 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Mon, 23 Mar 2020 12:58:41 +0100 Subject: [PATCH 23/25] Rename example --- examples/{offscreen-canvas-tiles.css => offscreen-canvas.css} | 0 .../{offscreen-canvas-tiles.html => offscreen-canvas.html} | 0 examples/{offscreen-canvas-tiles.js => offscreen-canvas.js} | 4 ++-- ...reen-canvas-tiles.worker.js => offscreen-canvas.worker.js} | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename examples/{offscreen-canvas-tiles.css => offscreen-canvas.css} (100%) rename examples/{offscreen-canvas-tiles.html => offscreen-canvas.html} (100%) rename examples/{offscreen-canvas-tiles.js => offscreen-canvas.js} (97%) rename examples/{offscreen-canvas-tiles.worker.js => offscreen-canvas.worker.js} (100%) diff --git a/examples/offscreen-canvas-tiles.css b/examples/offscreen-canvas.css similarity index 100% rename from examples/offscreen-canvas-tiles.css rename to examples/offscreen-canvas.css diff --git a/examples/offscreen-canvas-tiles.html b/examples/offscreen-canvas.html similarity index 100% rename from examples/offscreen-canvas-tiles.html rename to examples/offscreen-canvas.html diff --git a/examples/offscreen-canvas-tiles.js b/examples/offscreen-canvas.js similarity index 97% rename from examples/offscreen-canvas-tiles.js rename to examples/offscreen-canvas.js index 53696a7414..b51fceeea2 100644 --- a/examples/offscreen-canvas-tiles.js +++ b/examples/offscreen-canvas.js @@ -1,7 +1,7 @@ import Map from '../src/ol/Map.js'; import View from '../src/ol/View.js'; import Layer from '../src/ol/layer/Layer.js'; -import Worker from 'worker-loader!./offscreen-canvas-tiles.worker.js'; //eslint-disable-line +import Worker from 'worker-loader!./offscreen-canvas.worker.js'; //eslint-disable-line import {compose, create} from '../src/ol/transform.js'; import {createTransformString} from '../src/ol/render/canvas.js'; import {createXYZ} from '../src/ol/tilegrid.js'; @@ -89,7 +89,7 @@ worker.addEventListener('message', message => { worker.postMessage({ action: 'imageLoaded', image: imageBitmap, - src: event.data.src + src: message.data.src }, [imageBitmap]); }); }); diff --git a/examples/offscreen-canvas-tiles.worker.js b/examples/offscreen-canvas.worker.js similarity index 100% rename from examples/offscreen-canvas-tiles.worker.js rename to examples/offscreen-canvas.worker.js From 576f50331bff757132f12affd2fde139e2ccbe09 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Mon, 23 Mar 2020 19:44:31 +0100 Subject: [PATCH 24/25] Add attribution --- examples/offscreen-canvas.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/examples/offscreen-canvas.js b/examples/offscreen-canvas.js index b51fceeea2..991b757300 100644 --- a/examples/offscreen-canvas.js +++ b/examples/offscreen-canvas.js @@ -7,6 +7,7 @@ import {createTransformString} from '../src/ol/render/canvas.js'; import {createXYZ} from '../src/ol/tilegrid.js'; import {FullScreen} from '../src/ol/control.js'; import stringify from 'json-stringify-safe'; +import Source from '../src/ol/source/Source.js'; const worker = new Worker(); @@ -66,7 +67,13 @@ const map = new Map({ frameState: JSON.parse(stringify(frameState)) }); return container; - } + }, + source: new Source({ + attributions: [ + '© MapTiler', + '© OpenStreetMap contributors' + ] + }) }) ], target: 'map', From d70b3aa3d5cc4d8e85f4f07990d42556fc641ac1 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Tue, 24 Mar 2020 10:32:37 +0100 Subject: [PATCH 25/25] Move catch-up logic to main thread This avoids requestAnimationFrame in the worker. --- examples/offscreen-canvas.js | 35 ++++++++++------- examples/offscreen-canvas.worker.js | 59 ++++++++++++----------------- 2 files changed, 45 insertions(+), 49 deletions(-) diff --git a/examples/offscreen-canvas.js b/examples/offscreen-canvas.js index 991b757300..180e5c990e 100644 --- a/examples/offscreen-canvas.js +++ b/examples/offscreen-canvas.js @@ -11,7 +11,7 @@ import Source from '../src/ol/source/Source.js'; const worker = new Worker(); -let container, transformContainer, canvas, workerFrameState, mainThreadFrameState; +let container, transformContainer, canvas, rendering, workerFrameState, mainThreadFrameState; // Transform the container to account for the differnece between the (newer) // main thread frameState and the (older) worker frameState @@ -62,10 +62,15 @@ const map = new Map({ } mainThreadFrameState = frameState; updateContainerTransform(); - worker.postMessage({ - action: 'render', - frameState: JSON.parse(stringify(frameState)) - }); + if (!rendering) { + rendering = true; + worker.postMessage({ + action: 'render', + frameState: JSON.parse(stringify(frameState)) + }); + } else { + frameState.animate = true; + } return container; }, source: new Source({ @@ -101,18 +106,20 @@ worker.addEventListener('message', message => { }); }); image.src = event.data.src; - } else if (message.data.action === 'request-render') { + } else if (message.data.action === 'requestRender') { // Worker requested a new render frame map.render(); } else if (canvas && message.data.action === 'rendered') { // Worker provies a new render frame - transformContainer.style.transform = ''; - const imageData = message.data.imageData; - canvas.width = imageData.width; - canvas.height = imageData.height; - canvas.getContext('2d').drawImage(imageData, 0, 0); - canvas.style.transform = message.data.transform; - workerFrameState = message.data.frameState; - updateContainerTransform(); + requestAnimationFrame(function() { + const imageData = message.data.imageData; + canvas.width = imageData.width; + canvas.height = imageData.height; + canvas.getContext('2d').drawImage(imageData, 0, 0); + canvas.style.transform = message.data.transform; + workerFrameState = message.data.frameState; + updateContainerTransform(); + }); + rendering = false; } }); diff --git a/examples/offscreen-canvas.worker.js b/examples/offscreen-canvas.worker.js index e80187e7af..b03f0be61d 100644 --- a/examples/offscreen-canvas.worker.js +++ b/examples/offscreen-canvas.worker.js @@ -14,6 +14,9 @@ const worker = self; let frameState, pixelRatio, rendererTransform; const canvas = new OffscreenCanvas(1, 1); +// OffscreenCanvas does not have a style, so we mock it +canvas.style = {}; +const context = canvas.getContext('2d'); const sources = { landcover: new VectorTileSource({ @@ -77,18 +80,17 @@ function loadStyles() { }); layer.getRenderer().useContainer = function(target, transform) { this.containerReused = this.getLayer() !== layers[0]; - target.style = {}; - this.canvas = target; - this.context = target.getContext('2d'); + this.canvas = canvas; + this.context = context; this.container = { - firstElementChild: target + firstElementChild: canvas }; rendererTransform = transform; }; styleFunction(layer, styleJson, bucket.layers, undefined, spriteJson, spriteImageUrl, getFont); layers.push(layer); }); - worker.postMessage({action: 'request-render'}); + worker.postMessage({action: 'requestRender'}); }); }); } @@ -97,11 +99,10 @@ function loadStyles() { const tileQueue = new TileQueue( (tile, tileSourceKey, tileCenter, tileResolution) => tilePriorityFunction(frameState, tile, tileSourceKey, tileCenter, tileResolution), - () => worker.postMessage({action: 'request-render'})); + () => worker.postMessage({action: 'requestRender'})); const maxTotalLoading = 8; const maxNewLoads = 2; -let rendering = false; worker.addEventListener('message', event => { if (event.data.action !== 'render') { @@ -114,34 +115,22 @@ worker.addEventListener('message', event => { } frameState.tileQueue = tileQueue; frameState.viewState.projection.__proto__ = Projection.prototype; - if (rendering) { - return; - } - rendering = true; - requestAnimationFrame(() => { - let rendered = false; - layers.forEach(layer => { - if (inView(layer.getLayerState(), frameState.viewState)) { - rendered = true; - const renderer = layer.getRenderer(); - renderer.renderFrame(frameState, canvas); - } - }); - rendering = false; - if (!rendered) { - return; + layers.forEach(layer => { + if (inView(layer.getLayerState(), frameState.viewState)) { + const renderer = layer.getRenderer(); + renderer.renderFrame(frameState, canvas); } - renderDeclutterItems(frameState, null); - if (tileQueue.getTilesLoading() < maxTotalLoading) { - tileQueue.reprioritize(); - tileQueue.loadMoreTiles(maxTotalLoading, maxNewLoads); - } - const imageData = canvas.transferToImageBitmap(); - worker.postMessage({ - action: 'rendered', - imageData: imageData, - transform: rendererTransform, - frameState: JSON.parse(stringify(frameState)) - }, [imageData]); }); + renderDeclutterItems(frameState, null); + if (tileQueue.getTilesLoading() < maxTotalLoading) { + tileQueue.reprioritize(); + tileQueue.loadMoreTiles(maxTotalLoading, maxNewLoads); + } + const imageData = canvas.transferToImageBitmap(); + worker.postMessage({ + action: 'rendered', + imageData: imageData, + transform: rendererTransform, + frameState: JSON.parse(stringify(frameState)) + }, [imageData]); });