Skip to content

Add refreshTiles() to map public API #5806

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Apr 26, 2025
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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

### 🐞 Bug fixes
Expand Down
90 changes: 89 additions & 1 deletion src/source/source_cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {SourceCache} from './source_cache';
import {type Map} from '../ui/map';
import {type Source, addSourceType} from './source';
import {Tile} from './tile';
import {OverscaledTileID} from './tile_id';
import {CanonicalTileID, OverscaledTileID} from './tile_id';
import {LngLat} from '../geo/lng_lat';
import Point from '@mapbox/point-geometry';
import {Event, ErrorEvent, Evented} from '../util/evented';
Expand Down Expand Up @@ -2142,4 +2142,92 @@ describe('SourceCache#usedForTerrain', () => {
['3s44', '3r44', '3c44', '3b44']
);
});

});

describe('SourceCache::refreshTiles', () => {
test('calls reloadTile when tile exists', async () => {
const coord = new OverscaledTileID(1, 0, 1, 0, 1);
const sourceCache = createSourceCache();

const spy = vi.fn();
sourceCache._reloadTile = spy;
sourceCache._source.loadTile = async (tile) => {
tile.state = 'loaded';
};

sourceCache._addTile(coord);
sourceCache.refreshTiles([new CanonicalTileID(1, 0, 1)]);
expect(spy).toHaveBeenCalledOnce();
expect(spy.mock.calls[0][1]).toBe('expired');
});

test('does not call reloadTile when tile does not exist', async () => {
const coord = new OverscaledTileID(1, 0, 1, 1, 1);
const sourceCache = createSourceCache();

const spy = vi.fn();
sourceCache._reloadTile = spy;
sourceCache._source.loadTile = async (tile) => {
tile.state = 'loaded';
};

sourceCache._addTile(coord);
sourceCache.refreshTiles([new CanonicalTileID(1, 0, 1)]);
expect(spy).toHaveBeenCalledTimes(0);
});

test('calls reloadTile when wrapped tile exists', async () => {
const coord = new OverscaledTileID(1, 1, 1, 0, 1);
const sourceCache = createSourceCache();

const spy = vi.fn();
sourceCache._reloadTile = spy;
sourceCache._source.loadTile = async (tile) => {
tile.state = 'loaded';
};

sourceCache._addTile(coord);
sourceCache.refreshTiles([new CanonicalTileID(1, 0, 1)]);
expect(spy).toHaveBeenCalledOnce();
expect(spy.mock.calls[0][1]).toBe('expired');
});

test('calls reloadTile when overscaled tile exists', async () => {
const coord = new OverscaledTileID(2, 0, 1, 0, 1);
const sourceCache = createSourceCache();

const spy = vi.fn();
sourceCache._reloadTile = spy;
sourceCache._source.loadTile = async (tile) => {
tile.state = 'loaded';
};

sourceCache._addTile(coord);
sourceCache.refreshTiles([new CanonicalTileID(1, 0, 1)]);
expect(spy).toHaveBeenCalledOnce();
expect(spy.mock.calls[0][1]).toBe('expired');
});

test('calls reloadTile for standard, wrapped, and overscaled tiles', async () => {
const sourceCache = createSourceCache();

const spy = vi.fn();
sourceCache._reloadTile = spy;
sourceCache._source.loadTile = async (tile) => {
tile.state = 'loaded';
};

sourceCache._addTile(new OverscaledTileID(1, 0, 1, 0, 1));
sourceCache._addTile(new OverscaledTileID(1, 1, 1, 0, 1));
sourceCache._addTile(new OverscaledTileID(2, 0, 1, 0, 1));
sourceCache._addTile(new OverscaledTileID(2, 1, 1, 0, 1));
sourceCache.refreshTiles([new CanonicalTileID(1, 0, 1)]);
expect(spy).toHaveBeenCalledTimes(4);
expect(spy.mock.calls[0][1]).toBe('expired');
expect(spy.mock.calls[1][1]).toBe('expired');
expect(spy.mock.calls[2][1]).toBe('expired');
expect(spy.mock.calls[3][1]).toBe('expired');
});

});
16 changes: 15 additions & 1 deletion src/source/source_cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type {Style} from '../style/style';
import type {Dispatcher} from '../util/dispatcher';
import type {IReadonlyTransform, ITransform} from '../geo/transform_interface';
import type {TileState} from './tile';
import type {SourceSpecification} from '@maplibre/maplibre-gl-style-spec';
import type {ICanonicalTileID, SourceSpecification} from '@maplibre/maplibre-gl-style-spec';
import type {MapSourceDataEvent} from '../ui/events';
import type {Terrain} from '../render/terrain';
import type {CanvasSourceSpecification} from './canvas_source';
Expand Down Expand Up @@ -894,6 +894,20 @@ export class SourceCache extends Evented {
}
}

