Merge pull request #10828 from ahocevar/offscreen-canvas

Offscreen canvas example
This commit is contained in:
Andreas Hocevar
2020-03-24 21:45:06 +01:00
committed by GitHub
24 changed files with 546 additions and 143 deletions

View File

@@ -0,0 +1,6 @@
.ol-rotate {
left: .5em;
bottom: .5em;
top: unset;
right: unset;
}

View File

@@ -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/
---
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width">
<title>Mapbox Style objects with ol-mapbox-style</title>
<link rel="stylesheet" href="../css/ol.css" type="text/css">
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=fetch,String.prototype.startsWith,Object.assign"></script>
<style type="text/css">
html, body, .map {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="map" class="map"></div>
<script src="common.js"></script>
<script src="mapbox-style.js"></script>
</body>
</html>
<div id="map" class="map"></div>

View File

@@ -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());
});

View File

@@ -0,0 +1,9 @@
.map {
background: rgba(232, 230, 223, 1);
}
.ol-rotate {
left: .5em;
bottom: .5em;
top: unset;
right: unset;
}

View File

@@ -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
---
<div id="map" class="map"></div>

View File

@@ -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: [
'<a href="https://www.maptiler.com/copyright/" target="_blank">© MapTiler</a>',
'<a href="https://www.openstreetmap.org/copyright" target="_blank">© OpenStreetMap contributors</a>'
]
})
})
],
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;
}
});

View File

@@ -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]);
});

View File

@@ -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];

View File

@@ -309,4 +309,4 @@ function createMapboxStreetsV6Style(Style, Fill, Stroke, Icon, Text) {
styles.length = length;
return styles;
};
}
}

View File

@@ -160,6 +160,14 @@
<pre><legend>index.js</legend><code id="example-js-source" class="language-js">import 'ol/ol.css';
{{ js.source }}</code></pre>
</div>
{{#if worker.source}}
<div class="row-fluid">
<div class="source-controls">
<a class="copy-button" id="copy-worker-button" data-clipboard-target="#example-worker-source"><i class="fa fa-clipboard"></i> Copy</a>
</div>
<pre><legend>worker.js</legend><code id="example-worker-source" class="language-js">{{ worker.source }}</code></pre>
</div>
{{/if}}
<div class="row-fluid">
<div class="source-controls">
<a class="copy-button" id="copy-pkg-button" data-clipboard-target="#example-pkg-source"><i class="fa fa-clipboard"></i> Copy</a>
@@ -167,7 +175,6 @@
<pre><legend>package.json</legend><code id="example-pkg-source" class="language-js">{{ pkgJson }}</code></pre>
</div>
</div>
<script src="./resources/common.js"></script>
<script src="./resources/prism/prism.min.js"></script>
{{{ js.tag }}}

View File

@@ -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: `<script src="${this.common}.js"></script><script src="${jsName}"></script>`,
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'
},

22
package-lock.json generated
View File

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

View File

@@ -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": {

View File

@@ -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);
}
/**

View File

@@ -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;
}

View File

@@ -4,9 +4,13 @@
/**
* @typedef {Object} FontParameters
* @property {Array<string>} families
* @property {string} style
* @property {string} variant
* @property {string} weight
* @property {string} size
* @property {string} lineHeight
* @property {string} family
* @property {Array<string>} 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<string, FontParameters>}
*/
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;
};

View File

@@ -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'));
}

View File

@@ -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.

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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. */