maplibre-gl 6.2.0 → 6.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/maplibre-gl-dev.mjs +1108 -427
- package/dist/maplibre-gl-dev.mjs.map +1 -1
- package/dist/maplibre-gl-shared-dev.mjs +359 -291
- package/dist/maplibre-gl-shared-dev.mjs.map +1 -1
- package/dist/maplibre-gl-shared.mjs +2 -2
- package/dist/maplibre-gl-shared.mjs.map +1 -1
- package/dist/maplibre-gl-worker-dev.mjs +23 -49
- package/dist/maplibre-gl-worker-dev.mjs.map +1 -1
- package/dist/maplibre-gl-worker.mjs +2 -2
- package/dist/maplibre-gl-worker.mjs.map +1 -1
- package/dist/maplibre-gl.css +1 -1
- package/dist/maplibre-gl.d.ts +467 -151
- package/dist/maplibre-gl.mjs +49 -49
- package/dist/maplibre-gl.mjs.map +1 -1
- package/package.json +12 -22
- package/src/data/bucket/fill_extrusion_bucket.ts +1 -13
- package/src/data/bucket/round_polygon_corners.test.ts +84 -36
- package/src/data/bucket/round_polygon_corners.ts +90 -39
- package/src/data/extent_bounds.ts +28 -1
- package/src/data/feature_index.bench.ts +21 -0
- package/src/data/segment.test.ts +2 -2
- package/src/geo/edge_insets.test.ts +1 -1
- package/src/geo/lng_lat.test.ts +7 -7
- package/src/geo/lng_lat_bounds.test.ts +4 -4
- package/src/geo/mercator_coordinate.test.ts +2 -2
- package/src/geo/projection/camera_helper.ts +5 -0
- package/src/geo/projection/globe_transform.test.ts +154 -1
- package/src/geo/projection/globe_transform.ts +7 -3
- package/src/geo/projection/globe_utils.ts +189 -2
- package/src/geo/projection/mercator_camera_helper.ts +1 -1
- package/src/geo/projection/mercator_transform.ts +21 -5
- package/src/geo/projection/vertical_perspective_camera_helper.test.ts +24 -0
- package/src/geo/projection/vertical_perspective_camera_helper.ts +71 -41
- package/src/geo/projection/vertical_perspective_transform.ts +7 -1
- package/src/geo/transform_interface.ts +13 -1
- package/src/index.ts +7 -2
- package/src/render/image_atlas.test.ts +180 -0
- package/src/render/image_atlas.ts +52 -6
- package/src/render/image_manager.test.ts +111 -0
- package/src/render/image_manager.ts +143 -144
- package/src/render/painter.ts +6 -14
- package/src/render/pattern_atlas.test.ts +131 -0
- package/src/render/pattern_atlas.ts +144 -0
- package/src/render/update_pattern_positions_in_program.test.ts +1 -1
- package/src/shaders/glsl/_prelude.vertex.glsl +10 -11
- package/src/shaders/glsl/_prelude.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/circle.vertex.glsl +4 -4
- package/src/shaders/glsl/circle.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/color_relief.fragment.glsl +1 -2
- package/src/shaders/glsl/color_relief.fragment.glsl.g.ts +1 -1
- package/src/shaders/glsl/fill_extrusion.vertex.glsl +3 -3
- package/src/shaders/glsl/fill_extrusion.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/fill_extrusion_pattern.vertex.glsl +5 -5
- package/src/shaders/glsl/fill_extrusion_pattern.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/heatmap.vertex.glsl +4 -4
- package/src/shaders/glsl/heatmap.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/hillshade_prepare.fragment.glsl +12 -12
- package/src/shaders/glsl/hillshade_prepare.fragment.glsl.g.ts +1 -1
- package/src/shaders/glsl/line.vertex.glsl +7 -7
- package/src/shaders/glsl/line.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/line_gradient.vertex.glsl +6 -6
- package/src/shaders/glsl/line_gradient.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/line_gradient_sdf.vertex.glsl +7 -7
- package/src/shaders/glsl/line_gradient_sdf.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/line_pattern.vertex.glsl +7 -7
- package/src/shaders/glsl/line_pattern.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/line_sdf.vertex.glsl +7 -7
- package/src/shaders/glsl/line_sdf.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/raster.fragment.glsl +4 -4
- package/src/shaders/glsl/raster.fragment.glsl.g.ts +1 -1
- package/src/shaders/glsl/raster.vertex.glsl +11 -6
- package/src/shaders/glsl/raster.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/symbol_icon.vertex.glsl +5 -5
- package/src/shaders/glsl/symbol_icon.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/symbol_sdf.vertex.glsl +5 -5
- package/src/shaders/glsl/symbol_sdf.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/symbol_text_and_icon.vertex.glsl +6 -6
- package/src/shaders/glsl/symbol_text_and_icon.vertex.glsl.g.ts +1 -1
- package/src/source/canvas_source.test.ts +31 -0
- package/src/source/canvas_source.ts +2 -1
- package/src/source/geojson_worker_source.test.ts +88 -0
- package/src/source/geojson_worker_source.ts +22 -39
- package/src/source/image_source.test.ts +246 -38
- package/src/source/image_source.ts +132 -19
- package/src/source/raster_dem_tile_source.test.ts +37 -3
- package/src/source/raster_dem_tile_source.ts +1 -2
- package/src/source/raster_dem_tile_worker_source.test.ts +1 -1
- package/src/source/raster_tile_source.test.ts +5 -26
- package/src/source/raster_tile_source.ts +4 -3
- package/src/source/vector_tile_worker_source.test.ts +94 -2
- package/src/source/vector_tile_worker_source.ts +18 -27
- package/src/source/video_source.test.ts +12 -8
- package/src/source/worker_tile.bench.ts +10 -0
- package/src/source/worker_tile.ts +0 -3
- package/src/style/create_style_layer.bench.ts +15 -0
- package/src/style/properties.bench.ts +81 -0
- package/src/style/style.test.ts +75 -10
- package/src/style/style.ts +37 -64
- package/src/style/style_image.ts +67 -9
- package/src/style/style_layer.bench.ts +65 -0
- package/src/style/style_layer.test.ts +1 -1
- package/src/style/style_layer.ts +2 -2
- package/src/style/validate_style.bench.ts +11 -0
- package/src/style/validate_style.test.ts +2 -2
- package/src/symbol/anchor.test.ts +2 -2
- package/src/symbol/collision_index.bench.ts +116 -0
- package/src/symbol/cross_tile_symbol_index.bench.ts +49 -0
- package/src/symbol/cross_tile_symbol_index.test.ts +31 -0
- package/src/symbol/cross_tile_symbol_index.ts +67 -37
- package/src/symbol/shaping.test.ts +2 -1
- package/src/symbol/symbol_layout.bench.ts +30 -0
- package/src/tile/tile_id.test.ts +1 -1
- package/src/tile/tile_manager.test.ts +69 -1
- package/src/tile/tile_manager.ts +16 -11
- package/src/ui/control/fullscreen_control.test.ts +10 -10
- package/src/ui/control/geolocate_control.test.ts +3 -5
- package/src/ui/control/globe_control.test.ts +6 -6
- package/src/ui/events.ts +1 -1
- package/src/ui/handler/scroll_zoom.test.ts +50 -0
- package/src/ui/handler/two_fingers_touch.test.ts +17 -6
- package/src/ui/handler_inertia.test.ts +91 -0
- package/src/ui/handler_inertia.ts +65 -14
- package/src/ui/handler_manager.test.ts +243 -3
- package/src/ui/handler_manager.ts +79 -16
- package/src/ui/map.ts +17 -3
- package/src/ui/map_tests/map_cross_window.test.ts +2 -2
- package/src/ui/map_tests/map_events.test.ts +3 -3
- package/src/ui/map_tests/map_images.test.ts +55 -0
- package/src/ui/map_tests/map_style.test.ts +9 -8
- package/src/ui/map_tests/map_webgl.test.ts +30 -0
- package/src/ui/marker.test.ts +177 -42
- package/src/ui/marker.ts +120 -10
- package/src/ui/popup.test.ts +14 -14
- package/src/util/evented.test.ts +33 -28
- package/src/util/evented.ts +69 -43
- package/src/util/image_request.ts +31 -1
- package/src/util/test/null_gl.ts +10 -0
- package/src/util/test/util.ts +2 -1
- package/src/util/util.test.ts +1 -1
- package/src/util/web_worker_transfer.test.ts +5 -5
- package/src/webgl/context.ts +20 -0
- package/src/webgl/draw/draw_background.ts +1 -1
- package/src/webgl/draw/draw_raster.ts +11 -6
- package/src/webgl/program/pattern.ts +3 -3
- package/src/webgl/program/raster_program.ts +11 -0
- package/src/webgl/program.ts +31 -16
- package/src/webgl/texture.test.ts +16 -1
- package/src/webgl/texture.ts +3 -0
- package/src/webgl/vertex_buffer.test.ts +17 -2
- package/src/webgl/vertex_buffer.ts +24 -13
|
@@ -5,7 +5,6 @@ import {type FakeServer, fakeServer} from 'nise';
|
|
|
5
5
|
import {beforeMapTest, createMap, sleep, stubAjaxGetImage, waitForEvent} from '../util/test/util.ts';
|
|
6
6
|
import {Tile} from '../tile/tile.ts';
|
|
7
7
|
import {OverscaledTileID} from '../tile/tile_id.ts';
|
|
8
|
-
import {ImageRequest} from '../util/image_request.ts';
|
|
9
8
|
import type {Texture} from '../webgl/texture.ts';
|
|
10
9
|
import type {ImageSourceSpecification} from '@maplibre/maplibre-gl-style-spec';
|
|
11
10
|
import type {MapSourceDataEvent} from '../ui/events.ts';
|
|
@@ -19,6 +18,19 @@ function createSource(options) {
|
|
|
19
18
|
return new ImageSource('id', options, {} as any, options.eventedParent);
|
|
20
19
|
}
|
|
21
20
|
|
|
21
|
+
async function createLoadedSourceWithTile(map: Map, server: FakeServer) {
|
|
22
|
+
server.respondImmediately = true;
|
|
23
|
+
const source = createSource({url: '/image.png', eventedParent: map});
|
|
24
|
+
const loaded = waitForEvent(source, 'data', (e: MapSourceDataEvent) => e.sourceDataType === 'metadata');
|
|
25
|
+
source.onAdd(map);
|
|
26
|
+
await loaded;
|
|
27
|
+
|
|
28
|
+
const {z, x, y} = source.tileID;
|
|
29
|
+
const tile = new Tile(new OverscaledTileID(z, 0, z, x, y), 512);
|
|
30
|
+
await source.loadTile(tile);
|
|
31
|
+
return {source, tile};
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
describe('ImageSource', () => {
|
|
23
35
|
stubAjaxGetImage(undefined);
|
|
24
36
|
let server: FakeServer;
|
|
@@ -58,40 +70,24 @@ describe('ImageSource', () => {
|
|
|
58
70
|
expect(source.image).toBeTruthy();
|
|
59
71
|
});
|
|
60
72
|
|
|
61
|
-
test('
|
|
73
|
+
test('does not request the image when the source is removed while its request is being transformed', async () => {
|
|
74
|
+
server.respondImmediately = true;
|
|
62
75
|
const source = createSource({url: '/image.png'});
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
transformStarted = resolve;
|
|
66
|
-
});
|
|
76
|
+
const errorHandler = vi.fn();
|
|
77
|
+
source.on('error', errorHandler);
|
|
67
78
|
let releaseTransform: (params: {url: string}) => void;
|
|
68
|
-
map.setTransformRequest(() => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
const image = {width: 1, height: 1} as ImageBitmap;
|
|
75
|
-
let requestController: AbortController;
|
|
76
|
-
let sourceControllerAtRequestTime: AbortController;
|
|
77
|
-
const getImageSpy = vi.spyOn(ImageRequest, 'getImage').mockImplementation(async (_request, abortController) => {
|
|
78
|
-
requestController = abortController;
|
|
79
|
-
sourceControllerAtRequestTime = source._request;
|
|
80
|
-
return {data: image};
|
|
81
|
-
});
|
|
79
|
+
map.setTransformRequest(() => new Promise<{url: string}>((resolve) => {
|
|
80
|
+
releaseTransform = resolve;
|
|
81
|
+
}));
|
|
82
|
+
const load = vi.spyOn(source, 'load');
|
|
82
83
|
|
|
83
84
|
source.onAdd(map);
|
|
84
|
-
|
|
85
|
-
source.onRemove(); // the abort lands during that suspension
|
|
86
|
-
const loaded = waitForEvent(source, 'data', (e: MapSourceDataEvent) => e.sourceDataType === 'metadata');
|
|
85
|
+
source.onRemove();
|
|
87
86
|
releaseTransform({url: '/image.png'});
|
|
88
|
-
await
|
|
87
|
+
await load.mock.results[0].value;
|
|
89
88
|
|
|
90
|
-
expect(
|
|
91
|
-
|
|
92
|
-
// arriving while it is in flight reaches exactly this request.
|
|
93
|
-
expect(requestController).toBeInstanceOf(AbortController);
|
|
94
|
-
expect(requestController).toBe(sourceControllerAtRequestTime);
|
|
89
|
+
expect(server.requests).toHaveLength(0);
|
|
90
|
+
expect(errorHandler).not.toHaveBeenCalled();
|
|
95
91
|
});
|
|
96
92
|
|
|
97
93
|
test('transforms url request', () => {
|
|
@@ -147,6 +143,112 @@ describe('ImageSource', () => {
|
|
|
147
143
|
expect(afterSerialized.coordinates).toEqual([[0, 0], [-1, 0], [-1, -1], [0, -1]]);
|
|
148
144
|
});
|
|
149
145
|
|
|
146
|
+
test('sets perspective transform for non-parallelogram coordinates', () => {
|
|
147
|
+
const source = createSource({url: '/image.png'});
|
|
148
|
+
source.setCoordinates([
|
|
149
|
+
[-122.52, 37.815],
|
|
150
|
+
[-122.355, 37.8],
|
|
151
|
+
[-122.325, 37.7],
|
|
152
|
+
[-122.545, 37.735]
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
expect(source.perspectiveTransform).not.toEqual([0, 0, 1]);
|
|
156
|
+
expect(source.perspectiveTransform.every(Number.isFinite)).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('sets perspective transform when initial coordinates are loaded', async () => {
|
|
160
|
+
const source = createSource({
|
|
161
|
+
url: '/image.png',
|
|
162
|
+
coordinates: [
|
|
163
|
+
[-122.52, 37.815],
|
|
164
|
+
[-122.355, 37.8],
|
|
165
|
+
[-122.325, 37.7],
|
|
166
|
+
[-122.545, 37.735]
|
|
167
|
+
]
|
|
168
|
+
});
|
|
169
|
+
const promise = waitForEvent(source, 'data', (e) => e.sourceDataType === 'content');
|
|
170
|
+
|
|
171
|
+
source.onAdd(map);
|
|
172
|
+
await sleep(0);
|
|
173
|
+
server.respond();
|
|
174
|
+
await promise;
|
|
175
|
+
|
|
176
|
+
expect(source.perspectiveTransform).not.toEqual([0, 0, 1]);
|
|
177
|
+
expect(source.perspectiveTransform.every(Number.isFinite)).toBe(true);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('keeps projective transform when its constant coefficient is zero', () => {
|
|
181
|
+
const source = createSource({url: '/image.png'});
|
|
182
|
+
source.setCoordinates([
|
|
183
|
+
[-67.5, 55.77657301866769],
|
|
184
|
+
[-22.5, 55.77657301866769],
|
|
185
|
+
[0, 40.97989806962013],
|
|
186
|
+
[-90, 40.97989806962013]
|
|
187
|
+
]);
|
|
188
|
+
|
|
189
|
+
expect(source.perspectiveTransform).toEqual([0, 1, 0]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('sets identity perspective transform for parallelogram coordinates', () => {
|
|
193
|
+
const source = createSource({url: '/image.png'});
|
|
194
|
+
source.setCoordinates([
|
|
195
|
+
[-122.431640625, 37.857507156],
|
|
196
|
+
[-122.409667969, 37.857507156],
|
|
197
|
+
[-122.409667969, 37.840156836],
|
|
198
|
+
[-122.431640625, 37.840156836]
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
expect(source.perspectiveTransform).toEqual([0, 0, 1]);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('sets identity perspective transform for collinear destination coordinates', () => {
|
|
205
|
+
const source = createSource({url: '/image.png'});
|
|
206
|
+
source.setCoordinates([
|
|
207
|
+
[-122.431640625, 37.857507156],
|
|
208
|
+
[-122.409667969, 37.857507156],
|
|
209
|
+
[-122.387695312, 37.857507156],
|
|
210
|
+
[-122.365722656, 37.857507156]
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
expect(source.perspectiveTransform).toEqual([0, 0, 1]);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('sets identity perspective transform when the inverse basis is singular', () => {
|
|
217
|
+
const source = createSource({url: '/image.png'});
|
|
218
|
+
source.setCoordinates([
|
|
219
|
+
[-122.431640625, 37.857507156],
|
|
220
|
+
[-122.431640625, 37.840156836],
|
|
221
|
+
[-122.431640625, 37.822802434],
|
|
222
|
+
[-122.409667969, 37.857507156]
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
expect(source.perspectiveTransform).toEqual([0, 0, 1]);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('sets identity perspective transform for a cancellation-degenerate quad', () => {
|
|
229
|
+
const source = createSource({url: '/image.png'});
|
|
230
|
+
source.setCoordinates([
|
|
231
|
+
[96.85546875, 79.36770077764092],
|
|
232
|
+
[-127.265625, 79.36770077764092],
|
|
233
|
+
[88.06640625, 79.36770077764092],
|
|
234
|
+
[-17.40234375, 59.534318001095585]
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
expect(source.perspectiveTransform).toEqual([0, 0, 1]);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('sets identity perspective transform when its denominator crosses zero inside the quad', () => {
|
|
241
|
+
const source = createSource({url: '/image.png'});
|
|
242
|
+
source.setCoordinates([
|
|
243
|
+
[-90, 66.51326044311186],
|
|
244
|
+
[0, 66.51326044311186],
|
|
245
|
+
[-78.75, 61.606396371386275],
|
|
246
|
+
[-90, 0]
|
|
247
|
+
]);
|
|
248
|
+
|
|
249
|
+
expect(source.perspectiveTransform).toEqual([0, 0, 1]);
|
|
250
|
+
});
|
|
251
|
+
|
|
150
252
|
test('sets coordinates via updateImage', async () => {
|
|
151
253
|
const source = createSource({url: '/image.png'});
|
|
152
254
|
source.onAdd(map);
|
|
@@ -199,6 +301,57 @@ describe('ImageSource', () => {
|
|
|
199
301
|
expect(tile.state).toBe('loaded');
|
|
200
302
|
});
|
|
201
303
|
|
|
304
|
+
test('uploads a url into the texture the tiles already hold', async () => {
|
|
305
|
+
const {source, tile} = await createLoadedSourceWithTile(map, server);
|
|
306
|
+
source.prepare();
|
|
307
|
+
const texture = source.texture;
|
|
308
|
+
const upload = vi.spyOn(texture, 'update');
|
|
309
|
+
const destroy = vi.spyOn(texture, 'destroy');
|
|
310
|
+
|
|
311
|
+
const load = vi.spyOn(source, 'load');
|
|
312
|
+
source.updateImage({url: '/image2.png'});
|
|
313
|
+
await load.mock.results[0].value;
|
|
314
|
+
source.prepare();
|
|
315
|
+
|
|
316
|
+
expect(upload).toHaveBeenCalledTimes(1);
|
|
317
|
+
expect(destroy).not.toHaveBeenCalled();
|
|
318
|
+
expect(source.texture).toBe(texture);
|
|
319
|
+
expect(tile.texture).toBe(texture);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test('uploads a decoded image into the texture the tiles already hold', async () => {
|
|
323
|
+
const {source, tile} = await createLoadedSourceWithTile(map, server);
|
|
324
|
+
source.prepare();
|
|
325
|
+
const texture = source.texture;
|
|
326
|
+
const upload = vi.spyOn(texture, 'update');
|
|
327
|
+
const destroy = vi.spyOn(texture, 'destroy');
|
|
328
|
+
|
|
329
|
+
source.updateImage({image: new ImageBitmap()});
|
|
330
|
+
source.prepare();
|
|
331
|
+
|
|
332
|
+
expect(upload).toHaveBeenCalledTimes(1);
|
|
333
|
+
expect(destroy).not.toHaveBeenCalled();
|
|
334
|
+
expect(source.texture).toBe(texture);
|
|
335
|
+
expect(tile.texture).toBe(texture);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('keeps the texture the tiles hold live when updateImage runs from an event fired during prepare', async () => {
|
|
339
|
+
const {source, tile} = await createLoadedSourceWithTile(map, server);
|
|
340
|
+
source.on('data', (e: MapSourceDataEvent) => {
|
|
341
|
+
if (e.sourceDataType === 'idle') source.updateImage({image: new ImageBitmap()});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
source.prepare();
|
|
345
|
+
|
|
346
|
+
expect(source.texture).toBeTruthy();
|
|
347
|
+
expect(tile.texture).toBe(source.texture);
|
|
348
|
+
expect(source.texture.texture).not.toBeNull();
|
|
349
|
+
|
|
350
|
+
const upload = vi.spyOn(source.texture, 'update');
|
|
351
|
+
source.prepare();
|
|
352
|
+
expect(upload).toHaveBeenCalledTimes(1);
|
|
353
|
+
});
|
|
354
|
+
|
|
202
355
|
test('serialize url and coordinates', () => {
|
|
203
356
|
const source = createSource({url: '/image.png'});
|
|
204
357
|
|
|
@@ -223,6 +376,19 @@ describe('ImageSource', () => {
|
|
|
223
376
|
expect(source.image).toBeTruthy();
|
|
224
377
|
});
|
|
225
378
|
|
|
379
|
+
test('keeps the image handed to updateImage instead of the url load it replaced', async () => {
|
|
380
|
+
const source = createSource({url: '/image.png', eventedParent: map});
|
|
381
|
+
const bitmap = new ImageBitmap();
|
|
382
|
+
const load = vi.spyOn(source, 'load');
|
|
383
|
+
|
|
384
|
+
source.onAdd(map);
|
|
385
|
+
source.updateImage({image: bitmap});
|
|
386
|
+
server.respondImmediately = true;
|
|
387
|
+
await load.mock.results[0].value;
|
|
388
|
+
|
|
389
|
+
expect(source.image).toBe(bitmap);
|
|
390
|
+
});
|
|
391
|
+
|
|
226
392
|
test('cancels request if updateImage is used', async () => {
|
|
227
393
|
const source = createSource({url: '/image.png', eventedParent: map});
|
|
228
394
|
|
|
@@ -237,6 +403,20 @@ describe('ImageSource', () => {
|
|
|
237
403
|
expect(spy).toHaveBeenCalled();
|
|
238
404
|
});
|
|
239
405
|
|
|
406
|
+
test('cancels the request updateImage started when updateImage is used again', async () => {
|
|
407
|
+
const source = createSource({url: '/image.png', eventedParent: map});
|
|
408
|
+
const load = vi.spyOn(source, 'load');
|
|
409
|
+
|
|
410
|
+
source.onAdd(map);
|
|
411
|
+
source.updateImage({url: '/image2.png'});
|
|
412
|
+
await load.mock.results[0].value;
|
|
413
|
+
|
|
414
|
+
const spy = vi.spyOn(server.requests[0] as any, 'abort');
|
|
415
|
+
|
|
416
|
+
source.updateImage({url: '/image3.png'});
|
|
417
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
418
|
+
});
|
|
419
|
+
|
|
240
420
|
test('marks the source as loaded when the request has received a response', async () => {
|
|
241
421
|
const source = createSource({url: '/image.png', eventedParent: map});
|
|
242
422
|
|
|
@@ -275,6 +455,41 @@ describe('ImageSource', () => {
|
|
|
275
455
|
expect(errorHandler).not.toHaveBeenCalled();
|
|
276
456
|
});
|
|
277
457
|
|
|
458
|
+
test('keeps the image it displays, and its texture, when the url handed to updateImage fails to load', async () => {
|
|
459
|
+
const {source} = await createLoadedSourceWithTile(map, server);
|
|
460
|
+
source.prepare();
|
|
461
|
+
const texture = source.texture;
|
|
462
|
+
const image = source.image;
|
|
463
|
+
const upload = vi.spyOn(texture, 'update');
|
|
464
|
+
const destroy = vi.spyOn(texture, 'destroy');
|
|
465
|
+
const errorHandler = vi.fn();
|
|
466
|
+
map.on('error', errorHandler);
|
|
467
|
+
|
|
468
|
+
const load = vi.spyOn(source, 'load');
|
|
469
|
+
source.updateImage({url: '/missing-image.png'});
|
|
470
|
+
await load.mock.results[0].value;
|
|
471
|
+
source.prepare();
|
|
472
|
+
|
|
473
|
+
expect(errorHandler).toHaveBeenCalledTimes(1);
|
|
474
|
+
expect(destroy).not.toHaveBeenCalled();
|
|
475
|
+
expect(upload).not.toHaveBeenCalled();
|
|
476
|
+
expect(source.texture).toBe(texture);
|
|
477
|
+
expect(source.image).toBe(image);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test('deletes its texture and drops its image when the source is removed', async () => {
|
|
481
|
+
const {source} = await createLoadedSourceWithTile(map, server);
|
|
482
|
+
source.prepare();
|
|
483
|
+
const destroy = vi.spyOn(source.texture, 'destroy');
|
|
484
|
+
|
|
485
|
+
source.onRemove();
|
|
486
|
+
|
|
487
|
+
expect(destroy).toHaveBeenCalledTimes(1);
|
|
488
|
+
expect(source.texture).toBeNull();
|
|
489
|
+
expect(source.image).toBeNull();
|
|
490
|
+
expect(source.tiles).toEqual({});
|
|
491
|
+
});
|
|
492
|
+
|
|
278
493
|
describe('updateImage with a decoded image', () => {
|
|
279
494
|
let source: ImageSource;
|
|
280
495
|
let transformRequest: Mock<(url: string, resourceType?: string) => any>;
|
|
@@ -286,7 +501,7 @@ describe('ImageSource', () => {
|
|
|
286
501
|
map.on('error', () => {});
|
|
287
502
|
source = createSource({url: '/image.png', eventedParent: map});
|
|
288
503
|
// onAdd starts the initial load synchronously up to its first await, so
|
|
289
|
-
// this.
|
|
504
|
+
// this._abortController is set and transformRequest is called once. Clear that call
|
|
290
505
|
// so tests can assert the image path issues no further request.
|
|
291
506
|
source.onAdd(map);
|
|
292
507
|
transformRequest.mockClear();
|
|
@@ -309,13 +524,6 @@ describe('ImageSource', () => {
|
|
|
309
524
|
expect(firedMetadata).toBe(true);
|
|
310
525
|
});
|
|
311
526
|
|
|
312
|
-
test('resets the texture so the new image is uploaded on the next prepare', () => {
|
|
313
|
-
source.texture = {} as Texture;
|
|
314
|
-
source.updateImage({image: new ImageBitmap()});
|
|
315
|
-
|
|
316
|
-
expect(source.texture).toBeNull();
|
|
317
|
-
});
|
|
318
|
-
|
|
319
527
|
test('updates coordinates alongside the image', () => {
|
|
320
528
|
source.updateImage({
|
|
321
529
|
image: new ImageBitmap(),
|
|
@@ -326,7 +534,7 @@ describe('ImageSource', () => {
|
|
|
326
534
|
});
|
|
327
535
|
|
|
328
536
|
test('cancels a pending request', () => {
|
|
329
|
-
const spy = vi.spyOn(source.
|
|
537
|
+
const spy = vi.spyOn(source._abortController, 'abort');
|
|
330
538
|
source.updateImage({image: new ImageBitmap()});
|
|
331
539
|
expect(spy).toHaveBeenCalled();
|
|
332
540
|
});
|
|
@@ -19,6 +19,11 @@ import type Point from '@mapbox/point-geometry';
|
|
|
19
19
|
import {ensureError, MAX_TILE_ZOOM} from '../util/util.ts';
|
|
20
20
|
import {Bounds} from '../geo/bounds.ts';
|
|
21
21
|
import {isAbortError} from '../util/abort_error.ts';
|
|
22
|
+
import {
|
|
23
|
+
identityPerspectiveTransform,
|
|
24
|
+
type RasterPerspectiveTransform
|
|
25
|
+
} from '../webgl/program/raster_program.ts';
|
|
26
|
+
import {mat2, vec3} from 'gl-matrix';
|
|
22
27
|
|
|
23
28
|
/**
|
|
24
29
|
* Four geographical coordinates,
|
|
@@ -141,9 +146,11 @@ export class ImageSource extends Evented<SourceEventType> implements Source {
|
|
|
141
146
|
image: ImageSourceImage;
|
|
142
147
|
tileID: CanonicalTileID;
|
|
143
148
|
tileCoords: Point[];
|
|
149
|
+
perspectiveTransform: RasterPerspectiveTransform = identityPerspectiveTransform;
|
|
144
150
|
flippedWindingOrder: boolean = false;
|
|
145
151
|
_loaded: boolean;
|
|
146
|
-
|
|
152
|
+
_abortController: AbortController;
|
|
153
|
+
private _imageDirty: boolean = false;
|
|
147
154
|
|
|
148
155
|
/** @internal */
|
|
149
156
|
constructor(id: string, options: ImageSourceSpecification | VideoSourceSpecification | CanvasSourceSpecification, dispatcher: Dispatcher, eventedParent: Evented) {
|
|
@@ -170,26 +177,25 @@ export class ImageSource extends Evented<SourceEventType> implements Source {
|
|
|
170
177
|
|
|
171
178
|
this.url = this.options.url;
|
|
172
179
|
|
|
173
|
-
|
|
174
|
-
this._request = new AbortController();
|
|
180
|
+
this._abortController = new AbortController();
|
|
175
181
|
try {
|
|
176
|
-
const image = await ImageRequest.
|
|
177
|
-
this.
|
|
182
|
+
const image = await ImageRequest.transformAndGetImage(this.map._requestManager, this.url, ResourceType.Image, this._abortController);
|
|
183
|
+
this._abortController = null;
|
|
178
184
|
this._loaded = true;
|
|
179
185
|
|
|
180
186
|
if (image?.data) {
|
|
181
|
-
this.image
|
|
187
|
+
this._setImage(image.data);
|
|
182
188
|
if (newCoordinates) {
|
|
183
189
|
this.coordinates = newCoordinates;
|
|
184
190
|
}
|
|
185
191
|
this._finishLoading();
|
|
186
192
|
}
|
|
187
193
|
} catch (err) {
|
|
188
|
-
|
|
194
|
+
// In case of abort error, the aborter may have started a new request so we don't want to clear its abort controller.
|
|
195
|
+
if (isAbortError(err)) return;
|
|
196
|
+
this._abortController = null;
|
|
189
197
|
this._loaded = true;
|
|
190
|
-
|
|
191
|
-
this.fire(new ErrorEvent(ensureError(err)));
|
|
192
|
-
}
|
|
198
|
+
this.fire(new ErrorEvent(ensureError(err)));
|
|
193
199
|
}
|
|
194
200
|
}
|
|
195
201
|
|
|
@@ -208,19 +214,18 @@ export class ImageSource extends Evented<SourceEventType> implements Source {
|
|
|
208
214
|
* @param options - The options object.
|
|
209
215
|
*/
|
|
210
216
|
updateImage(options: UpdateImageOptions): this {
|
|
211
|
-
if (this.
|
|
212
|
-
this.
|
|
213
|
-
this.
|
|
217
|
+
if (this._abortController) {
|
|
218
|
+
this._abortController.abort();
|
|
219
|
+
this._abortController = null;
|
|
214
220
|
}
|
|
215
221
|
|
|
216
222
|
if ('image' in options) {
|
|
217
223
|
// Use the already-decoded image directly, skipping the network request.
|
|
218
224
|
this._loaded = true;
|
|
219
|
-
this.
|
|
225
|
+
this._setImage(options.image);
|
|
220
226
|
if (options.coordinates) {
|
|
221
227
|
this.coordinates = options.coordinates;
|
|
222
228
|
}
|
|
223
|
-
this.texture = null;
|
|
224
229
|
this._finishLoading();
|
|
225
230
|
return this;
|
|
226
231
|
}
|
|
@@ -230,10 +235,22 @@ export class ImageSource extends Evented<SourceEventType> implements Source {
|
|
|
230
235
|
}
|
|
231
236
|
|
|
232
237
|
this.options.url = options.url;
|
|
233
|
-
this.load(options.coordinates)
|
|
238
|
+
this.load(options.coordinates);
|
|
234
239
|
return this;
|
|
235
240
|
}
|
|
236
241
|
|
|
242
|
+
/** Loaded tiles hold `this.texture`, so the wrapper has to outlive the images in it. */
|
|
243
|
+
private _setImage(image: ImageSourceImage): void {
|
|
244
|
+
this.image = image;
|
|
245
|
+
this._imageDirty = true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Teardown only: dropping the reference alone leaves the allocation to the GC. */
|
|
249
|
+
private _disposeTexture(): void {
|
|
250
|
+
this.texture?.destroy();
|
|
251
|
+
this.texture = null;
|
|
252
|
+
}
|
|
253
|
+
|
|
237
254
|
_finishLoading(): void {
|
|
238
255
|
if (this.map) {
|
|
239
256
|
this.setCoordinates(this.coordinates);
|
|
@@ -247,10 +264,13 @@ export class ImageSource extends Evented<SourceEventType> implements Source {
|
|
|
247
264
|
}
|
|
248
265
|
|
|
249
266
|
onRemove(): void {
|
|
250
|
-
if (this.
|
|
251
|
-
this.
|
|
252
|
-
this.
|
|
267
|
+
if (this._abortController) {
|
|
268
|
+
this._abortController.abort();
|
|
269
|
+
this._abortController = null;
|
|
253
270
|
}
|
|
271
|
+
this._disposeTexture();
|
|
272
|
+
this.image = null;
|
|
273
|
+
this.tiles = {};
|
|
254
274
|
}
|
|
255
275
|
|
|
256
276
|
/**
|
|
@@ -287,6 +307,7 @@ export class ImageSource extends Evented<SourceEventType> implements Source {
|
|
|
287
307
|
// Transform the corner coordinates into the coordinate space of our
|
|
288
308
|
// tile.
|
|
289
309
|
this.tileCoords = cornerCoords.map((coord) => this.tileID.getTilePoint(coord)._round());
|
|
310
|
+
this.perspectiveTransform = calculateRasterPerspectiveTransform(this.tileCoords);
|
|
290
311
|
this.flippedWindingOrder = hasWrongWindingOrder(this.tileCoords);
|
|
291
312
|
|
|
292
313
|
this.fire(new MapSourceDataEvent('data', {sourceDataType: 'content'}));
|
|
@@ -304,7 +325,11 @@ export class ImageSource extends Evented<SourceEventType> implements Source {
|
|
|
304
325
|
if (!this.texture) {
|
|
305
326
|
this.texture = new Texture(context, this.image, gl.RGBA);
|
|
306
327
|
this.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
|
|
328
|
+
} else if (this._imageDirty) {
|
|
329
|
+
this.texture.update(this.image);
|
|
330
|
+
this.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
|
|
307
331
|
}
|
|
332
|
+
this._imageDirty = false;
|
|
308
333
|
|
|
309
334
|
let newTilesLoaded = false;
|
|
310
335
|
for (const w in this.tiles) {
|
|
@@ -418,3 +443,91 @@ function hasWrongWindingOrder(coords: Point[]) {
|
|
|
418
443
|
|
|
419
444
|
return crossProduct < 0;
|
|
420
445
|
}
|
|
446
|
+
|
|
447
|
+
const perspectiveEpsilon = Number.EPSILON;
|
|
448
|
+
// Allows for accumulated rounding error in the three-term homogeneous dot products.
|
|
449
|
+
const perspectiveErrorFactor = 8;
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Calculates the inverse-homography denominator used to sample non-affine
|
|
453
|
+
* image-source texture coordinates. Image source tile coordinates are rounded
|
|
454
|
+
* to integers before this runs, so singularity checks use exact zero or
|
|
455
|
+
* Number.EPSILON scaled to the corner denominators.
|
|
456
|
+
*
|
|
457
|
+
* Based on Paul S. Heckbert, "Fundamentals of Texture Mapping and Image
|
|
458
|
+
* Warping", UCB/CSD-89-516, 1989, section 2.2.3 and appendix A.2.
|
|
459
|
+
*
|
|
460
|
+
* @see https://www2.eecs.berkeley.edu/Pubs/TechRpts/1989/5504.html
|
|
461
|
+
* @see https://www.cs.cmu.edu/~ph/texfund/texfund.pdf
|
|
462
|
+
*/
|
|
463
|
+
function calculateRasterPerspectiveTransform(cornerCoords: Point[]): RasterPerspectiveTransform {
|
|
464
|
+
if (isParallelogram(cornerCoords)) {
|
|
465
|
+
return identityPerspectiveTransform;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const [topLeft, topRight, bottomRight, bottomLeft] = cornerCoords;
|
|
469
|
+
const sx = topLeft.x - topRight.x + bottomRight.x - bottomLeft.x;
|
|
470
|
+
const sy = topLeft.y - topRight.y + bottomRight.y - bottomLeft.y;
|
|
471
|
+
const dx1 = topRight.x - bottomRight.x;
|
|
472
|
+
const dy1 = topRight.y - bottomRight.y;
|
|
473
|
+
const dx2 = bottomLeft.x - bottomRight.x;
|
|
474
|
+
const dy2 = bottomLeft.y - bottomRight.y;
|
|
475
|
+
const basis: mat2 = [dx1, dy1, dx2, dy2];
|
|
476
|
+
const determinant = mat2.determinant(basis);
|
|
477
|
+
|
|
478
|
+
if (Math.abs(determinant) < perspectiveEpsilon) {
|
|
479
|
+
return identityPerspectiveTransform;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const perspectiveX = (sx * dy2 - dx2 * sy) / determinant;
|
|
483
|
+
const perspectiveY = (dx1 * sy - sx * dy1) / determinant;
|
|
484
|
+
|
|
485
|
+
// A finite image of the unit square has positive homogeneous denominators
|
|
486
|
+
// at all four corners after normalizing the top-left denominator to one.
|
|
487
|
+
const forwardDenominators = [1, 1 + perspectiveX, 1 + perspectiveX + perspectiveY, 1 + perspectiveY];
|
|
488
|
+
const forwardDenominatorScale = Math.max(...forwardDenominators.map(value => Math.abs(value)));
|
|
489
|
+
const hasInvalidForwardDenominator = forwardDenominators.some(value =>
|
|
490
|
+
!Number.isFinite(value) || value <= perspectiveErrorFactor * perspectiveEpsilon * forwardDenominatorScale);
|
|
491
|
+
if (!Number.isFinite(forwardDenominatorScale) || forwardDenominatorScale === 0 || hasInvalidForwardDenominator) {
|
|
492
|
+
return identityPerspectiveTransform;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const a = topRight.x - topLeft.x + perspectiveX * topRight.x;
|
|
496
|
+
const b = bottomLeft.x - topLeft.x + perspectiveY * bottomLeft.x;
|
|
497
|
+
const d = topRight.y - topLeft.y + perspectiveX * topRight.y;
|
|
498
|
+
const e = bottomLeft.y - topLeft.y + perspectiveY * bottomLeft.y;
|
|
499
|
+
const inverseDenominator: RasterPerspectiveTransform = [0, 0, 0];
|
|
500
|
+
vec3.cross(inverseDenominator, [a, d, perspectiveX], [b, e, perspectiveY]);
|
|
501
|
+
const normalizationScale = Math.max(...inverseDenominator.map(value => Math.abs(value)));
|
|
502
|
+
const cornerDenominators = cornerCoords.map(({x, y}) =>
|
|
503
|
+
vec3.dot(inverseDenominator, [x, y, 1]));
|
|
504
|
+
const cornerDenominatorErrors = cornerCoords.map(({x, y}) =>
|
|
505
|
+
perspectiveErrorFactor * perspectiveEpsilon * (
|
|
506
|
+
Math.abs(inverseDenominator[0] * x) +
|
|
507
|
+
Math.abs(inverseDenominator[1] * y) +
|
|
508
|
+
Math.abs(inverseDenominator[2])));
|
|
509
|
+
const denominatorScale = Math.max(...cornerDenominators.map(value => Math.abs(value)));
|
|
510
|
+
const denominatorSign = Math.sign(cornerDenominators[0]);
|
|
511
|
+
|
|
512
|
+
// The denominator is affine within each rendered triangle. Reject a
|
|
513
|
+
// transform that reaches or crosses zero anywhere inside the quad.
|
|
514
|
+
const hasInvalidDenominator = cornerDenominators.some((value, index) =>
|
|
515
|
+
!Number.isFinite(value) ||
|
|
516
|
+
Math.abs(value) <= Math.max(perspectiveEpsilon * denominatorScale, cornerDenominatorErrors[index]) ||
|
|
517
|
+
Math.sign(value) !== denominatorSign);
|
|
518
|
+
if (!Number.isFinite(normalizationScale) || normalizationScale === 0 || !Number.isFinite(denominatorScale) ||
|
|
519
|
+
denominatorScale === 0 || hasInvalidDenominator) {
|
|
520
|
+
return identityPerspectiveTransform;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// The denominator is homogeneous, so normalize by its largest coefficient
|
|
524
|
+
// instead of assuming that its constant coefficient is nonzero.
|
|
525
|
+
const transform = inverseDenominator.map(value => value / normalizationScale) as RasterPerspectiveTransform;
|
|
526
|
+
return transform.every(Number.isFinite) ? transform : identityPerspectiveTransform;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function isParallelogram(cornerCoords: Point[]) {
|
|
530
|
+
const [tl, tr, br, bl] = cornerCoords;
|
|
531
|
+
return Math.abs(tl.x + br.x - tr.x - bl.x) < perspectiveEpsilon &&
|
|
532
|
+
Math.abs(tl.y + br.y - tr.y - bl.y) < perspectiveEpsilon;
|
|
533
|
+
}
|
|
@@ -3,6 +3,7 @@ import {fakeServer, type FakeServer} from 'nise';
|
|
|
3
3
|
import {RasterDEMTileSource} from './raster_dem_tile_source.ts';
|
|
4
4
|
import {OverscaledTileID} from '../tile/tile_id.ts';
|
|
5
5
|
import {RequestManager} from '../util/request_manager.ts';
|
|
6
|
+
import {ImageRequest} from '../util/image_request.ts';
|
|
6
7
|
import {type Tile} from '../tile/tile.ts';
|
|
7
8
|
import {getMockDispatcher} from '../util/test/util.ts';
|
|
8
9
|
import {sleep, waitForEvent, waitForMetadataEvent} from '../util/test/util.ts';
|
|
@@ -33,6 +34,7 @@ describe('RasterDEMTileSource', () => {
|
|
|
33
34
|
|
|
34
35
|
afterEach(() => {
|
|
35
36
|
server.restore();
|
|
37
|
+
vi.restoreAllMocks();
|
|
36
38
|
});
|
|
37
39
|
|
|
38
40
|
test('transforms request for TileJSON URL', () => {
|
|
@@ -74,7 +76,7 @@ describe('RasterDEMTileSource', () => {
|
|
|
74
76
|
expect(server.requests[0].requestHeaders.Authorization).toBe('Bearer token');
|
|
75
77
|
});
|
|
76
78
|
|
|
77
|
-
test('transforms tile urls
|
|
79
|
+
test('transforms tile urls and requests them without color space conversion', async () => {
|
|
78
80
|
server.respondWith('/source.json', JSON.stringify({
|
|
79
81
|
minzoom: 0,
|
|
80
82
|
maxzoom: 22,
|
|
@@ -84,6 +86,8 @@ describe('RasterDEMTileSource', () => {
|
|
|
84
86
|
}));
|
|
85
87
|
const source = createSource({url: '/source.json'});
|
|
86
88
|
const transformSpy = vi.spyOn(source.map._requestManager, 'transformRequest');
|
|
89
|
+
const image = await createImageBitmap(new ImageData(16, 16));
|
|
90
|
+
const getImageSpy = vi.spyOn(ImageRequest, 'getImage').mockResolvedValue({data: image});
|
|
87
91
|
const promise = waitForMetadataEvent(source);
|
|
88
92
|
await sleep(0);
|
|
89
93
|
server.respond();
|
|
@@ -92,13 +96,15 @@ describe('RasterDEMTileSource', () => {
|
|
|
92
96
|
tileID: new OverscaledTileID(10, 0, 10, 5, 5),
|
|
93
97
|
state: 'loading',
|
|
94
98
|
loadVectorData () {},
|
|
95
|
-
setExpiryData() {}
|
|
99
|
+
setExpiryData() {},
|
|
100
|
+
actor: 1
|
|
96
101
|
} as any as Tile;
|
|
97
|
-
source.loadTile(tile);
|
|
102
|
+
await source.loadTile(tile);
|
|
98
103
|
|
|
99
104
|
expect(transformSpy).toHaveBeenCalledTimes(1);
|
|
100
105
|
expect(transformSpy.mock.calls[0][0]).toBe('http://example.com/10/5/5.png');
|
|
101
106
|
expect(transformSpy.mock.calls[0][1]).toBe('Tile');
|
|
107
|
+
expect(getImageSpy.mock.calls[0][3]).toEqual({colorSpaceConversion: 'none'});
|
|
102
108
|
});
|
|
103
109
|
|
|
104
110
|
test('can asynchronously transform tile request', async () => {
|
|
@@ -311,6 +317,34 @@ describe('RasterDEMTileSource', () => {
|
|
|
311
317
|
expect(expiryDataSpy).toHaveBeenCalledTimes(1);
|
|
312
318
|
});
|
|
313
319
|
|
|
320
|
+
test('does not request a tile that was aborted while its request was being transformed', async () => {
|
|
321
|
+
let releaseTransform: (params: {url: string}) => void;
|
|
322
|
+
const source = createSource({
|
|
323
|
+
tiles: ['http://example.com/{z}/{x}/{y}.png']
|
|
324
|
+
}, (url, type) => {
|
|
325
|
+
if (type !== 'Tile') return {url};
|
|
326
|
+
return new Promise<{url: string}>((resolve) => {
|
|
327
|
+
releaseTransform = resolve;
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
source.tiles = ['http://example.com/{z}/{x}/{y}.png'];
|
|
331
|
+
|
|
332
|
+
const tile = {
|
|
333
|
+
tileID: new OverscaledTileID(10, 0, 10, 5, 5),
|
|
334
|
+
state: 'loading',
|
|
335
|
+
setExpiryData() {}
|
|
336
|
+
} as any as Tile;
|
|
337
|
+
|
|
338
|
+
const loadPromise = source.loadTile(tile);
|
|
339
|
+
tile.aborted = true;
|
|
340
|
+
await source.abortTile(tile);
|
|
341
|
+
releaseTransform({url: 'http://example.com/10/5/5.png'});
|
|
342
|
+
await expect(loadPromise).resolves.toBeUndefined();
|
|
343
|
+
|
|
344
|
+
expect(server.requests).toHaveLength(0);
|
|
345
|
+
expect(tile.state).toBe('unloaded');
|
|
346
|
+
});
|
|
347
|
+
|
|
314
348
|
test('does not throw when tile is aborted', async () => {
|
|
315
349
|
server.respondWith('/source.json', JSON.stringify({
|
|
316
350
|
minzoom: 0,
|