/**
* Reload any currently renderable tiles that are match one of the incoming `tileId` x/y/z
*/
refreshTiles(tileIds: Array<ICanonicalTileID>) {
for (const id in this._tiles) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to do some .filter(...) to the array to remove some indentation in this method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed some indentation without using filter.

if (!this._isIdRenderable(id)) {
continue;
}
if (tileIds.some(tid => tid.equals(this._tiles[id].tileID.canonical))) {
this._reloadTile(id, 'expired');
}
}
}

/**
* Remove a tile, given its id, from the pyramid
*/
Expand Down
23 changes: 23 additions & 0 deletions src/ui/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import {MercatorCameraHelper} from '../geo/projection/mercator_camera_helper';
import {isAbortError} from '../util/abort_error';
import {isFramebufferNotCompleteError} from '../util/framebuffer_error';
import {createCalculateTileZoomFunction} from '../geo/projection/covering_tiles';
import {CanonicalTileID} from '../source/tile_id';

const version = packageJSON.version;

Expand Down Expand Up @@ -2213,6 +2214,28 @@ export class Map extends Camera {
return this;
}

/**
* Triggers a reload of the selected tiles
*
* @param sourceId - The ID of the source
* @param tileIds - An array of tile IDs to be reloaded. If not defined, all tiles will be reloaded.
* @example
* ```ts
* map.refreshTiles('satellite', [{x:1024, y: 1023, z: 11}, {x:1023, y: 1023, z: 11}]);
* ```
*/
refreshTiles(sourceId: string, tileIds?: Array<{x: number; y: number; z: number}>) {
const sourceCache = this.style.sourceCaches[sourceId];
if(!sourceCache) {
throw new Error(`There is no source cache with ID "${sourceId}", cannot refresh tile`);
}
if (tileIds === undefined) {
sourceCache.reload();
} else {
sourceCache.refreshTiles(tileIds.map((tileId) => {return new CanonicalTileID(tileId.z, tileId.x, tileId.y);}));
}
}

/**
* Add an image to the style. This image can be displayed on the map like any other icon in the style's
* sprite using the image's ID with
Expand Down
48 changes: 48 additions & 0 deletions src/ui/map_tests/map_refresh_tiles.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {beforeEach, test, expect, vi, describe} from 'vitest';
import {createMap, beforeMapTest} from '../../util/test/util';
import {CanonicalTileID} from '../../source/tile_id';

beforeEach(() => {
beforeMapTest();
global.fetch = null;
});

describe('Map::refreshTiles', () => {
test('refreshTiles, non-existent source', async () => {
const map = createMap({interactive: false});
await map.once('style.load');

map.addSource('source-id1', {type: 'raster', url: ''});
const spy = vi.fn();
map.style.sourceCaches['source-id1'].refreshTiles = spy;

expect(() => {map.refreshTiles('source-id2', [{x: 1024, y: 1023, z: 11}]);})
.toThrow('There is no source cache with ID "source-id2", cannot refresh tile');
expect(spy).toHaveBeenCalledTimes(0);
});

test('refreshTiles, existing source', async () => {
const map = createMap({interactive: false});
await map.once('style.load');

map.addSource('source-id1', {type: 'raster', url: ''});
const spy = vi.fn();
map.style.sourceCaches['source-id1'].refreshTiles = spy;

map.refreshTiles('source-id1', [{x: 1024, y: 1023, z: 11}]);
expect(spy).toHaveBeenCalledOnce();
expect(spy.mock.calls[0][0]).toEqual([new CanonicalTileID(11, 1024, 1023)]);
});

test('refreshTiles, existing source, undefined tileIds', async () => {
const map = createMap({interactive: false});
await map.once('style.load');

map.addSource('source-id1', {type: 'raster', url: ''});
const spy = vi.fn();
map.style.sourceCaches['source-id1'].reload = spy;

map.refreshTiles('source-id1');
expect(spy).toHaveBeenCalledOnce();
});
});