Merge pull request #10828 from ahocevar/offscreen-canvas
Offscreen canvas example
This commit is contained in:
6
examples/mapbox-style.css
Normal file
6
examples/mapbox-style.css
Normal file
@@ -0,0 +1,6 @@
|
||||
.ol-rotate {
|
||||
left: .5em;
|
||||
bottom: .5em;
|
||||
top: unset;
|
||||
right: unset;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
|
||||
9
examples/offscreen-canvas.css
Normal file
9
examples/offscreen-canvas.css
Normal file
@@ -0,0 +1,9 @@
|
||||
.map {
|
||||
background: rgba(232, 230, 223, 1);
|
||||
}
|
||||
.ol-rotate {
|
||||
left: .5em;
|
||||
bottom: .5em;
|
||||
top: unset;
|
||||
right: unset;
|
||||
}
|
||||
10
examples/offscreen-canvas.html
Normal file
10
examples/offscreen-canvas.html
Normal 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>
|
||||
125
examples/offscreen-canvas.js
Normal file
125
examples/offscreen-canvas.js
Normal 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;
|
||||
}
|
||||
});
|
||||
136
examples/offscreen-canvas.worker.js
Normal file
136
examples/offscreen-canvas.worker.js
Normal 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]);
|
||||
});
|
||||
@@ -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];
|
||||
|
||||
@@ -309,4 +309,4 @@ function createMapboxStreetsV6Style(Style, Fill, Stroke, Icon, Text) {
|
||||
styles.length = length;
|
||||
return styles;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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 }}}
|
||||
|
||||
@@ -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
22
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user