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",