diff --git a/README.md b/README.md index 9cc9a90b..766428bc 100644 --- a/README.md +++ b/README.md @@ -586,7 +586,7 @@ sure that sprite image loading works: | `sourceOrLayers` | `string` \| `string`\[] | `undefined` | `source` key or an array of layer `id`s from the Mapbox Style object. When a `source` key is provided, all layers for the specified source will be included in the style function. When layer `id`s are provided, they must be from layers that use the same source. | | `resolutions` | `number`\[] | `defaultResolutions` | Resolutions for mapping resolution to zoom level. | | `spriteData` | `any` | `undefined` | Sprite data from the url specified in the Mapbox Style object's `sprite` property. Only required if a `sprite` property is specified in the Mapbox Style object. | -| `spriteImageUrl` | `string` \| `Request` | `undefined` | Sprite image url for the sprite specified in the Mapbox Style object's `sprite` property. Only required if a `sprite` property is specified in the Mapbox Style object. | +| `spriteImageUrl` | `string` \| `Request` \| `Promise`<`string` \| `Request`> | `undefined` | Sprite image url for the sprite specified in the Mapbox Style object's `sprite` property. Only required if a `sprite` property is specified in the Mapbox Style object. | | `getFonts` | (`arg0`: `string`\[], `arg1`: `string`) => `string`\[] | `undefined` | Function that receives a font stack and the url template from the GL style's `metadata['ol:webfonts']` property (if set) as arguments, and returns a (modified) font stack that is available. Font names are the names used in the Mapbox Style object. If not provided, the font stack will be used as-is. This function can also be used for loading web fonts. | | `getImage?` | (`arg0`: `VectorLayer`<`any`> \| `VectorTileLayer`, `arg1`: `string`) => `string` \| `HTMLCanvasElement` \| `HTMLImageElement` | `undefined` | Function that returns an image or a URL for an image name. If the result is an HTMLImageElement, it must already be loaded. The layer can be used to call layer.changed() when the loading and processing of the image has finished. This function can be used for icons not in the sprite or to override sprite icons. | | `...args` | `any` | `undefined` | - | @@ -1167,11 +1167,11 @@ as object, when they contain a relative sprite url, or sources referencing data #### transformRequest -• **transformRequest**: (`arg0`: `string`, `arg1`: [`ResourceType`](#ResourceType)) => `string` \| `void` \| `Request` +• **transformRequest**: (`arg0`: `string`, `arg1`: [`ResourceType`](#ResourceType)) => `string` \| `void` \| `Request` \| `Promise`<`string` \| `Request`> ##### Type declaration -▸ (`arg0`, `arg1`): `string` \| `void` \| `Request` +▸ (`arg0`, `arg1`): `string` \| `void` \| `Request` \| `Promise`<`string` \| `Request`> Function for controlling how `ol-mapbox-style` fetches resources. Can be used for modifying the url, adding headers or setting credentials options. Called with the url and the resource @@ -1187,7 +1187,7 @@ the original request will not be modified. ###### Returns -`string` \| `void` \| `Request` +`string` \| `void` \| `Request` \| `Promise`<`string` \| `Request`> diff --git a/src/apply.js b/src/apply.js index 5813c40f..55990fb9 100644 --- a/src/apply.js +++ b/src/apply.js @@ -65,11 +65,11 @@ import { /** * @typedef {Object} Options * @property {string} [accessToken] Access token for 'mapbox://' urls. - * @property {function(string, import("./util.js").ResourceType): (Request|string|void)} [transformRequest] + * @property {function(string, import("./util.js").ResourceType): (Request|string|Promise|void)} [transformRequest] * Function for controlling how `ol-mapbox-style` fetches resources. Can be used for modifying * the url, adding headers or setting credentials options. Called with the url and the resource - * type as arguments, this function is supposed to return a `Request` or a url `string`. Without a return value, - * the original request will not be modified. + * type as arguments, this function is supposed to return a `Request` or a url `string`, or a promise tehereof. + * Without a return value the original request will not be modified. * @property {string} [projection='EPSG:3857'] Only useful when working with non-standard projections. * Code of a projection registered with OpenLayers. All sources of the style must be provided in this * projection. The projection must also have a valid extent defined, which will be used to determine the @@ -419,7 +419,10 @@ export function applyStyle( const transformed = options.transformRequest(spriteImageUrl, 'SpriteImage') || spriteImageUrl; - if (transformed instanceof Request) { + if ( + transformed instanceof Request || + transformed instanceof Promise + ) { spriteImageUrl = transformed; } } diff --git a/src/stylefunction.js b/src/stylefunction.js index b176769b..1c381d2e 100644 --- a/src/stylefunction.js +++ b/src/stylefunction.js @@ -11,6 +11,7 @@ import RenderFeature from 'ol/render/Feature.js'; import Stroke from 'ol/style/Stroke.js'; import Style from 'ol/style/Style.js'; import Text from 'ol/style/Text.js'; +import {toPromise} from 'ol/functions.js'; import Color from '@mapbox/mapbox-gl-style-spec/util/color.js'; import convertFunction from '@mapbox/mapbox-gl-style-spec/function/convert.js'; @@ -319,7 +320,7 @@ export const styleFunctionArgs = {}; * @param {Object} spriteData Sprite data from the url specified in * the Mapbox Style object's `sprite` property. Only required if a `sprite` * property is specified in the Mapbox Style object. - * @param {string|Request} spriteImageUrl Sprite image url for the sprite + * @param {string|Request|Promise} spriteImageUrl Sprite image url for the sprite * specified in the Mapbox Style object's `sprite` property. Only required if a * `sprite` property is specified in the Mapbox Style object. * @param {function(Array, string=):Array} getFonts Function that @@ -361,21 +362,23 @@ export function stylefunction( if (typeof Image !== 'undefined') { const img = new Image(); let blobUrl; - if (spriteImageUrl instanceof Request) { - fetch(spriteImageUrl) - .then((response) => response.blob()) - .then((blob) => { - blobUrl = URL.createObjectURL(blob); - img.src = blobUrl; - }) - .catch(() => {}); - } else { - img.crossOrigin = 'anonymous'; - img.src = spriteImageUrl; - if (blobUrl) { - URL.revokeObjectURL(blobUrl); + toPromise(() => spriteImageUrl).then((spriteImageUrl) => { + if (spriteImageUrl instanceof Request) { + fetch(spriteImageUrl) + .then((response) => response.blob()) + .then((blob) => { + blobUrl = URL.createObjectURL(blob); + img.src = blobUrl; + }) + .catch(() => {}); + } else { + img.crossOrigin = 'anonymous'; + img.src = spriteImageUrl; + if (blobUrl) { + URL.revokeObjectURL(blobUrl); + } } - } + }); img.onload = function () { spriteImage = img; spriteImageSize = [img.width, img.height]; diff --git a/src/util.js b/src/util.js index aab67511..911e78ac 100644 --- a/src/util.js +++ b/src/util.js @@ -3,6 +3,7 @@ import {VectorTile} from 'ol'; import {expandUrl} from 'ol/tileurlfunction.js'; import {getUid} from 'ol/util.js'; import {normalizeSourceUrl, normalizeStyleUrl} from './mapbox.js'; +import {toPromise} from 'ol/functions.js'; /** @typedef {'Style'|'Source'|'Sprite'|'SpriteImage'|'Tiles'|'GeoJSON'} ResourceType */ @@ -117,29 +118,33 @@ export function fetchResource(resourceType, url, options = {}, metadata) { } return pendingRequests[url][1]; } - let transformedRequest = options.transformRequest + const transformedRequest = options.transformRequest ? options.transformRequest(url, resourceType) || url : url; - if (!(transformedRequest instanceof Request)) { - transformedRequest = new Request(transformedRequest); - } - if (!transformedRequest.headers.get('Accept')) { - transformedRequest.headers.set('Accept', 'application/json'); - } - if (metadata) { - metadata.request = transformedRequest; - } - const pendingRequest = fetch(transformedRequest) - .then(function (response) { - delete pendingRequests[url]; - return response.ok - ? response.json() - : Promise.reject(new Error('Error fetching source ' + url)); - }) - .catch(function (error) { - delete pendingRequests[url]; - return Promise.reject(new Error('Error fetching source ' + url)); - }); + const pendingRequest = toPromise(() => transformedRequest).then( + (transformedRequest) => { + if (!(transformedRequest instanceof Request)) { + transformedRequest = new Request(transformedRequest); + } + if (!transformedRequest.headers.get('Accept')) { + transformedRequest.headers.set('Accept', 'application/json'); + } + if (metadata) { + metadata.request = transformedRequest; + } + return fetch(transformedRequest) + .then(function (response) { + delete pendingRequests[url]; + return response.ok + ? response.json() + : Promise.reject(new Error('Error fetching source ' + url)); + }) + .catch(function (error) { + delete pendingRequests[url]; + return Promise.reject(new Error('Error fetching source ' + url)); + }); + } + ); pendingRequests[url] = [transformedRequest, pendingRequest]; return pendingRequest; } @@ -181,34 +186,38 @@ export function getTileJson(glSource, styleUrl, options = {}) { : src; if (tile instanceof VectorTile) { tile.setLoader((extent, resolution, projection) => { - fetch(transformedRequest) - .then((response) => response.arrayBuffer()) - .then((data) => { - const format = tile.getFormat(); - const features = format.readFeatures(data, { - extent: extent, - featureProjection: projection, - }); - // @ts-ignore - tile.setFeatures(features); - }) - .catch((e) => tile.setState(TileState.ERROR)); + toPromise(() => transformedRequest).then((transformedRequest) => { + fetch(transformedRequest) + .then((response) => response.arrayBuffer()) + .then((data) => { + const format = tile.getFormat(); + const features = format.readFeatures(data, { + extent: extent, + featureProjection: projection, + }); + // @ts-ignore + tile.setFeatures(features); + }) + .catch((e) => tile.setState(TileState.ERROR)); + }); }); } else { const img = tile.getImage(); - if (transformedRequest instanceof Request) { - fetch(transformedRequest) - .then((response) => response.blob()) - .then((blob) => { - const url = URL.createObjectURL(blob); - img.addEventListener('load', () => URL.revokeObjectURL(url)); - img.addEventListener('error', () => URL.revokeObjectURL(url)); - img.src = url; - }) - .catch((e) => tile.setState(TileState.ERROR)); - } else { - img.src = transformedRequest; - } + toPromise(() => transformedRequest).then((transformedRequest) => { + if (transformedRequest instanceof Request) { + fetch(transformedRequest) + .then((response) => response.blob()) + .then((blob) => { + const url = URL.createObjectURL(blob); + img.addEventListener('load', () => URL.revokeObjectURL(url)); + img.addEventListener('error', () => URL.revokeObjectURL(url)); + img.src = url; + }) + .catch((e) => tile.setState(TileState.ERROR)); + } else { + img.src = transformedRequest; + } + }); } }; } diff --git a/test/MapboxVectorLayer.test.js b/test/MapboxVectorLayer.test.js index a624ef73..63ccb57e 100644 --- a/test/MapboxVectorLayer.test.js +++ b/test/MapboxVectorLayer.test.js @@ -162,20 +162,28 @@ describe('ol/layer/MapboxVector', () => { afterEach(function () { window.fetch = originalFetch; }); - it('applies correct access token', function () { + it('applies correct access token', function (done) { new MapboxVectorLayer({ styleUrl: 'mapbox://styles/mapbox/streets-v7', accessToken: '123', - }); - should(fetchUrl.url).eql( - 'https://api.mapbox.com/styles/v1/mapbox/streets-v7?&access_token=123' - ); + }) + .getSource() + .once('change', () => { + should(fetchUrl.url).eql( + 'https://api.mapbox.com/styles/v1/mapbox/streets-v7?&access_token=123' + ); + done(); + }); }); - it('applies correct access token from url', function () { + it('applies correct access token from url', function (done) { new MapboxVectorLayer({ styleUrl: 'foo?key=123', - }); - should(fetchUrl.url).eql(`${location.origin}/foo?key=123`); + }) + .getSource() + .once('change', () => { + should(fetchUrl.url).eql(`${location.origin}/foo?key=123`); + done(); + }); }); }); }); diff --git a/test/apply.test.js b/test/apply.test.js index b337b4c2..14b9d992 100644 --- a/test/apply.test.js +++ b/test/apply.test.js @@ -236,12 +236,14 @@ describe('ol-mapbox-style', function () { 1, map.getView().getProjection() ); - window.fetch = originalFetch; - const url = new URL(requests[0].url); - const bbox = url.searchParams.get('bbox'); - const extent = map.getView().calculateExtent(); - should(bbox).be.equal(extent.join(',')); - done(); + source.once('change', () => { + window.fetch = originalFetch; + const url = new URL(requests[0].url); + const bbox = url.searchParams.get('bbox'); + const extent = map.getView().calculateExtent(); + should(bbox).be.equal(extent.join(',')); + done(); + }); }) .catch(done); }); @@ -340,12 +342,14 @@ describe('ol-mapbox-style', function () { 1, get('EPSG:3857') ); - window.fetch = originalFetch; - const url = new URL(requests[0].url); - should(url.searchParams.get('transformRequest')).be.equal('true'); - should(source).be.instanceof(VectorSource); - should(layer.getStyle()).be.a.Function(); - done(); + source.once('change', () => { + window.fetch = originalFetch; + const url = new URL(requests[0].url); + should(url.searchParams.get('transformRequest')).be.equal('true'); + should(source).be.instanceof(VectorSource); + should(layer.getStyle()).be.a.Function(); + done(); + }); }) .catch(done); }); diff --git a/test/applyStyle.test.js b/test/applyStyle.test.js index fc1e828d..37afb3b8 100644 --- a/test/applyStyle.test.js +++ b/test/applyStyle.test.js @@ -73,17 +73,19 @@ describe('applyStyle with source creation', function () { layer .getSource() .loadFeatures([0, 0, 10000, 10000], 10, get('EPSG:3857')); - window.fetch = originalFetch; - try { - should(layer.getSource()).be.an.instanceOf(VectorSource); - should(requests[0].url).equal( - `${location.origin}/fixtures/states.geojson?foo=bar` - ); - should(layer.getStyle()).be.an.instanceOf(Function); - done(); - } catch (e) { - done(e); - } + layer.getSource().once('change', () => { + window.fetch = originalFetch; + try { + should(layer.getSource()).be.an.instanceOf(VectorSource); + should(requests[0].url).equal( + `${location.origin}/fixtures/states.geojson?foo=bar` + ); + should(layer.getStyle()).be.an.instanceOf(Function); + done(); + } catch (e) { + done(e); + } + }); }); }); it('respects source options from layer config', function (done) { @@ -158,16 +160,20 @@ describe('applyStyle with source creation', function () { try { should(layer.getSource()).be.an.instanceOf(VectorTileSource); const image = {}; + Object.defineProperty(image, 'src', { + set: function (src) { + should(src).equal( + `${location.origin}/fixtures/osm-liberty/tiles/v3/0/0/0.pbf?foo=bar` + ); + should(layer.getStyle()).be.an.instanceOf(Function); + done(); + }, + }); const img = {getImage: () => image}; layer.getSource().getTileLoadFunction()( img, `${location.origin}/fixtures/osm-liberty/tiles/v3/0/0/0.pbf` ); - should(image.src).equal( - `${location.origin}/fixtures/osm-liberty/tiles/v3/0/0/0.pbf?foo=bar` - ); - should(layer.getStyle()).be.an.instanceOf(Function); - done(); } catch (e) { done(e); } @@ -554,6 +560,40 @@ describe('applyStyle supports transformRequest object', function () { done(error); }); }); + it('applies async transformRequest to all Vector Tile request types', function (done) { + const expectedRequestTypes = new Set([ + 'Style', + 'Sprite', + 'SpriteImage', + 'Source', + 'Tiles', + ]); + const seenRequestTypes = new Set(); + apply(document.createElement('div'), '/fixtures/hot-osm/hot-osm.json', { + transformRequest: function (url, type) { + seenRequestTypes.add(type); + return Promise.resolve(new Request(url)); + }, + }) + .then(function (map) { + map.once('rendercomplete', () => { + should.deepEqual( + expectedRequestTypes, + seenRequestTypes, + `Request types seen by transformRequest: ${Array.from( + seenRequestTypes + )} do not match those expected for a Vector Tile style: ${Array.from( + expectedRequestTypes + )}` + ); + done(); + }); + map.setSize([100, 100]); + }) + .catch(function (error) { + done(error); + }); + }); it('applies transformRequest to GeoJSON request types', function (done) { const expectedRequestTypes = new Set(['Style', 'GeoJSON']); const seenRequestTypes = new Set(); @@ -582,4 +622,32 @@ describe('applyStyle supports transformRequest object', function () { done(error); }); }); + it('applies async transformRequest to GeoJSON request types', function (done) { + const expectedRequestTypes = new Set(['Style', 'GeoJSON']); + const seenRequestTypes = new Set(); + apply(document.createElement('div'), '/fixtures/geojson.json', { + transformRequest: function (url, type) { + seenRequestTypes.add(type); + return Promise.resolve(url); + }, + }) + .then(function (map) { + map.once('rendercomplete', () => { + should.deepEqual( + expectedRequestTypes, + seenRequestTypes, + `Request types seen by transformRequest: ${Array.from( + seenRequestTypes + )} do not match those expected for a GeoJSON style: ${Array.from( + expectedRequestTypes + )}` + ); + done(); + }); + map.setSize([100, 100]); + }) + .catch(function (error) { + done(error); + }); + }); }); diff --git a/test/util.test.js b/test/util.test.js index 7d7acb83..c69ef1e8 100644 --- a/test/util.test.js +++ b/test/util.test.js @@ -37,33 +37,38 @@ describe('util', function () { it('adds the request to the metadata for both pending and new requests', function (done) { const metadataNotPending = {}; const metadataPending = {}; - fetchResource( - 'Sprite', - 'my://resource', - { - transformRequest: function (url, resourceType) { - should(url).equal('my://resource'); - should(resourceType).equal('Sprite'); - return new Request('/fixtures/sprites.json'); + Promise.all([ + fetchResource( + 'Sprite', + 'my://resource', + { + transformRequest: function (url, resourceType) { + should(url).equal('my://resource'); + should(resourceType).equal('Sprite'); + return new Request('/fixtures/sprites.json'); + }, }, - }, - metadataNotPending - ); - fetchResource( - 'Sprite', - 'my://resource', - { - transformRequest: function (url, resourceType) { - should(url).equal('my://resource'); - should(resourceType).equal('Sprite'); - return new Request('/fixtures/sprites.json'); + metadataNotPending + ), + fetchResource( + 'Sprite', + 'my://resource', + { + transformRequest: function (url, resourceType) { + should(url).equal('my://resource'); + should(resourceType).equal('Sprite'); + return new Request('/fixtures/sprites.json'); + }, }, - }, - metadataPending - ); - should('request' in metadataPending).true(); - should(metadataPending.request).equal(metadataNotPending.request); - done(); + metadataPending + ), + ]) + .then(() => { + should('request' in metadataPending).true(); + should(metadataPending.request).equal(metadataNotPending.request); + done(); + }) + .catch((err) => done(err)); }); }); describe('getTileJson', function () {