Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
- Add Etag unmodified support to optimize vector tile reloading ([#7074](https://github.com/maplibre/maplibre-gl-js/pull/7074)) (by [@rivkamatan](https://github.com/rivkamatan and [@wayofthefuture](https://github.com/wayofthefuture))
- Add `boxZoom.boxZoomEnd` option to customize the action after Shift-drag box selection ([#6397](https://github.com/maplibre/maplibre-gl-js/issues/6397))
- Add updateable support for GeoJSON-VT ([#7172](https://github.com/maplibre/maplibre-gl-js/issues/7172)) (by [@wayofthefuture](https://github.com/wayofthefuture) and [HarelM](https://github.com/HarelM))
- Make `setTransformRequest` accept an async function in addition to a sync function. ([#7184](https://github.com/maplibre/maplibre-gl-js/issues/7184)) (by [@kikuomax
](https://github.com/kikuomax))
- _...Add new stuff here..._

### 🐞 Bug fixes
Expand Down
120 changes: 89 additions & 31 deletions src/source/geojson_source.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ describe('GeoJSONSource.setData', () => {
await expect(promise).resolves.toBeDefined();
});

test('respects collectResourceTiming parameter on source', () => {
test('respects collectResourceTiming parameter on source', async () => {
const source = createSource({collectResourceTiming: true});
source.map = {
_requestManager: {
Expand All @@ -149,16 +149,40 @@ describe('GeoJSONSource.setData', () => {
} as any;
const spy = vi.fn();
source.actor.sendAsync = (message: ActorMessage<MessageType>) => {
return new Promise((resolve) => {
return new Promise((resolve, reject) => {
if (message.type === MessageType.loadData) {
expect((message.data as any).request.collectResourceTiming).toBeTruthy();
setTimeout(() => resolve({} as any), 0);
spy();
spy(message);
} else {
reject(new Error(`MessageType.loadData is expected but got ${message.type}`));
}
});
};
source.setData('http://localhost/nonexistent');
expect(spy).toHaveBeenCalled();
await sleep(0);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0].data.request.collectResourceTiming).toBeTruthy();
});

test('respects collectResourceTiming parameter on source (async transformRequest)', async () => {
const source = createSource({collectResourceTiming: true});
source.map = {
_requestManager: {
transformRequest: async (url) => { return {url}; }
} as any as RequestManager
} as any;
const spy = vi.fn();
source.actor.sendAsync = (message: ActorMessage<MessageType>) => {
return new Promise((resolve) => {
if (message.type === MessageType.loadData) {
spy(message);
resolve({} as any);
}
});
};
await source.setData('http://localhost/nonexistent', true);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0].data.request.collectResourceTiming).toBeTruthy();
});

test('only marks source as loaded when there are no pending loads', async () => {
Expand Down Expand Up @@ -221,14 +245,14 @@ describe('GeoJSONSource.onRemove', () => {
const spy = vi.fn();
const source = new GeoJSONSource('id', {data: {}} as GeoJSONSourceOptions, wrapDispatcher({
sendAsync(message: ActorMessage<MessageType>) {
expect(message.type).toBe(MessageType.removeSource);
expect(message.data).toEqual({type: 'geojson', source: 'id'});
spy();
spy(message);
return Promise.resolve({});
}
}), undefined);
source.onRemove();
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0].type).toBe(MessageType.removeSource);
expect(spy.mock.calls[0][0].data).toEqual({type: 'geojson', source: 'id'});
});
});

Expand All @@ -240,34 +264,26 @@ describe('GeoJSONSource.update', () => {
transform.setZoom(15);
transform.setLocationAtPoint(lngLat, point);

test('sends initial loadData request to dispatcher', () => {
test('sends initial loadData request to dispatcher', async () => {
const spy = vi.fn();
const mockDispatcher = wrapDispatcher({
sendAsync(message: ActorMessage<MessageType>) {
expect(message.type).toBe(MessageType.loadData);
spy();
spy(message);
return Promise.resolve({});
}
});

new GeoJSONSource('id', {data: {}} as GeoJSONSourceOptions, mockDispatcher, undefined).load();
expect(spy).toHaveBeenCalled();
await sleep(0);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0].type).toBe(MessageType.loadData);
});

test('forwards geojson-vt options with worker request', () => {
test('forwards geojson-vt options with worker request', async () => {
const spy = vi.fn();
const mockDispatcher = wrapDispatcher({
sendAsync(message: ActorMessage<any>) {
expect(message.type).toBe(MessageType.loadData);
expect(message.data.geojsonVtOptions).toEqual({
extent: EXTENT,
maxZoom: 10,
tolerance: 4,
buffer: 256,
lineMetrics: false,
generateId: true
});
spy();
spy(message);
return Promise.resolve({});
}
});
Expand All @@ -279,10 +295,20 @@ describe('GeoJSONSource.update', () => {
buffer: 16,
generateId: true
} as GeoJSONSourceOptions, mockDispatcher, undefined).load();
expect(spy).toHaveBeenCalled();
await sleep(0);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0].type).toBe(MessageType.loadData);
expect(spy.mock.calls[0][0].data.geojsonVtOptions).toEqual({
extent: EXTENT,
maxZoom: 10,
tolerance: 4,
buffer: 256,
lineMetrics: false,
generateId: true
});
});

test('forwards Supercluster options with worker request', () => {
test('forwards Supercluster options with worker request', async () => {
const spy = vi.fn();
const mockDispatcher = wrapDispatcher({
sendAsync(message) {
Expand All @@ -300,7 +326,8 @@ describe('GeoJSONSource.update', () => {
generateId: true
} as GeoJSONSourceOptions, mockDispatcher, undefined);
source.load();
expect(spy).toHaveBeenCalled();
await sleep(0);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0].type).toBe(MessageType.loadData);
expect(spy.mock.calls[0][0].data.superclusterOptions).toEqual({
maxZoom: 12,
Expand Down Expand Up @@ -338,6 +365,7 @@ describe('GeoJSONSource.update', () => {
spy.mockClear();

source.setClusterOptions({cluster: true, clusterRadius: 80, clusterMaxZoom: 16});
await sleep(0);

expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0].type).toBe(MessageType.loadData);
Expand Down Expand Up @@ -379,9 +407,9 @@ describe('GeoJSONSource.update', () => {
source.setData(sourceData2);
source.setClusterOptions({cluster: true, clusterRadius: 80, clusterMaxZoom: 16});

await waitForEvent(source, 'data', (e: MapSourceDataEvent) => e.sourceDataType === 'metadata');
await sleep(0);

expect(spy).toHaveBeenCalledTimes(2);
expect(spy).toHaveBeenCalledTimes(3);
expect(spy.mock.calls[0][0].type).toBe(MessageType.loadData);
expect(spy.mock.calls[0][0].data.cluster).toBe(false);
expect(spy.mock.calls[0][0].data.data).toBe(sourceData1);
Expand All @@ -391,6 +419,11 @@ describe('GeoJSONSource.update', () => {
expect(spy.mock.calls[1][0].data.superclusterOptions.maxZoom).toBe(16);
expect(spy.mock.calls[1][0].data.data).toBe(sourceData2);
expect(spy.mock.calls[1][0].data.dataDiff).toBeUndefined();
expect(spy.mock.calls[2][0].data.cluster).toBe(true);
expect(spy.mock.calls[2][0].data.superclusterOptions.radius).toBe(80 * EXTENT / source.tileSize);
expect(spy.mock.calls[2][0].data.superclusterOptions.maxZoom).toBe(16);
expect(spy.mock.calls[2][0].data.data).toBeUndefined();
expect(spy.mock.calls[2][0].data.dataDiff).toBeUndefined();
});

test('modifying cluster properties after sending a diff', async () => {
Expand Down Expand Up @@ -436,7 +469,7 @@ describe('GeoJSONSource.update', () => {
source.updateData(diff);
source.setClusterOptions({cluster: true, clusterRadius: 80, clusterMaxZoom: 16});

await waitForEvent(source, 'data', (e: MapSourceDataEvent) => e.sourceDataType === 'metadata');
await sleep(0);

expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[0][0].data.cluster).toBe(false);
Expand All @@ -446,7 +479,7 @@ describe('GeoJSONSource.update', () => {
expect(spy.mock.calls[1][0].data.dataDiff).not.toBeDefined();
});

test('forwards Supercluster options with worker request, ignore max zoom of source', () => {
test('forwards Supercluster options with worker request, ignore max zoom of source', async () => {
const spy = vi.fn();
vi.spyOn(console, 'warn').mockImplementation(() => {});
const mockDispatcher = wrapDispatcher({
Expand All @@ -466,6 +499,7 @@ describe('GeoJSONSource.update', () => {
generateId: true
} as GeoJSONSourceOptions, mockDispatcher, undefined);
source.load();
await sleep(0);
expect(spy).toHaveBeenCalled();
expect(spy.mock.calls[0][0].type).toBe(MessageType.loadData);
expect(spy.mock.calls[0][0].data.superclusterOptions).toEqual({
Expand Down Expand Up @@ -504,6 +538,30 @@ describe('GeoJSONSource.update', () => {
expect(source.serialize()).toStrictEqual({type: 'geojson', data: hawkHill});
});

test('can asynchronously transform request', async () => {
const spy = vi.fn();
const source = new GeoJSONSource('id', {data: 'https://example.com/data.geojson'} as GeoJSONSourceOptions, wrapDispatcher({
sendAsync(message) {
spy(message);
return Promise.resolve({});
}
}), undefined);
source.map = {
_requestManager: {
transformRequest: async (url) => ({
url,
headers: {Authorization: 'Bearer token'}
})
}
} as any;
await source.load();
expect(spy).toHaveBeenCalled();
expect(spy.mock.calls[0][0].data.request).toEqual({
url: 'https://example.com/data.geojson',
headers: {Authorization: 'Bearer token'}
});
});

test('fires event when metadata loads', async () => {
const mockDispatcher = wrapDispatcher({
sendAsync(_message: ActorMessage<MessageType>) {
Expand Down
8 changes: 5 additions & 3 deletions src/source/geojson_source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ export class GeoJSONSource extends Evented implements Source {
}

const {data, diff, updateCluster} = this._pendingWorkerUpdate;
// delay awaiting params until _isUpdatingWorker is set, otherwise, a race condition could happen
const params = this._getLoadGeoJSONParameters(data, diff, updateCluster);

if (data !== undefined) {
Expand All @@ -418,12 +419,12 @@ export class GeoJSONSource extends Evented implements Source {
/**
* Create the parameters object that will be sent to the worker and used to load GeoJSON.
*/
private _getLoadGeoJSONParameters(data: string | GeoJSON.GeoJSON<GeoJSON.Geometry>, diff: GeoJSONSourceDiff, updateCluster: boolean): LoadGeoJSONParameters | undefined {
private async _getLoadGeoJSONParameters(data: string | GeoJSON.GeoJSON<GeoJSON.Geometry>, diff: GeoJSONSourceDiff, updateCluster: boolean): Promise<LoadGeoJSONParameters | undefined> {
const params: LoadGeoJSONParameters = extend({type: this.type}, this.workerOptions);

// Data comes from a remote url
if (typeof data === 'string') {
params.request = this.map._requestManager.transformRequest(browser.resolveURL(data as string), ResourceType.Source);
params.request = await this.map._requestManager.transformRequest(browser.resolveURL(data as string), ResourceType.Source);
params.request.collectResourceTiming = this._collectResourceTiming;
return params;
}
Expand All @@ -450,11 +451,12 @@ export class GeoJSONSource extends Evented implements Source {
/**
* Send the worker update data from the main thread to the worker
*/
private async _dispatchWorkerUpdate(options: LoadGeoJSONParameters) {
private async _dispatchWorkerUpdate(optionsPromise: Promise<LoadGeoJSONParameters>) {
this._isUpdatingWorker = true;
this.fire(new Event('dataloading', {dataType: 'source'}));

try {
const options = await optionsPromise;
const result = await this.actor.sendAsync({type: MessageType.loadData, data: options});
this._isUpdatingWorker = false;

Expand Down
28 changes: 27 additions & 1 deletion src/source/image_source.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ describe('ImageSource', () => {
expect(e.dataType).toBe('source');
});
source.onAdd(new StubMap() as any);
await sleep(0);
server.respond();
await sleep(0);
expect(source.image).toBeTruthy();
Expand All @@ -83,6 +84,24 @@ describe('ImageSource', () => {
expect(spy.mock.calls[0][1]).toBe('Image');
});

test('can asynchronously transform request', async () => {
const source = createSource({url: '/image.png'});
const map = new StubMap() as any;
map._requestManager = {
transformRequest: async (url) => ({
url,
headers: {Authorization: 'Bearer token'}
})
};
const promise = source.once('data');
source.onAdd(map);
await sleep(0);
server.respond();
await promise;
expect(server.requests[0].url).toBe('/image.png');
expect(server.requests[0].requestHeaders['Authorization']).toBe('Bearer token');
});

test('updates url from updateImage', () => {
const source = createSource({url: '/image.png'});
const map = new StubMap() as any;
Expand Down Expand Up @@ -122,6 +141,7 @@ describe('ImageSource', () => {
url: '/image2.png',
coordinates: [[0, 0], [-1, 0], [-1, -1], [0, -1]]
});
await sleep(0);
server.respond();
await sleep(0);
const afterSerialized = source.serialize();
Expand All @@ -132,6 +152,7 @@ describe('ImageSource', () => {
const source = createSource({url: '/image.png'});
const promise = waitForEvent(source, 'data', (e) => e.dataType === 'source' && e.sourceDataType === 'content');
source.onAdd(new StubMap() as any);
await sleep(0);
server.respond();
await promise;
expect(typeof source.tileID == 'object').toBeTruthy();
Expand All @@ -141,6 +162,7 @@ describe('ImageSource', () => {
const source = createSource({url: '/image.png'});
const promise = waitForEvent(source, 'data', (e) => e.dataType === 'source' && e.sourceDataType === 'metadata');
source.onAdd(new StubMap() as any);
await sleep(0);
server.respond();
await expect(promise).resolves.toBeDefined();
});
Expand Down Expand Up @@ -179,19 +201,21 @@ describe('ImageSource', () => {
source.onAdd(map);
expect(source.image).toBeUndefined();
source.updateImage({url: '/image2.png'});
await sleep(0);
server.respond();
await sleep(10);

expect(source.image).toBeTruthy();
});

test('cancels request if updateImage is used', () => {
test('cancels request if updateImage is used', async () => {
const map = new StubMap() as any;
const source = createSource({url: '/image.png', eventedParent: map});

// Suppress errors because we're aborting.
map.on('error', () => {});
source.onAdd(map);
await sleep(0);

const spy = vi.spyOn(server.requests[0] as any, 'abort');

Expand All @@ -205,6 +229,7 @@ describe('ImageSource', () => {

expect(source.loaded()).toBe(false);
source.onAdd(map);
await sleep(0);
server.respond();
await sleep(0);
expect(source.loaded()).toBe(true);
Expand All @@ -216,6 +241,7 @@ describe('ImageSource', () => {

expect(missingImagesource.loaded()).toBe(false);
missingImagesource.onAdd(map);
await sleep(0);
server.respond();
await sleep(0);

Expand Down
2 changes: 1 addition & 1 deletion src/source/image_source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export class ImageSource extends Evented implements Source {

this._request = new AbortController();
try {
const image = await ImageRequest.getImage(this.map._requestManager.transformRequest(this.url, ResourceType.Image), this._request);
const image = await ImageRequest.getImage(await this.map._requestManager.transformRequest(this.url, ResourceType.Image), this._request);
this._request = null;
this._loaded = true;

Expand Down
Loading