Mapbox vector layer
This commit is contained in:
@@ -200,6 +200,11 @@ exports.handlers = {
|
||||
processingComplete(e) {
|
||||
const byLongname = e.doclets.index.longname;
|
||||
for (const name in defaultExports) {
|
||||
if (!(name in byLongname)) {
|
||||
throw new Error(
|
||||
`missing ${name} in doclet index, did you forget a @module tag?`
|
||||
);
|
||||
}
|
||||
byLongname[name].forEach(function (doclet) {
|
||||
doclet.isDefaultExport = true;
|
||||
});
|
||||
|
||||
3
examples/mapbox-vector-layer.css
Normal file
3
examples/mapbox-vector-layer.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.map {
|
||||
background: #f8f4f0;
|
||||
}
|
||||
15
examples/mapbox-vector-layer.html
Normal file
15
examples/mapbox-vector-layer.html
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
layout: example.html
|
||||
title: Mapbox Vector Layer
|
||||
shortdesc: Rendering a layer with a Mapbox-hosted style.
|
||||
docs: >
|
||||
The MapboxVector layer allows you to create a layer based on a Mapbox-hosted style using a single
|
||||
vector source. If your style uses more than one source, use the `source` property to choose a
|
||||
single vector source. Use the `layers` property if you only want to render a subset of the style's
|
||||
layers (provided they all share the same source).
|
||||
tags: "mapbox, studio, vector, tiles"
|
||||
cloak:
|
||||
- key: pk.eyJ1IjoiYWhvY2V2YXIiLCJhIjoiY2pzbmg0Nmk5MGF5NzQzbzRnbDNoeHJrbiJ9.7_-_gL8ur7ZtEiNwRfCy7Q
|
||||
value: Your Mapbox access token from https://mapbox.com/ here
|
||||
---
|
||||
<div id="map" class="map"></div>
|
||||
18
examples/mapbox-vector-layer.js
Normal file
18
examples/mapbox-vector-layer.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import Map from '../src/ol/Map.js';
|
||||
import MapboxVector from '../src/ol/layer/MapboxVector.js';
|
||||
import View from '../src/ol/View.js';
|
||||
|
||||
const map = new Map({
|
||||
target: 'map',
|
||||
layers: [
|
||||
new MapboxVector({
|
||||
styleUrl: 'mapbox://styles/mapbox/bright-v9',
|
||||
accessToken:
|
||||
'pk.eyJ1IjoiYWhvY2V2YXIiLCJhIjoiY2pzbmg0Nmk5MGF5NzQzbzRnbDNoeHJrbiJ9.7_-_gL8ur7ZtEiNwRfCy7Q',
|
||||
}),
|
||||
],
|
||||
view: new View({
|
||||
center: [0, 0],
|
||||
zoom: 2,
|
||||
}),
|
||||
});
|
||||
36
package-lock.json
generated
36
package-lock.json
generated
@@ -1637,14 +1637,12 @@
|
||||
"@mapbox/jsonlint-lines-primitives": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
|
||||
"integrity": "sha1-zlblOfg1UrWNENZy6k1vya3HsjQ=",
|
||||
"dev": true
|
||||
"integrity": "sha1-zlblOfg1UrWNENZy6k1vya3HsjQ="
|
||||
},
|
||||
"@mapbox/mapbox-gl-style-spec": {
|
||||
"version": "13.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-style-spec/-/mapbox-gl-style-spec-13.13.0.tgz",
|
||||
"integrity": "sha512-PBXa/Bw2G87NZckBZqY3omI3THF5MYQQY5B1IZWVLI7Ujsy149cjC8Sm1Ub1BgAnyslepdjtwWVS43IOjVYmUw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@mapbox/jsonlint-lines-primitives": "~2.0.2",
|
||||
"@mapbox/point-geometry": "^0.1.0",
|
||||
@@ -1659,22 +1657,19 @@
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
|
||||
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
|
||||
"dev": true
|
||||
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@mapbox/point-geometry": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz",
|
||||
"integrity": "sha1-ioP5M1x4YO/6Lu7KJUMyqgru2PI=",
|
||||
"dev": true
|
||||
"integrity": "sha1-ioP5M1x4YO/6Lu7KJUMyqgru2PI="
|
||||
},
|
||||
"@mapbox/unitbezier": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz",
|
||||
"integrity": "sha1-FWUb1VOme4WB+zmIEMmK2Go0Uk4=",
|
||||
"dev": true
|
||||
"integrity": "sha1-FWUb1VOme4WB+zmIEMmK2Go0Uk4="
|
||||
},
|
||||
"@nodelib/fs.scandir": {
|
||||
"version": "2.1.3",
|
||||
@@ -3786,8 +3781,7 @@
|
||||
"csscolorparser": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz",
|
||||
"integrity": "sha1-s085HupNqPPpgjHizNjfnAQfFxs=",
|
||||
"dev": true
|
||||
"integrity": "sha1-s085HupNqPPpgjHizNjfnAQfFxs="
|
||||
},
|
||||
"custom-event": {
|
||||
"version": "1.0.1",
|
||||
@@ -7644,8 +7638,7 @@
|
||||
"json-stringify-pretty-compact": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-2.0.0.tgz",
|
||||
"integrity": "sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ=="
|
||||
},
|
||||
"json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
@@ -8168,8 +8161,7 @@
|
||||
"mapbox-to-css-font": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/mapbox-to-css-font/-/mapbox-to-css-font-2.4.0.tgz",
|
||||
"integrity": "sha512-v674D0WtpxCXlA6E+sBlG1QJWdUkz/s9qAD91bJSXBGuBL5lL4tJXpoJEftecphCh2SVQCjWMS2vhylc3AIQTg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-v674D0WtpxCXlA6E+sBlG1QJWdUkz/s9qAD91bJSXBGuBL5lL4tJXpoJEftecphCh2SVQCjWMS2vhylc3AIQTg=="
|
||||
},
|
||||
"markdown-it": {
|
||||
"version": "10.0.0",
|
||||
@@ -9039,7 +9031,6 @@
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ol-mapbox-style/-/ol-mapbox-style-6.1.1.tgz",
|
||||
"integrity": "sha512-0Hgz2BX2tWe1ZNPMLpJkLdm3XI6ILrFbgmJIvdrlDYRce2ul1mXLmkJmbyLFs2tozsBJDcPJmI55UsKbiKg6ow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@mapbox/mapbox-gl-style-spec": "13.13.0",
|
||||
"mapbox-to-css-font": "^2.4.0",
|
||||
@@ -10383,8 +10374,7 @@
|
||||
"rw": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
|
||||
"integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=",
|
||||
"dev": true
|
||||
"integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q="
|
||||
},
|
||||
"rxjs": {
|
||||
"version": "6.5.5",
|
||||
@@ -11075,20 +11065,17 @@
|
||||
"sort-asc": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.1.0.tgz",
|
||||
"integrity": "sha1-q3md9h/HPqCVbHnEtTHtHp53J+k=",
|
||||
"dev": true
|
||||
"integrity": "sha1-q3md9h/HPqCVbHnEtTHtHp53J+k="
|
||||
},
|
||||
"sort-desc": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/sort-desc/-/sort-desc-0.1.1.tgz",
|
||||
"integrity": "sha1-GYuMDN6wlcRjNBhh45JdTuNZqe4=",
|
||||
"dev": true
|
||||
"integrity": "sha1-GYuMDN6wlcRjNBhh45JdTuNZqe4="
|
||||
},
|
||||
"sort-object": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/sort-object/-/sort-object-0.3.2.tgz",
|
||||
"integrity": "sha1-mODRme3kDgfGGoRAPGHWw7KQ+eI=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"sort-asc": "^0.1.0",
|
||||
"sort-desc": "^0.1.1"
|
||||
@@ -12335,8 +12322,7 @@
|
||||
"webfont-matcher": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/webfont-matcher/-/webfont-matcher-1.1.0.tgz",
|
||||
"integrity": "sha1-mM6VCXsp4x++czBT4Q5XFkLRxsc=",
|
||||
"dev": true
|
||||
"integrity": "sha1-mM6VCXsp4x++czBT4Q5XFkLRxsc="
|
||||
},
|
||||
"webpack": {
|
||||
"version": "4.43.0",
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"elm-pep": "^1.0.4",
|
||||
"ol-mapbox-style": "^6.1.1",
|
||||
"pbf": "3.2.1",
|
||||
"pixelworks": "1.1.0",
|
||||
"rbush": "^3.0.1"
|
||||
@@ -85,7 +86,6 @@
|
||||
"loglevelnext": "^4.0.1",
|
||||
"marked": "1.0.0",
|
||||
"mocha": "7.1.2",
|
||||
"ol-mapbox-style": "^6.1.1",
|
||||
"pixelmatch": "^5.1.0",
|
||||
"pngjs": "^5.0.0",
|
||||
"proj4": "2.6.1",
|
||||
|
||||
BIN
rendering/cases/layer-mapbox-vector/expected.png
Normal file
BIN
rendering/cases/layer-mapbox-vector/expected.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
22
rendering/cases/layer-mapbox-vector/main.js
Normal file
22
rendering/cases/layer-mapbox-vector/main.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import Map from '../../../src/ol/Map.js';
|
||||
import MapboxVector from '../../../src/ol/layer/MapboxVector.js';
|
||||
import View from '../../../src/ol/View.js';
|
||||
|
||||
new Map({
|
||||
layers: [
|
||||
new MapboxVector({
|
||||
styleUrl: '/data/styles/bright-v9.json',
|
||||
accessToken: 'test-token',
|
||||
}),
|
||||
],
|
||||
target: 'map',
|
||||
view: new View({
|
||||
center: [1825927.7316762917, 6143091.089223046],
|
||||
zoom: 15,
|
||||
}),
|
||||
});
|
||||
|
||||
render({
|
||||
message: 'Mapbox vector layer renders',
|
||||
tolerance: 0.025,
|
||||
});
|
||||
1
rendering/data/sprites/bright-v9/sprite.json
Normal file
1
rendering/data/sprites/bright-v9/sprite.json
Normal file
File diff suppressed because one or more lines are too long
BIN
rendering/data/sprites/bright-v9/sprite.png
Normal file
BIN
rendering/data/sprites/bright-v9/sprite.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
1
rendering/data/sprites/bright-v9/sprite@2x.json
Normal file
1
rendering/data/sprites/bright-v9/sprite@2x.json
Normal file
File diff suppressed because one or more lines are too long
BIN
rendering/data/sprites/bright-v9/sprite@2x.png
Normal file
BIN
rendering/data/sprites/bright-v9/sprite@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
4035
rendering/data/styles/bright-v9.json
Normal file
4035
rendering/data/styles/bright-v9.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,12 @@ To run a single rendering test case:
|
||||
node rendering/test.js --match your-test-case-name
|
||||
```
|
||||
|
||||
If you want to leave the test server running (and the test browser open) after running a test, use the `--interactive` option.
|
||||
|
||||
```bash
|
||||
node rendering/test.js --match your-test-case-name --interactive
|
||||
```
|
||||
|
||||
## Creating a new test
|
||||
|
||||
To create a new test case, add a directory under `cases` and add a `main.js` to it (copied from one of the other cases). Then to generate the `expected.png` screenshot, run the test script with the `--fix` flag:
|
||||
|
||||
@@ -251,7 +251,7 @@ async function render(entries, options) {
|
||||
page.on('console', (message) => {
|
||||
const type = message.type();
|
||||
if (options.log[type]) {
|
||||
options.log[type](message.text());
|
||||
options.log[type](`console: ${message.text()}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -260,6 +260,10 @@ async function render(entries, options) {
|
||||
await page.setViewport({width: 256, height: 256});
|
||||
fail = await renderEach(page, entries, options);
|
||||
} finally {
|
||||
if (options.interactive) {
|
||||
options.log.info('🐛 you have thirty minutes to debug, go!');
|
||||
await sleep(30 * 60 * 1000);
|
||||
}
|
||||
browser.close();
|
||||
}
|
||||
|
||||
@@ -324,10 +328,14 @@ async function main(entries, options) {
|
||||
try {
|
||||
await render(entries, options);
|
||||
} finally {
|
||||
if (!options.interactive) {
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
@@ -356,8 +364,7 @@ if (require.main === module) {
|
||||
default: false,
|
||||
})
|
||||
.option('interactive', {
|
||||
describe:
|
||||
'Run all tests and keep the test server running (this option will be reworked later)',
|
||||
describe: 'Run all tests and keep the test server running for 30 minutes',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
|
||||
@@ -34,4 +34,10 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
// allow imports from 'ol/module' instead of specifiying the source path
|
||||
ol: path.join(__dirname, '..', 'src', 'ol'),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
405
src/ol/layer/MapboxVector.js
Normal file
405
src/ol/layer/MapboxVector.js
Normal file
@@ -0,0 +1,405 @@
|
||||
/**
|
||||
* @module ol/layer/MapboxVector
|
||||
*/
|
||||
import BaseEvent from '../events/Event.js';
|
||||
import EventType from '../events/EventType.js';
|
||||
import MVT from '../format/MVT.js';
|
||||
import SourceState from '../source/State.js';
|
||||
import VectorTileLayer from '../layer/VectorTile.js';
|
||||
import VectorTileSource from '../source/VectorTile.js';
|
||||
import {applyStyle} from 'ol-mapbox-style';
|
||||
|
||||
const mapboxBaseUrl = 'https://api.mapbox.com';
|
||||
|
||||
/**
|
||||
* Gets the path from a mapbox:// URL.
|
||||
* @param {string} url The Mapbox URL.
|
||||
* @return {string} The path.
|
||||
* @private
|
||||
*/
|
||||
export function getMapboxPath(url) {
|
||||
const startsWith = 'mapbox://';
|
||||
if (url.indexOf(startsWith) !== 0) {
|
||||
return '';
|
||||
}
|
||||
return url.slice(startsWith.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns mapbox:// sprite URLs into resolvable URLs.
|
||||
* @param {string} url The sprite URL.
|
||||
* @param {string} token The access token.
|
||||
* @return {string} A resolvable URL.
|
||||
* @private
|
||||
*/
|
||||
export function normalizeSpriteUrl(url, token) {
|
||||
const mapboxPath = getMapboxPath(url);
|
||||
if (!mapboxPath) {
|
||||
return url;
|
||||
}
|
||||
const startsWith = 'sprites/';
|
||||
if (mapboxPath.indexOf(startsWith) !== 0) {
|
||||
throw new Error(`unexpected sprites url: ${url}`);
|
||||
}
|
||||
const sprite = mapboxPath.slice(startsWith.length);
|
||||
|
||||
return `${mapboxBaseUrl}/styles/v1/${sprite}/sprite?access_token=${token}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns mapbox:// glyphs URLs into resolvable URLs.
|
||||
* @param {string} url The glyphs URL.
|
||||
* @param {string} token The access token.
|
||||
* @return {string} A resolvable URL.
|
||||
* @private
|
||||
*/
|
||||
export function normalizeGlyphsUrl(url, token) {
|
||||
const mapboxPath = getMapboxPath(url);
|
||||
if (!mapboxPath) {
|
||||
return url;
|
||||
}
|
||||
const startsWith = 'fonts/';
|
||||
if (mapboxPath.indexOf(startsWith) !== 0) {
|
||||
throw new Error(`unexpected fonts url: ${url}`);
|
||||
}
|
||||
const font = mapboxPath.slice(startsWith.length);
|
||||
|
||||
return `${mapboxBaseUrl}/fonts/v1/${font}/0-255.pbf?access_token=${token}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns mapbox:// style URLs into resolvable URLs.
|
||||
* @param {string} url The style URL.
|
||||
* @param {string} token The access token.
|
||||
* @return {string} A resolvable URL.
|
||||
* @private
|
||||
*/
|
||||
export function normalizeStyleUrl(url, token) {
|
||||
const mapboxPath = getMapboxPath(url);
|
||||
if (!mapboxPath) {
|
||||
return url;
|
||||
}
|
||||
const startsWith = 'styles/';
|
||||
if (mapboxPath.indexOf(startsWith) !== 0) {
|
||||
throw new Error(`unexpected style url: ${url}`);
|
||||
}
|
||||
const style = mapboxPath.slice(startsWith.length);
|
||||
|
||||
return `${mapboxBaseUrl}/styles/v1/${style}?&access_token=${token}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns mapbox:// source URLs into vector tile URL templates.
|
||||
* @param {string} url The source URL.
|
||||
* @param {string} token The access token.
|
||||
* @return {string} A vector tile template.
|
||||
* @private
|
||||
*/
|
||||
export function normalizeSourceUrl(url, token) {
|
||||
const mapboxPath = getMapboxPath(url);
|
||||
if (!mapboxPath) {
|
||||
return url;
|
||||
}
|
||||
return `https://{a-d}.tiles.mapbox.com/v4/${mapboxPath}/{z}/{x}/{y}.vector.pbf?access_token=${token}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @classdesc
|
||||
* Event emitted on configuration or loading error.
|
||||
*/
|
||||
class ErrorEvent extends BaseEvent {
|
||||
/**
|
||||
* @param {Error} error error object.
|
||||
*/
|
||||
constructor(error) {
|
||||
super(EventType.ERROR);
|
||||
|
||||
/**
|
||||
* @type {Error}
|
||||
*/
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} StyleObject
|
||||
* @property {Object<string, SourceObject>} sources The style sources.
|
||||
* @property {string} sprite The sprite URL.
|
||||
* @property {string} glyphs The glyphs URL.
|
||||
* @property {Array<LayerObject>} layers The style layers.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SourceObject
|
||||
* @property {string} url The source URL.
|
||||
* @property {SourceType} type The source type.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Mapbox source type.
|
||||
* @enum {string}
|
||||
*/
|
||||
const SourceType = {
|
||||
VECTOR: 'vector',
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {Object} LayerObject
|
||||
* @property {string} id The layer id.
|
||||
* @property {string} source The source id.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Options
|
||||
* @property {string} styleUrl The URL of the Mapbox style object to use for this layer. For a
|
||||
* style created with Mapbox Studio and hosted on Mapbox, this will look like
|
||||
* 'mapbox://styles/you/your-style'.
|
||||
* @property {string} accessToken The access token for your Mapbox style.
|
||||
* @property {string} [source] If your style uses more than one source, you need to use either the
|
||||
* `source` property or the `layers` property to limit rendering to a single vector source. The
|
||||
* `source` property corresponds to the id of a vector source in your Mapbox style.
|
||||
* @property {Array<string>} [layers] Limit rendering to the list of included layers. All layers
|
||||
* must share the same vector soource. If your style uses more than one source, you need to use
|
||||
* either the `source` property or the `layers` property to limit rendering to a single vector
|
||||
* source.
|
||||
* @property {boolean} [declutter=true] Declutter images and text. Decluttering is applied to all
|
||||
* image and text styles of all Vector and VectorTile layers that have set this to `true`. The priority
|
||||
* is defined by the z-index of the layer, the `zIndex` of the style and the render order of features.
|
||||
* Higher z-index means higher priority. Within the same z-index, a feature rendered before another has
|
||||
* higher priority.
|
||||
* @property {string} [className='ol-layer'] A CSS class name to set to the layer element.
|
||||
* @property {number} [opacity=1] Opacity (0, 1).
|
||||
* @property {boolean} [visible=true] Visibility.
|
||||
* @property {import("../extent.js").Extent} [extent] The bounding extent for layer rendering. The layer will not be
|
||||
* rendered outside of this extent.
|
||||
* @property {number} [zIndex] The z-index for layer rendering. At rendering time, the layers
|
||||
* will be ordered, first by Z-index and then by position. When `undefined`, a `zIndex` of 0 is assumed
|
||||
* for layers that are added to the map's `layers` collection, or `Infinity` when the layer's `setMap()`
|
||||
* method was used.
|
||||
* @property {number} [minResolution] The minimum resolution (inclusive) at which this layer will be
|
||||
* visible.
|
||||
* @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will
|
||||
* be visible.
|
||||
* @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be
|
||||
* visible.
|
||||
* @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will
|
||||
* be visible.
|
||||
* @property {import("../render.js").OrderFunction} [renderOrder] Render order. Function to be used when sorting
|
||||
* features before rendering. By default features are drawn in the order that they are created. Use
|
||||
* `null` to avoid the sort, but get an undefined draw order.
|
||||
* @property {number} [renderBuffer=100] The buffer in pixels around the tile extent used by the
|
||||
* renderer when getting features from the vector tile for the rendering or hit-detection.
|
||||
* Recommended value: Vector tiles are usually generated with a buffer, so this value should match
|
||||
* the largest possible buffer of the used tiles. It should be at least the size of the largest
|
||||
* point symbol or line width.
|
||||
* @property {import("./VectorTileRenderType.js").default|string} [renderMode='hybrid'] Render mode for vector tiles:
|
||||
* * `'image'`: Vector tiles are rendered as images. Great performance, but point symbols and texts
|
||||
* are always rotated with the view and pixels are scaled during zoom animations. When `declutter`
|
||||
* is set to `true`, the decluttering is done per tile resulting in labels and point symbols getting
|
||||
* cut off at tile boundaries.
|
||||
* * `'hybrid'`: Polygon and line elements are rendered as images, so pixels are scaled during zoom
|
||||
* animations. Point symbols and texts are accurately rendered as vectors and can stay upright on
|
||||
* rotated views.
|
||||
* * `'vector'`: Everything is rendered as vectors. Use this mode for improved performance on vector
|
||||
* tile layers with only a few rendered features (e.g. for highlighting a subset of features of
|
||||
* another layer with the same source).
|
||||
* @property {import("../PluggableMap.js").default} [map] Sets the layer as overlay on a map. The map will not manage
|
||||
* this layer in its layers collection, and the layer will be rendered on top. This is useful for
|
||||
* temporary layers. The standard way to add a layer to a map and have it managed by the map is to
|
||||
* use {@link module:ol/Map#addLayer}.
|
||||
* @property {boolean} [updateWhileAnimating=false] When set to `true`, feature batches will be
|
||||
* recreated during animations. This means that no vectors will be shown clipped, but the setting
|
||||
* will have a performance impact for large amounts of vector data. When set to `false`, batches
|
||||
* will be recreated when no animation is active.
|
||||
* @property {boolean} [updateWhileInteracting=false] When set to `true`, feature batches will be
|
||||
* recreated during interactions. See also `updateWhileAnimating`.
|
||||
* @property {number} [preload=0] Preload. Load low-resolution tiles up to `preload` levels. `0`
|
||||
* means no preloading.
|
||||
* @property {boolean} [useInterimTilesOnError=true] Use interim tiles on error.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @classdesc
|
||||
* A vector tile layer based on a Mapbox style that uses a single vector source. Configure
|
||||
* the layer with the `styleUrl` and `accessToken` shown in Mapbox Studio's share panel.
|
||||
* If the style uses more than one source, use the `source` property to choose a single
|
||||
* vector source. If you want to render a subset of the layers in the style, use the `layers`
|
||||
* property (all layers must share the same vector source). See the constructor options for
|
||||
* more detail.
|
||||
*
|
||||
* var map = new Map({
|
||||
* view: new View({
|
||||
* center: [0, 0],
|
||||
* zoom: 1
|
||||
* }),
|
||||
* layers: [
|
||||
* new MapboxVector({
|
||||
* styleUrl: 'mapbox://styles/mapbox/bright-v9',
|
||||
* accessToken: 'your-mapbox-access-token-here'
|
||||
* })
|
||||
* ],
|
||||
* target: 'map'
|
||||
* });
|
||||
*
|
||||
* On configuration or loading error, the layer will trigger an `'error'` event. Listeners
|
||||
* will receive an object with an `error` property that can be used to diagnose the problem.
|
||||
*
|
||||
* @param {Options} options Options.
|
||||
* @extends {VectorTileLayer}
|
||||
* @fires module:ol/events/Event~BaseEvent#event:error
|
||||
* @api
|
||||
*/
|
||||
class MapboxVectorLayer extends VectorTileLayer {
|
||||
/**
|
||||
* @param {Options} options Layer options. At a minimum, `styleUrl` and `accessToken`
|
||||
* must be provided.
|
||||
*/
|
||||
constructor(options) {
|
||||
const declutter = 'declutter' in options ? options.declutter : true;
|
||||
const source = new VectorTileSource({
|
||||
state: SourceState.LOADING,
|
||||
format: new MVT(),
|
||||
});
|
||||
|
||||
super({
|
||||
source: source,
|
||||
declutter: declutter,
|
||||
className: options.className,
|
||||
opacity: options.opacity,
|
||||
visible: options.visible,
|
||||
zIndex: options.zIndex,
|
||||
minResolution: options.minResolution,
|
||||
maxResolution: options.maxResolution,
|
||||
minZoom: options.minZoom,
|
||||
maxZoom: options.maxZoom,
|
||||
renderOrder: options.renderOrder,
|
||||
renderBuffer: options.renderBuffer,
|
||||
renderMode: options.renderMode,
|
||||
map: options.map,
|
||||
updateWhileAnimating: options.updateWhileAnimating,
|
||||
updateWhileInteracting: options.updateWhileInteracting,
|
||||
preload: options.preload,
|
||||
useInterimTilesOnError: options.useInterimTilesOnError,
|
||||
});
|
||||
|
||||
this.sourceId = options.source;
|
||||
this.layers = options.layers;
|
||||
this.accessToken = options.accessToken;
|
||||
this.fetchStyle(options.styleUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the style object.
|
||||
* @param {string} styleUrl The URL of the style to load.
|
||||
* @protected
|
||||
*/
|
||||
fetchStyle(styleUrl) {
|
||||
const url = normalizeStyleUrl(styleUrl, this.accessToken);
|
||||
fetch(url)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`unexpected response when fetching style: ${response.status}`
|
||||
);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((style) => {
|
||||
this.onStyleLoad(style);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.handleError(error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the loaded style object.
|
||||
* @param {StyleObject} style The loaded style.
|
||||
* @protected
|
||||
*/
|
||||
onStyleLoad(style) {
|
||||
let sourceId;
|
||||
let sourceIdOrLayersList;
|
||||
if (this.layers) {
|
||||
// confirm all layers share the same source
|
||||
const lookup = {};
|
||||
for (let i = 0; i < style.layers.length; ++i) {
|
||||
const layer = style.layers[i];
|
||||
if (layer.source) {
|
||||
lookup[layer.id] = layer.source;
|
||||
}
|
||||
}
|
||||
let firstSource;
|
||||
for (let i = 0; i < this.layers.length; ++i) {
|
||||
const candidate = lookup[this.layers[i]];
|
||||
if (!candidate) {
|
||||
this.handleError(
|
||||
new Error(`could not find source for ${this.layers[i]}`)
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!firstSource) {
|
||||
firstSource = candidate;
|
||||
} else if (firstSource !== candidate) {
|
||||
this.handleError(
|
||||
new Error(
|
||||
`layers can only use a single source, found ${firstSource} and ${candidate}`
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
sourceId = firstSource;
|
||||
sourceIdOrLayersList = this.layers;
|
||||
} else {
|
||||
sourceId = this.sourceId;
|
||||
sourceIdOrLayersList = sourceId;
|
||||
}
|
||||
|
||||
if (!sourceIdOrLayersList) {
|
||||
// default to the first source in the style
|
||||
sourceId = Object.keys(style.sources)[0];
|
||||
sourceIdOrLayersList = sourceId;
|
||||
}
|
||||
|
||||
if (style.sprite) {
|
||||
style.sprite = normalizeSpriteUrl(style.sprite, this.accessToken);
|
||||
}
|
||||
|
||||
if (style.glyphs) {
|
||||
style.glyphs = normalizeGlyphsUrl(style.glyphs, this.accessToken);
|
||||
}
|
||||
|
||||
const styleSource = style.sources[sourceId];
|
||||
if (styleSource.type !== SourceType.VECTOR) {
|
||||
this.handleError(
|
||||
new Error(`only works for vector sources, found ${styleSource.type}`)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const source = this.getSource();
|
||||
source.setUrl(normalizeSourceUrl(styleSource.url, this.accessToken));
|
||||
|
||||
applyStyle(this, style, sourceIdOrLayersList)
|
||||
.then(() => {
|
||||
source.setState(SourceState.READY);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.handleError(error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle configuration or loading error.
|
||||
* @param {Error} error The error.
|
||||
* @protected
|
||||
*/
|
||||
handleError(error) {
|
||||
this.dispatchEvent(new ErrorEvent(error));
|
||||
const source = this.getSource();
|
||||
source.setState(SourceState.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
export default MapboxVectorLayer;
|
||||
@@ -163,7 +163,6 @@ class Source extends BaseObject {
|
||||
/**
|
||||
* Set the state of the source.
|
||||
* @param {import("./State.js").default} state State.
|
||||
* @protected
|
||||
*/
|
||||
setState(state) {
|
||||
this.state_ = state;
|
||||
|
||||
@@ -118,6 +118,12 @@ module.exports = function (karma) {
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
// allow imports from 'ol/module' instead of specifiying the source path
|
||||
ol: path.join(__dirname, '..', 'src', 'ol'),
|
||||
},
|
||||
},
|
||||
},
|
||||
webpackMiddleware: {
|
||||
noInfo: true,
|
||||
|
||||
94
test/spec/ol/layer/MapboxVector.test.js
Normal file
94
test/spec/ol/layer/MapboxVector.test.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
getMapboxPath,
|
||||
normalizeSourceUrl,
|
||||
normalizeSpriteUrl,
|
||||
normalizeStyleUrl,
|
||||
} from '../../../../src/ol/layer/MapboxVector.js';
|
||||
|
||||
describe('ol/layer/MapboxVector', () => {
|
||||
describe('getMapboxPath()', () => {
|
||||
const cases = [
|
||||
{
|
||||
url: 'mapbox://path/to/resource',
|
||||
expected: 'path/to/resource',
|
||||
},
|
||||
{
|
||||
url: 'mapbox://path/to/resource?query',
|
||||
expected: 'path/to/resource?query',
|
||||
},
|
||||
{
|
||||
url: 'https://example.com/resource',
|
||||
expected: '',
|
||||
},
|
||||
];
|
||||
|
||||
for (const c of cases) {
|
||||
it(`works for ${c.url}`, () => {
|
||||
expect(getMapboxPath(c.url)).to.be(c.expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('normalizeStyleUrl()', () => {
|
||||
const cases = [
|
||||
{
|
||||
url: 'mapbox://styles/mapbox/bright-v9',
|
||||
expected:
|
||||
'https://api.mapbox.com/styles/v1/mapbox/bright-v9?&access_token=test-token',
|
||||
},
|
||||
{
|
||||
url: 'https://example.com/style',
|
||||
expected: 'https://example.com/style',
|
||||
},
|
||||
];
|
||||
|
||||
const token = 'test-token';
|
||||
for (const c of cases) {
|
||||
it(`works for ${c.url}`, () => {
|
||||
expect(normalizeStyleUrl(c.url, token)).to.be(c.expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('normalizeSpriteUrl()', () => {
|
||||
const cases = [
|
||||
{
|
||||
url: 'mapbox://sprites/mapbox/bright-v9',
|
||||
expected:
|
||||
'https://api.mapbox.com/styles/v1/mapbox/bright-v9/sprite?access_token=test-token',
|
||||
},
|
||||
{
|
||||
url: 'https://example.com/sprite',
|
||||
expected: 'https://example.com/sprite',
|
||||
},
|
||||
];
|
||||
|
||||
const token = 'test-token';
|
||||
for (const c of cases) {
|
||||
it(`works for ${c.url}`, () => {
|
||||
expect(normalizeSpriteUrl(c.url, token)).to.be(c.expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('normalizeSourceUrl()', () => {
|
||||
const cases = [
|
||||
{
|
||||
url: 'mapbox://mapbox.mapbox-streets-v7',
|
||||
expected:
|
||||
'https://{a-d}.tiles.mapbox.com/v4/mapbox.mapbox-streets-v7/{z}/{x}/{y}.vector.pbf?access_token=test-token',
|
||||
},
|
||||
{
|
||||
url: 'https://example.com/source/{z}/{x}/{y}.pbf',
|
||||
expected: 'https://example.com/source/{z}/{x}/{y}.pbf',
|
||||
},
|
||||
];
|
||||
|
||||
const token = 'test-token';
|
||||
for (const c of cases) {
|
||||
it(`works for ${c.url}`, () => {
|
||||
expect(normalizeSourceUrl(c.url, token)).to.be(c.expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user