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/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());
+});
diff --git a/examples/offscreen-canvas.css b/examples/offscreen-canvas.css
new file mode 100644
index 0000000000..10bacb9a63
--- /dev/null
+++ b/examples/offscreen-canvas.css
@@ -0,0 +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
diff --git a/examples/offscreen-canvas.html b/examples/offscreen-canvas.html
new file mode 100644
index 0000000000..741d469e23
--- /dev/null
+++ b/examples/offscreen-canvas.html
@@ -0,0 +1,10 @@
+---
+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"
+experimental: true
+---
+
diff --git a/examples/offscreen-canvas.js b/examples/offscreen-canvas.js
new file mode 100644
index 0000000000..180e5c990e
--- /dev/null
+++ b/examples/offscreen-canvas.js
@@ -0,0 +1,125 @@
+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.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';
+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();
+
+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
+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 = 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,
+ (center[1] - renderedCenter[1]) / resolution,
+ renderedResolution / resolution, renderedResolution / resolution,
+ rotation - renderedRotation,
+ 0, 0);
+ }
+ transformContainer.style.transform = createTransformString(transform);
+ }
+}
+
+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();
+ if (!rendering) {
+ rendering = true;
+ worker.postMessage({
+ action: 'render',
+ frameState: JSON.parse(stringify(frameState))
+ });
+ } else {
+ frameState.animate = true;
+ }
+ return container;
+ },
+ source: new Source({
+ attributions: [
+ '© MapTiler ',
+ '© OpenStreetMap contributors '
+ ]
+ })
+ })
+ ],
+ target: 'map',
+ view: new View({
+ resolutions: createXYZ({tileSize: 512}).getResolutions89,
+ center: [0, 0],
+ zoom: 2
+ })
+});
+map.addControl(new FullScreen());
+
+// 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: message.data.src
+ }, [imageBitmap]);
+ });
+ });
+ image.src = event.data.src;
+ } 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
+ 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
new file mode 100644
index 0000000000..b03f0be61d
--- /dev/null
+++ b/examples/offscreen-canvas.worker.js
@@ -0,0 +1,136 @@
+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';
+import stringify from 'json-stringify-safe';
+
+/** @type {any} */
+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({
+ maxZoom: 9,
+ format: new MVT(),
+ url: 'https://api.maptiler.com/tiles/landcover/{z}/{x}/{y}.pbf?key=get_your_own_D6rA4zTHduk6KOKTXzGB'
+ }),
+ 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'
+ }),
+ 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 = [];
+
+// Font replacement so we do not need to load web fonts in the worker
+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 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 => {
+ 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];
+ this.canvas = canvas;
+ this.context = context;
+ this.container = {
+ firstElementChild: canvas
+ };
+ rendererTransform = transform;
+ };
+ styleFunction(layer, styleJson, bucket.layers, undefined, spriteJson, spriteImageUrl, getFont);
+ layers.push(layer);
+ });
+ worker.postMessage({action: 'requestRender'});
+ });
+ });
+}
+
+// Minimal map-like functionality for rendering
+
+const tileQueue = new TileQueue(
+ (tile, tileSourceKey, tileCenter, tileResolution) => tilePriorityFunction(frameState, tile, tileSourceKey, tileCenter, tileResolution),
+ () => worker.postMessage({action: 'requestRender'}));
+
+const maxTotalLoading = 8;
+const maxNewLoads = 2;
+
+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;
+ 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]);
+});
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/resources/mapbox-streets-v6-style.js b/examples/resources/mapbox-streets-v6-style.js
index abd5babca3..afc1d8605d 100644
--- a/examples/resources/mapbox-streets-v6-style.js
+++ b/examples/resources/mapbox-streets-v6-style.js
@@ -309,4 +309,4 @@ function createMapboxStreetsV6Style(Style, Fill, Stroke, Icon, Text) {
styles.length = length;
return styles;
};
-}
+}
\ No newline at end of file
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.js import 'ol/ol.css';
{{ js.source }}
+{{#if worker.source}}
+
+
+
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'
},
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..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",
@@ -83,7 +84,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",
@@ -104,6 +105,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": {
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;
+}
diff --git a/src/ol/css.js b/src/ol/css.js
index c855fae7ea..88a6d5c9dc 100644
--- a/src/ol/css.js
+++ b/src/ol/css.js
@@ -4,9 +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
*/
@@ -64,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} The CSS font property.
- * @return {FontParameters} The font families (or null if the input spec is invalid).
+ * @param {string} fontSpec The CSS font property.
+ * @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(font) {
- 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 (!(font in cache)) {
- style.font = font;
- const family = style.fontFamily;
- const fontWeight = style.fontWeight;
- const fontStyle = style.fontStyle;
- style.font = '';
- if (!family) {
- return null;
- }
- const families = family.split(/,\s?/);
- cache[font] = {
- families: families,
- weight: fontWeight,
- style: fontStyle
- };
- }
- return cache[font];
- };
-})();
+ }
+ style.families = style.family.split(/,\s?/);
+ return style;
+};
diff --git a/src/ol/dom.js b/src/ol/dom.js
index ad10c14671..e3c403cc99 100644
--- a/src/ol/dom.js
+++ b/src/ol/dom.js
@@ -1,8 +1,11 @@
+import {WORKER_OFFSCREEN_CANVAS} from './has.js';
+
/**
* @module ol/dom
*/
+//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 +15,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() :
+ WORKER_OFFSCREEN_CANVAS ?
+ new OffscreenCanvas(opt_width || 300, opt_height || 300) :
+ document.createElement('canvas');
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'));
}
diff --git a/src/ol/has.js b/src/ol/has.js
index bfc8aabef3..6452b79a58 100644
--- a/src/ol/has.js
+++ b/src/ol/has.js
@@ -37,7 +37,15 @@ export const MAC = ua.indexOf('macintosh') !== -1;
* @type {number}
* @api
*/
-export const DEVICE_PIXEL_RATIO = window.devicePixelRatio || 1;
+export const DEVICE_PIXEL_RATIO = typeof devicePixelRatio !== 'undefined' ? devicePixelRatio : 1;
+
+/**
+ * The execution context is a worker with OffscreenCanvas available.
+ * @const
+ * @type {boolean}
+ */
+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 adb892dd34..edd5737a07 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 {WORKER_OFFSCREEN_CANVAS} from '../has.js';
+import {toString} from '../transform.js';
/**
@@ -297,34 +299,40 @@ 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;
};
})();
-
/**
* @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);
}
@@ -332,9 +340,17 @@ 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;
+}
/**
* Measure text width using a cache.
@@ -432,9 +448,31 @@ 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];
}
}
}
+
+/**
+ * @type {HTMLCanvasElement}
+ * @private
+ */
+let createTransformStringCanvas = null;
+
+/**
+ * @param {import("../transform.js").Transform} transform Transform.
+ * @return {string} CSS transform.
+ */
+export function createTransformString(transform) {
+ if (WORKER_OFFSCREEN_CANVAS) {
+ return toString(transform);
+ } else {
+ if (!createTransformStringCanvas) {
+ createTransformStringCanvas = createCanvasContext2D(1, 1).canvas;
+ }
+ createTransformStringCanvas.style.transform = toString(transform);
+ return createTransformStringCanvas.style.transform;
+ }
+}
diff --git a/src/ol/render/canvas/Executor.js b/src/ol/render/canvas/Executor.js
index ccdf260033..5f88e0aa94 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 {WORKER_OFFSCREEN_CANVAS} 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 = WORKER_OFFSCREEN_CANVAS ? OffscreenCanvasRenderingContext2D : CanvasRenderingContext2D;
+ if (Context.prototype.setLineDash) {
contextInstructions.push('setLineDash', [strokeState.lineDash]);
contextInstructions.push('lineDashOffset', strokeState.lineDashOffset);
}
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 d082f13923..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_ = createCanvasContext2D(1, 1).canvas;
-
}
/**
@@ -269,15 +263,7 @@ class CanvasLayerRenderer extends LayerRenderer {
return data;
}
- /**
- * @param {import("../../transform.js").Transform} transform Transform.
- * @return {string} CSS transform.
- */
- createTransformString(transform) {
- this.createTransformStringCanvas_.style.transform = toString(transform);
- return this.createTransformStringCanvas_.style.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;
diff --git a/tsconfig.json b/tsconfig.json
index 438785ff84..4a838e8614 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,9 +3,10 @@
/* 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. */
+ "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. */