From 7585d49519fed56f2ee264e650d0434f9447eb24 Mon Sep 17 00:00:00 2001 From: Stepan Kuzmin Date: Thu, 2 Feb 2023 15:55:07 +0200 Subject: [PATCH] Add dynamic raster tiles support (#12352) * Add RasterTileSource setTiles and setUrl * Drop `setSourceProperty()`, expose `reload()` --- src/source/raster_tile_source.js | 91 +++++++++++++++++++-- src/source/vector_tile_source.js | 33 +++----- test/unit/source/raster_tile_source.test.js | 74 ++++++++++++++++- test/unit/source/vector_tile_source.test.js | 15 ++-- 4 files changed, 176 insertions(+), 37 deletions(-) diff --git a/src/source/raster_tile_source.js b/src/source/raster_tile_source.js index 8d25fbd03f9..24ad2535c7a 100644 --- a/src/source/raster_tile_source.js +++ b/src/source/raster_tile_source.js @@ -25,6 +25,27 @@ import type { RasterDEMSourceSpecification } from '../style-spec/types.js'; +/** + * A source containing raster tiles. + * See the [Style Specification](https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/#raster) for detailed documentation of options. + * + * @example + * map.addSource('some id', { + * type: 'raster', + * url: 'mapbox://mapbox.satellite', + * tileSize: 256 + * }); + * + * @example + * map.addSource('some id', { + * type: 'raster', + * tiles: ['https://img.nj.gov/imagerywms/Natural2015?bbox={bbox-epsg-3857}&format=image/png&service=WMS&version=1.1.1&request=GetMap&srs=EPSG:3857&transparent=true&width=256&height=256&layers=Natural2015'], + * tileSize: 256 + * }); + * + * @see [Example: Add a raster tile source](https://docs.mapbox.com/mapbox-gl-js/example/map-tiles/) + * @see [Example: Add a WMS source](https://docs.mapbox.com/mapbox-gl-js/example/wms/) + */ class RasterTileSource extends Evented implements Source { type: 'raster' | 'raster-dem'; id: string; @@ -63,7 +84,7 @@ class RasterTileSource extends Evented implements Source { extend(this, pick(options, ['url', 'scheme', 'tileSize'])); } - load() { + load(callback?: Callback) { this._loaded = false; this.fire(new Event('dataloading', {dataType: 'source'})); this._tileJSONRequest = loadTileJSON(this._options, this.map._requestManager, null, null, (err, tileJSON) => { @@ -83,6 +104,8 @@ class RasterTileSource extends Evented implements Source { this.fire(new Event('data', {dataType: 'source', sourceDataType: 'metadata'})); this.fire(new Event('data', {dataType: 'source', sourceDataType: 'content'})); } + + if (callback) callback(err); }); } @@ -95,11 +118,63 @@ class RasterTileSource extends Evented implements Source { this.load(); } + /** + * Reloads the source data and re-renders the map. + * + * @example + * map.getSource('source-id').reload(); + */ + reload() { + this.cancelTileJSONRequest(); + this.load(() => this.map.style._clearSource(this.id)); + } + + /** + * Sets the source `tiles` property and re-renders the map. + * + * @param {string[]} tiles An array of one or more tile source URLs, as in the TileJSON spec. + * @returns {RasterTileSource} Returns itself to allow for method chaining. + * @example + * map.addSource('source-id', { + * type: 'raster', + * tiles: ['https://some_end_point.net/{z}/{x}/{y}.png'], + * tileSize: 256 + * }); + * + * // Set the endpoint associated with a raster tile source. + * map.getSource('source-id').setTiles(['https://another_end_point.net/{z}/{x}/{y}.png']); + */ + setTiles(tiles: Array): this { + this._options.tiles = tiles; + this.reload(); + + return this; + } + + /** + * Sets the source `url` property and re-renders the map. + * + * @param {string} url A URL to a TileJSON resource. Supported protocols are `http:`, `https:`, and `mapbox://`. + * @returns {RasterTileSource} Returns itself to allow for method chaining. + * @example + * map.addSource('source-id', { + * type: 'raster', + * url: 'mapbox://mapbox.satellite' + * }); + * + * // Update raster tile source to a new URL endpoint + * map.getSource('source-id').setUrl('mapbox://mapbox.satellite'); + */ + setUrl(url: string): this { + this.url = url; + this._options.url = url; + this.reload(); + + return this; + } + onRemove() { - if (this._tileJSONRequest) { - this._tileJSONRequest.cancel(); - this._tileJSONRequest = null; - } + this.cancelTileJSONRequest(); } serialize(): RasterSourceSpecification | RasterDEMSourceSpecification { @@ -163,6 +238,12 @@ class RasterTileSource extends Evented implements Source { hasTransition(): boolean { return false; } + + cancelTileJSONRequest() { + if (!this._tileJSONRequest) return; + this._tileJSONRequest.cancel(); + this._tileJSONRequest = null; + } } export default RasterTileSource; diff --git a/src/source/vector_tile_source.js b/src/source/vector_tile_source.js index 95712c939a0..9be3c1fb9f9 100644 --- a/src/source/vector_tile_source.js +++ b/src/source/vector_tile_source.js @@ -143,22 +143,15 @@ class VectorTileSource extends Evented implements Source { this.load(); } + /** + * Reloads the source data and re-renders the map. + * + * @example + * map.getSource('source-id').reload(); + */ reload() { this.cancelTileJSONRequest(); - - const clearTiles = () => { - const sourceCaches = this.map.style._getSourceCaches(this.id); - for (const sourceCache of sourceCaches) { - sourceCache.clearTiles(); - } - }; - - this.load(clearTiles); - } - - setSourceProperty(callback: Function) { - callback(); - this.reload(); + this.load(() => this.map.style._clearSource(this.id)); } /** @@ -167,17 +160,15 @@ class VectorTileSource extends Evented implements Source { * @param {string[]} tiles An array of one or more tile source URLs, as in the TileJSON spec. * @returns {VectorTileSource} Returns itself to allow for method chaining. * @example - * map.addSource('vector_source_id', { + * map.addSource('source-id', { * type: 'vector', * tiles: ['https://some_end_point.net/{z}/{x}/{y}.mvt'], * minzoom: 6, * maxzoom: 14 * }); * - * const vectorTileSource = map.getSource('vector_source_id'); - * * // Set the endpoint associated with a vector tile source. - * vectorTileSource.setTiles(['https://another_end_point.net/{z}/{x}/{y}.mvt']); + * map.getSource('source-id').setTiles(['https://another_end_point.net/{z}/{x}/{y}.mvt']); */ setTiles(tiles: Array): this { this._options.tiles = tiles; @@ -192,15 +183,13 @@ class VectorTileSource extends Evented implements Source { * @param {string} url A URL to a TileJSON resource. Supported protocols are `http:`, `https:`, and `mapbox://`. * @returns {VectorTileSource} Returns itself to allow for method chaining. * @example - * map.addSource('vector_source_id', { + * map.addSource('source-id', { * type: 'vector', * url: 'mapbox://mapbox.mapbox-streets-v7' * }); * - * const vectorTileSource = map.getSource('vector_source_id'); - * * // Update vector tile source to a new URL endpoint - * vectorTileSource.setUrl("mapbox://mapbox.mapbox-streets-v8"); + * map.getSource('source-id').setUrl("mapbox://mapbox.mapbox-streets-v8"); */ setUrl(url: string): this { this.url = url; diff --git a/test/unit/source/raster_tile_source.test.js b/test/unit/source/raster_tile_source.test.js index 66074ec827d..896c40ca363 100644 --- a/test/unit/source/raster_tile_source.test.js +++ b/test/unit/source/raster_tile_source.test.js @@ -4,13 +4,18 @@ import window from '../../../src/util/window.js'; import config from '../../../src/util/config.js'; import {OverscaledTileID} from '../../../src/source/tile_id.js'; import {RequestManager} from '../../../src/util/mapbox.js'; +import sourceFixture from '../../fixtures/source.json'; function createSource(options, transformCallback) { const source = new RasterTileSource('id', options, {send() {}}, options.eventedParent); + source.onAdd({ transform: {angle: 0, pitch: 0, showCollisionBoxes: false}, _getMapId: () => 1, - _requestManager: new RequestManager(transformCallback) + _requestManager: new RequestManager(transformCallback), + style: { + _clearSource: () => {}, + } }); source.on('error', (e) => { @@ -188,5 +193,72 @@ test('RasterTileSource', (t) => { t.end(); }); + t.test('supports property updates', (t) => { + window.server.configure({respondImmediately: true}); + window.server.respondWith('/source.json', JSON.stringify(sourceFixture)); + const source = createSource({url: '/source.json'}); + + const loadSpy = t.spy(source, 'load'); + const clearSourceSpy = t.spy(source.map.style, '_clearSource'); + + const responseSpy = t.spy((xhr) => + xhr.respond(200, {"Content-Type": "application/json"}, JSON.stringify({...sourceFixture, maxzoom: 22}))); + + window.server.respondWith('/source.json', responseSpy); + + source.attribution = 'OpenStreetMap'; + source.reload(); + + t.ok(loadSpy.calledOnce); + t.ok(responseSpy.calledOnce); + t.ok(clearSourceSpy.calledOnce); + t.ok(clearSourceSpy.calledAfter(responseSpy), 'Tiles should be cleared after TileJSON is loaded'); + + t.end(); + }); + + t.test('supports url property updates', (t) => { + window.server.respondWith('/source.json', JSON.stringify(sourceFixture)); + window.server.respondWith('/new-source.json', JSON.stringify({...sourceFixture, minzoom: 0, maxzoom: 22})); + window.server.configure({autoRespond: true, autoRespondAfter: 0}); + + const source = createSource({url: '/source.json'}); + source.setUrl('/new-source.json'); + + source.on('data', (e) => { + if (e.sourceDataType === 'metadata') { + t.deepEqual(source.minzoom, 0); + t.deepEqual(source.maxzoom, 22); + t.deepEqual(source.attribution, 'Mapbox'); + t.deepEqual(source.serialize(), {type: 'raster', url: '/new-source.json'}); + t.end(); + } + }); + }); + + t.test('supports tiles property updates', (t) => { + const source = createSource({ + minzoom: 1, + maxzoom: 10, + attribution: 'Mapbox', + tiles: ['http://example.com/v1/{z}/{x}/{y}.png'] + }); + + source.setTiles(['http://example.com/v2/{z}/{x}/{y}.png']); + + source.on('data', (e) => { + if (e.sourceDataType === 'metadata') { + t.deepEqual(source.serialize(), { + type: 'raster', + minzoom: 1, + maxzoom: 10, + attribution: 'Mapbox', + tiles: ['http://example.com/v2/{z}/{x}/{y}.png'] + }); + t.end(); + } + }); + }); + t.end(); }); diff --git a/test/unit/source/vector_tile_source.test.js b/test/unit/source/vector_tile_source.test.js index b307dd4ae24..c663a60941b 100644 --- a/test/unit/source/vector_tile_source.test.js +++ b/test/unit/source/vector_tile_source.test.js @@ -21,15 +21,13 @@ const mockDispatcher = wrapDispatcher({ function createSource(options, {transformCallback, customAccessToken} = {}) { const source = new VectorTileSource('id', options, mockDispatcher, options.eventedParent); - const sourceCache = {clearTiles: () => {}}; source.onAdd({ transform: {showCollisionBoxes: false}, _getMapId: () => 1, _requestManager: new RequestManager(transformCallback, customAccessToken), - _sourceCaches: [sourceCache], style: { - _getSourceCaches: () => [sourceCache] + _clearSource: () => {}, } }); @@ -395,21 +393,20 @@ test('VectorTileSource', (t) => { const source = createSource({url: '/source.json'}); const loadSpy = t.spy(source, 'load'); - const clearTilesSpy = t.spy(source.map._sourceCaches[0], 'clearTiles'); + const clearSourceSpy = t.spy(source.map.style, '_clearSource'); const responseSpy = t.spy((xhr) => xhr.respond(200, {"Content-Type": "application/json"}, JSON.stringify({...sourceFixture, maxzoom: 22}))); window.server.respondWith('/source.json', responseSpy); - source.setSourceProperty(() => { - source.attribution = 'OpenStreetMap'; - }); + source.attribution = 'OpenStreetMap'; + source.reload(); t.ok(loadSpy.calledOnce); t.ok(responseSpy.calledOnce); - t.ok(clearTilesSpy.calledOnce); - t.ok(clearTilesSpy.calledAfter(responseSpy), 'Tiles should be cleared after TileJSON is loaded'); + t.ok(clearSourceSpy.calledOnce); + t.ok(clearSourceSpy.calledAfter(responseSpy), 'Tiles should be cleared after TileJSON is loaded'); t.end(); });