Skip to content

Commit a733662

Browse files
authored
Add refreshTiles() to map public API (#5806)
* add refreshTile API * add refreshTiles() to refresh many tiles at once. * add source cache unit tests * add map unit test and docs. * update Changelog * remove unneeded refreshTile() * Split test * it tileIds is not specified, perform global reload on the source. * add top-level describe to test file * reduce indentation, add break if matching tile is found * replace for loop with Array::some()
1 parent c3deeda commit a733662

File tree

5 files changed

+176
-2
lines changed

5 files changed

+176
-2
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
### ✨ Features and improvements
44
- Add additional hillshade methods ([#5768](https://github.com/maplibre/maplibre-gl-js/pull/5768))
5+
- Add `refreshTiles()` to the map public API ([#5806](https://github.com/maplibre/maplibre-gl-js/pull/5806))
56
- _...Add new stuff here..._
67

78
### 🐞 Bug fixes

src/source/source_cache.test.ts

+89-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {SourceCache} from './source_cache';
33
import {type Map} from '../ui/map';
44
import {type Source, addSourceType} from './source';
55
import {Tile} from './tile';
6-
import {OverscaledTileID} from './tile_id';
6+
import {CanonicalTileID, OverscaledTileID} from './tile_id';
77
import {LngLat} from '../geo/lng_lat';
88
import Point from '@mapbox/point-geometry';
99
import {Event, ErrorEvent, Evented} from '../util/evented';
@@ -2142,4 +2142,92 @@ describe('SourceCache#usedForTerrain', () => {
21422142
['3s44', '3r44', '3c44', '3b44']
21432143
);
21442144
});
2145+
2146+
});
2147+
2148+
describe('SourceCache::refreshTiles', () => {
2149+
test('calls reloadTile when tile exists', async () => {
2150+
const coord = new OverscaledTileID(1, 0, 1, 0, 1);
2151+
const sourceCache = createSourceCache();
2152+
2153+
const spy = vi.fn();
2154+
sourceCache._reloadTile = spy;
2155+
sourceCache._source.loadTile = async (tile) => {
2156+
tile.state = 'loaded';
2157+
};
2158+
2159+
sourceCache._addTile(coord);
2160+
sourceCache.refreshTiles([new CanonicalTileID(1, 0, 1)]);
2161+
expect(spy).toHaveBeenCalledOnce();
2162+
expect(spy.mock.calls[0][1]).toBe('expired');
2163+
});
2164+
2165+
test('does not call reloadTile when tile does not exist', async () => {
2166+
const coord = new OverscaledTileID(1, 0, 1, 1, 1);
2167+
const sourceCache = createSourceCache();
2168+
2169+
const spy = vi.fn();
2170+
sourceCache._reloadTile = spy;
2171+
sourceCache._source.loadTile = async (tile) => {
2172+
tile.state = 'loaded';
2173+
};
2174+
2175+
sourceCache._addTile(coord);
2176+
sourceCache.refreshTiles([new CanonicalTileID(1, 0, 1)]);
2177+
expect(spy).toHaveBeenCalledTimes(0);
2178+
});
2179+
2180+
test('calls reloadTile when wrapped tile exists', async () => {
2181+
const coord = new OverscaledTileID(1, 1, 1, 0, 1);
2182+
const sourceCache = createSourceCache();
2183+
2184+
const spy = vi.fn();
2185+
sourceCache._reloadTile = spy;
2186+
sourceCache._source.loadTile = async (tile) => {
2187+
tile.state = 'loaded';
2188+
};
2189+
2190+
sourceCache._addTile(coord);
2191+
sourceCache.refreshTiles([new CanonicalTileID(1, 0, 1)]);
2192+
expect(spy).toHaveBeenCalledOnce();
2193+
expect(spy.mock.calls[0][1]).toBe('expired');
2194+
});
2195+
2196+
test('calls reloadTile when overscaled tile exists', async () => {
2197+
const coord = new OverscaledTileID(2, 0, 1, 0, 1);
2198+
const sourceCache = createSourceCache();
2199+
2200+
const spy = vi.fn();
2201+
sourceCache._reloadTile = spy;
2202+
sourceCache._source.loadTile = async (tile) => {
2203+
tile.state = 'loaded';
2204+
};
2205+
2206+
sourceCache._addTile(coord);
2207+
sourceCache.refreshTiles([new CanonicalTileID(1, 0, 1)]);
2208+
expect(spy).toHaveBeenCalledOnce();
2209+
expect(spy.mock.calls[0][1]).toBe('expired');
2210+
});
2211+
2212+
test('calls reloadTile for standard, wrapped, and overscaled tiles', async () => {
2213+
const sourceCache = createSourceCache();
2214+
2215+
const spy = vi.fn();
2216+
sourceCache._reloadTile = spy;
2217+
sourceCache._source.loadTile = async (tile) => {
2218+
tile.state = 'loaded';
2219+
};
2220+
2221+
sourceCache._addTile(new OverscaledTileID(1, 0, 1, 0, 1));
2222+
sourceCache._addTile(new OverscaledTileID(1, 1, 1, 0, 1));
2223+
sourceCache._addTile(new OverscaledTileID(2, 0, 1, 0, 1));
2224+
sourceCache._addTile(new OverscaledTileID(2, 1, 1, 0, 1));
2225+
sourceCache.refreshTiles([new CanonicalTileID(1, 0, 1)]);
2226+
expect(spy).toHaveBeenCalledTimes(4);
2227+
expect(spy.mock.calls[0][1]).toBe('expired');
2228+
expect(spy.mock.calls[1][1]).toBe('expired');
2229+
expect(spy.mock.calls[2][1]).toBe('expired');
2230+
expect(spy.mock.calls[3][1]).toBe('expired');
2231+
});
2232+
21452233
});

src/source/source_cache.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import type {Style} from '../style/style';
1919
import type {Dispatcher} from '../util/dispatcher';
2020
import type {IReadonlyTransform, ITransform} from '../geo/transform_interface';
2121
import type {TileState} from './tile';
22-
import type {SourceSpecification} from '@maplibre/maplibre-gl-style-spec';
22+
import type {ICanonicalTileID, SourceSpecification} from '@maplibre/maplibre-gl-style-spec';
2323
import type {MapSourceDataEvent} from '../ui/events';
2424
import type {Terrain} from '../render/terrain';
2525
import type {CanvasSourceSpecification} from './canvas_source';
@@ -894,6 +894,20 @@ export class SourceCache extends Evented {
894894
}
895895
}
896896

897+
/**
898+
* Reload any currently renderable tiles that are match one of the incoming `tileId` x/y/z
899+
*/
900+
refreshTiles(tileIds: Array<ICanonicalTileID>) {
901+
for (const id in this._tiles) {
902+
if (!this._isIdRenderable(id)) {
903+
continue;
904+
}
905+
if (tileIds.some(tid => tid.equals(this._tiles[id].tileID.canonical))) {
906+
this._reloadTile(id, 'expired');
907+
}
908+
}
909+
}
910+
897911
/**
898912
* Remove a tile, given its id, from the pyramid
899913
*/

src/ui/map.ts

+23
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import {MercatorCameraHelper} from '../geo/projection/mercator_camera_helper';
6767
import {isAbortError} from '../util/abort_error';
6868
import {isFramebufferNotCompleteError} from '../util/framebuffer_error';
6969
import {createCalculateTileZoomFunction} from '../geo/projection/covering_tiles';
70+
import {CanonicalTileID} from '../source/tile_id';
7071

7172
const version = packageJSON.version;
7273

@@ -2213,6 +2214,28 @@ export class Map extends Camera {
22132214
return this;
22142215
}
22152216

2217+
/**
2218+
* Triggers a reload of the selected tiles
2219+
*
2220+
* @param sourceId - The ID of the source
2221+
* @param tileIds - An array of tile IDs to be reloaded. If not defined, all tiles will be reloaded.
2222+
* @example
2223+
* ```ts
2224+
* map.refreshTiles('satellite', [{x:1024, y: 1023, z: 11}, {x:1023, y: 1023, z: 11}]);
2225+
* ```
2226+
*/
2227+
refreshTiles(sourceId: string, tileIds?: Array<{x: number; y: number; z: number}>) {
2228+
const sourceCache = this.style.sourceCaches[sourceId];
2229+
if(!sourceCache) {
2230+
throw new Error(`There is no source cache with ID "${sourceId}", cannot refresh tile`);
2231+
}
2232+
if (tileIds === undefined) {
2233+
sourceCache.reload();
2234+
} else {
2235+
sourceCache.refreshTiles(tileIds.map((tileId) => {return new CanonicalTileID(tileId.z, tileId.x, tileId.y);}));
2236+
}
2237+
}
2238+
22162239
/**
22172240
* Add an image to the style. This image can be displayed on the map like any other icon in the style's
22182241
* sprite using the image's ID with
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import {beforeEach, test, expect, vi, describe} from 'vitest';
2+
import {createMap, beforeMapTest} from '../../util/test/util';
3+
import {CanonicalTileID} from '../../source/tile_id';
4+
5+
beforeEach(() => {
6+
beforeMapTest();
7+
global.fetch = null;
8+
});
9+
10+
describe('Map::refreshTiles', () => {
11+
test('refreshTiles, non-existent source', async () => {
12+
const map = createMap({interactive: false});
13+
await map.once('style.load');
14+
15+
map.addSource('source-id1', {type: 'raster', url: ''});
16+
const spy = vi.fn();
17+
map.style.sourceCaches['source-id1'].refreshTiles = spy;
18+
19+
expect(() => {map.refreshTiles('source-id2', [{x: 1024, y: 1023, z: 11}]);})
20+
.toThrow('There is no source cache with ID "source-id2", cannot refresh tile');
21+
expect(spy).toHaveBeenCalledTimes(0);
22+
});
23+
24+
test('refreshTiles, existing source', async () => {
25+
const map = createMap({interactive: false});
26+
await map.once('style.load');
27+
28+
map.addSource('source-id1', {type: 'raster', url: ''});
29+
const spy = vi.fn();
30+
map.style.sourceCaches['source-id1'].refreshTiles = spy;
31+
32+
map.refreshTiles('source-id1', [{x: 1024, y: 1023, z: 11}]);
33+
expect(spy).toHaveBeenCalledOnce();
34+
expect(spy.mock.calls[0][0]).toEqual([new CanonicalTileID(11, 1024, 1023)]);
35+
});
36+
37+
test('refreshTiles, existing source, undefined tileIds', async () => {
38+
const map = createMap({interactive: false});
39+
await map.once('style.load');
40+
41+
map.addSource('source-id1', {type: 'raster', url: ''});
42+
const spy = vi.fn();
43+
map.style.sourceCaches['source-id1'].reload = spy;
44+
45+
map.refreshTiles('source-id1');
46+
expect(spy).toHaveBeenCalledOnce();
47+
});
48+
});

0 commit comments

Comments
 (0)