Skip to content

Commit

Permalink
Add dynamic raster tiles support (#12352)
Browse files Browse the repository at this point in the history
* Add RasterTileSource setTiles and setUrl

* Drop `setSourceProperty()`, expose `reload()`
  • Loading branch information
stepankuzmin authored Feb 2, 2023
1 parent 3b88dc4 commit 7585d49
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 37 deletions.
91 changes: 86 additions & 5 deletions src/source/raster_tile_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -63,7 +84,7 @@ class RasterTileSource extends Evented implements Source {
extend(this, pick(options, ['url', 'scheme', 'tileSize']));
}

load() {
load(callback?: Callback<void>) {
this._loaded = false;
this.fire(new Event('dataloading', {dataType: 'source'}));
this._tileJSONRequest = loadTileJSON(this._options, this.map._requestManager, null, null, (err, tileJSON) => {
Expand All @@ -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);
});
}

Expand All @@ -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<string>): 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://<Tileset ID>`.
* @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 {
Expand Down Expand Up @@ -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;
33 changes: 11 additions & 22 deletions src/source/vector_tile_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

/**
Expand All @@ -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<string>): this {
this._options.tiles = tiles;
Expand All @@ -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://<Tileset ID>`.
* @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;
Expand Down
74 changes: 73 additions & 1 deletion test/unit/source/raster_tile_source.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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();
});
15 changes: 6 additions & 9 deletions test/unit/source/vector_tile_source.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => {},
}
});

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

0 comments on commit 7585d49

Please sign in to comment.