maplibre-gl 6.2.0 → 6.3.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 +522 -223
- package/dist/maplibre-gl-dev.mjs.map +1 -1
- package/dist/maplibre-gl-shared-dev.mjs +103 -58
- 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 +214 -59
- package/dist/maplibre-gl.mjs +44 -44
- package/dist/maplibre-gl.mjs.map +1 -1
- package/package.json +10 -20
- 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 +25 -0
- package/src/geo/projection/globe_transform.ts +7 -3
- 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.ts +60 -31
- 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 +72 -0
- package/src/render/image_atlas.ts +17 -2
- package/src/render/image_manager.test.ts +46 -0
- package/src/render/image_manager.ts +9 -2
- package/src/render/painter.ts +1 -12
- package/src/render/update_pattern_positions_in_program.test.ts +1 -1
- package/src/shaders/glsl/_prelude.vertex.glsl +2 -3
- 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/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/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 +205 -7
- package/src/source/image_source.ts +118 -4
- package/src/source/raster_dem_tile_worker_source.test.ts +1 -1
- 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 +11 -10
- 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/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.ts +2 -2
- 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 +3 -3
- 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 +29 -0
- package/src/ui/map_tests/map_style.test.ts +9 -8
- package/src/ui/map_tests/map_webgl.test.ts +29 -0
- package/src/ui/marker.test.ts +26 -42
- 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/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_raster.ts +11 -6
- 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
|
@@ -19,6 +19,19 @@ function createSource(options) {
|
|
|
19
19
|
return new ImageSource('id', options, {} as any, options.eventedParent);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
async function createLoadedSourceWithTile(map: Map, server: FakeServer) {
|
|
23
|
+
server.respondImmediately = true;
|
|
24
|
+
const source = createSource({url: '/image.png', eventedParent: map});
|
|
25
|
+
const loaded = waitForEvent(source, 'data', (e: MapSourceDataEvent) => e.sourceDataType === 'metadata');
|
|
26
|
+
source.onAdd(map);
|
|
27
|
+
await loaded;
|
|
28
|
+
|
|
29
|
+
const {z, x, y} = source.tileID;
|
|
30
|
+
const tile = new Tile(new OverscaledTileID(z, 0, z, x, y), 512);
|
|
31
|
+
await source.loadTile(tile);
|
|
32
|
+
return {source, tile};
|
|
33
|
+
}
|
|
34
|
+
|
|
22
35
|
describe('ImageSource', () => {
|
|
23
36
|
stubAjaxGetImage(undefined);
|
|
24
37
|
let server: FakeServer;
|
|
@@ -147,6 +160,112 @@ describe('ImageSource', () => {
|
|
|
147
160
|
expect(afterSerialized.coordinates).toEqual([[0, 0], [-1, 0], [-1, -1], [0, -1]]);
|
|
148
161
|
});
|
|
149
162
|
|
|
163
|
+
test('sets perspective transform for non-parallelogram coordinates', () => {
|
|
164
|
+
const source = createSource({url: '/image.png'});
|
|
165
|
+
source.setCoordinates([
|
|
166
|
+
[-122.52, 37.815],
|
|
167
|
+
[-122.355, 37.8],
|
|
168
|
+
[-122.325, 37.7],
|
|
169
|
+
[-122.545, 37.735]
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
expect(source.perspectiveTransform).not.toEqual([0, 0, 1]);
|
|
173
|
+
expect(source.perspectiveTransform.every(Number.isFinite)).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('sets perspective transform when initial coordinates are loaded', async () => {
|
|
177
|
+
const source = createSource({
|
|
178
|
+
url: '/image.png',
|
|
179
|
+
coordinates: [
|
|
180
|
+
[-122.52, 37.815],
|
|
181
|
+
[-122.355, 37.8],
|
|
182
|
+
[-122.325, 37.7],
|
|
183
|
+
[-122.545, 37.735]
|
|
184
|
+
]
|
|
185
|
+
});
|
|
186
|
+
const promise = waitForEvent(source, 'data', (e) => e.sourceDataType === 'content');
|
|
187
|
+
|
|
188
|
+
source.onAdd(map);
|
|
189
|
+
await sleep(0);
|
|
190
|
+
server.respond();
|
|
191
|
+
await promise;
|
|
192
|
+
|
|
193
|
+
expect(source.perspectiveTransform).not.toEqual([0, 0, 1]);
|
|
194
|
+
expect(source.perspectiveTransform.every(Number.isFinite)).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('keeps projective transform when its constant coefficient is zero', () => {
|
|
198
|
+
const source = createSource({url: '/image.png'});
|
|
199
|
+
source.setCoordinates([
|
|
200
|
+
[-67.5, 55.77657301866769],
|
|
201
|
+
[-22.5, 55.77657301866769],
|
|
202
|
+
[0, 40.97989806962013],
|
|
203
|
+
[-90, 40.97989806962013]
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
expect(source.perspectiveTransform).toEqual([0, 1, 0]);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('sets identity perspective transform for parallelogram coordinates', () => {
|
|
210
|
+
const source = createSource({url: '/image.png'});
|
|
211
|
+
source.setCoordinates([
|
|
212
|
+
[-122.431640625, 37.857507156],
|
|
213
|
+
[-122.409667969, 37.857507156],
|
|
214
|
+
[-122.409667969, 37.840156836],
|
|
215
|
+
[-122.431640625, 37.840156836]
|
|
216
|
+
]);
|
|
217
|
+
|
|
218
|
+
expect(source.perspectiveTransform).toEqual([0, 0, 1]);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('sets identity perspective transform for collinear destination coordinates', () => {
|
|
222
|
+
const source = createSource({url: '/image.png'});
|
|
223
|
+
source.setCoordinates([
|
|
224
|
+
[-122.431640625, 37.857507156],
|
|
225
|
+
[-122.409667969, 37.857507156],
|
|
226
|
+
[-122.387695312, 37.857507156],
|
|
227
|
+
[-122.365722656, 37.857507156]
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
expect(source.perspectiveTransform).toEqual([0, 0, 1]);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('sets identity perspective transform when the inverse basis is singular', () => {
|
|
234
|
+
const source = createSource({url: '/image.png'});
|
|
235
|
+
source.setCoordinates([
|
|
236
|
+
[-122.431640625, 37.857507156],
|
|
237
|
+
[-122.431640625, 37.840156836],
|
|
238
|
+
[-122.431640625, 37.822802434],
|
|
239
|
+
[-122.409667969, 37.857507156]
|
|
240
|
+
]);
|
|
241
|
+
|
|
242
|
+
expect(source.perspectiveTransform).toEqual([0, 0, 1]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('sets identity perspective transform for a cancellation-degenerate quad', () => {
|
|
246
|
+
const source = createSource({url: '/image.png'});
|
|
247
|
+
source.setCoordinates([
|
|
248
|
+
[96.85546875, 79.36770077764092],
|
|
249
|
+
[-127.265625, 79.36770077764092],
|
|
250
|
+
[88.06640625, 79.36770077764092],
|
|
251
|
+
[-17.40234375, 59.534318001095585]
|
|
252
|
+
]);
|
|
253
|
+
|
|
254
|
+
expect(source.perspectiveTransform).toEqual([0, 0, 1]);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('sets identity perspective transform when its denominator crosses zero inside the quad', () => {
|
|
258
|
+
const source = createSource({url: '/image.png'});
|
|
259
|
+
source.setCoordinates([
|
|
260
|
+
[-90, 66.51326044311186],
|
|
261
|
+
[0, 66.51326044311186],
|
|
262
|
+
[-78.75, 61.606396371386275],
|
|
263
|
+
[-90, 0]
|
|
264
|
+
]);
|
|
265
|
+
|
|
266
|
+
expect(source.perspectiveTransform).toEqual([0, 0, 1]);
|
|
267
|
+
});
|
|
268
|
+
|
|
150
269
|
test('sets coordinates via updateImage', async () => {
|
|
151
270
|
const source = createSource({url: '/image.png'});
|
|
152
271
|
source.onAdd(map);
|
|
@@ -199,6 +318,57 @@ describe('ImageSource', () => {
|
|
|
199
318
|
expect(tile.state).toBe('loaded');
|
|
200
319
|
});
|
|
201
320
|
|
|
321
|
+
test('uploads a url into the texture the tiles already hold', async () => {
|
|
322
|
+
const {source, tile} = await createLoadedSourceWithTile(map, server);
|
|
323
|
+
source.prepare();
|
|
324
|
+
const texture = source.texture;
|
|
325
|
+
const upload = vi.spyOn(texture, 'update');
|
|
326
|
+
const destroy = vi.spyOn(texture, 'destroy');
|
|
327
|
+
|
|
328
|
+
const load = vi.spyOn(source, 'load');
|
|
329
|
+
source.updateImage({url: '/image2.png'});
|
|
330
|
+
await load.mock.results[0].value;
|
|
331
|
+
source.prepare();
|
|
332
|
+
|
|
333
|
+
expect(upload).toHaveBeenCalledTimes(1);
|
|
334
|
+
expect(destroy).not.toHaveBeenCalled();
|
|
335
|
+
expect(source.texture).toBe(texture);
|
|
336
|
+
expect(tile.texture).toBe(texture);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('uploads a decoded image into the texture the tiles already hold', async () => {
|
|
340
|
+
const {source, tile} = await createLoadedSourceWithTile(map, server);
|
|
341
|
+
source.prepare();
|
|
342
|
+
const texture = source.texture;
|
|
343
|
+
const upload = vi.spyOn(texture, 'update');
|
|
344
|
+
const destroy = vi.spyOn(texture, 'destroy');
|
|
345
|
+
|
|
346
|
+
source.updateImage({image: new ImageBitmap()});
|
|
347
|
+
source.prepare();
|
|
348
|
+
|
|
349
|
+
expect(upload).toHaveBeenCalledTimes(1);
|
|
350
|
+
expect(destroy).not.toHaveBeenCalled();
|
|
351
|
+
expect(source.texture).toBe(texture);
|
|
352
|
+
expect(tile.texture).toBe(texture);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test('keeps the texture the tiles hold live when updateImage runs from an event fired during prepare', async () => {
|
|
356
|
+
const {source, tile} = await createLoadedSourceWithTile(map, server);
|
|
357
|
+
source.on('data', (e: MapSourceDataEvent) => {
|
|
358
|
+
if (e.sourceDataType === 'idle') source.updateImage({image: new ImageBitmap()});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
source.prepare();
|
|
362
|
+
|
|
363
|
+
expect(source.texture).toBeTruthy();
|
|
364
|
+
expect(tile.texture).toBe(source.texture);
|
|
365
|
+
expect(source.texture.texture).not.toBeNull();
|
|
366
|
+
|
|
367
|
+
const upload = vi.spyOn(source.texture, 'update');
|
|
368
|
+
source.prepare();
|
|
369
|
+
expect(upload).toHaveBeenCalledTimes(1);
|
|
370
|
+
});
|
|
371
|
+
|
|
202
372
|
test('serialize url and coordinates', () => {
|
|
203
373
|
const source = createSource({url: '/image.png'});
|
|
204
374
|
|
|
@@ -275,6 +445,41 @@ describe('ImageSource', () => {
|
|
|
275
445
|
expect(errorHandler).not.toHaveBeenCalled();
|
|
276
446
|
});
|
|
277
447
|
|
|
448
|
+
test('keeps the image it displays, and its texture, when the url handed to updateImage fails to load', async () => {
|
|
449
|
+
const {source} = await createLoadedSourceWithTile(map, server);
|
|
450
|
+
source.prepare();
|
|
451
|
+
const texture = source.texture;
|
|
452
|
+
const image = source.image;
|
|
453
|
+
const upload = vi.spyOn(texture, 'update');
|
|
454
|
+
const destroy = vi.spyOn(texture, 'destroy');
|
|
455
|
+
const errorHandler = vi.fn();
|
|
456
|
+
map.on('error', errorHandler);
|
|
457
|
+
|
|
458
|
+
const load = vi.spyOn(source, 'load');
|
|
459
|
+
source.updateImage({url: '/missing-image.png'});
|
|
460
|
+
await load.mock.results[0].value;
|
|
461
|
+
source.prepare();
|
|
462
|
+
|
|
463
|
+
expect(errorHandler).toHaveBeenCalledTimes(1);
|
|
464
|
+
expect(destroy).not.toHaveBeenCalled();
|
|
465
|
+
expect(upload).not.toHaveBeenCalled();
|
|
466
|
+
expect(source.texture).toBe(texture);
|
|
467
|
+
expect(source.image).toBe(image);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test('deletes its texture and drops its image when the source is removed', async () => {
|
|
471
|
+
const {source} = await createLoadedSourceWithTile(map, server);
|
|
472
|
+
source.prepare();
|
|
473
|
+
const destroy = vi.spyOn(source.texture, 'destroy');
|
|
474
|
+
|
|
475
|
+
source.onRemove();
|
|
476
|
+
|
|
477
|
+
expect(destroy).toHaveBeenCalledTimes(1);
|
|
478
|
+
expect(source.texture).toBeNull();
|
|
479
|
+
expect(source.image).toBeNull();
|
|
480
|
+
expect(source.tiles).toEqual({});
|
|
481
|
+
});
|
|
482
|
+
|
|
278
483
|
describe('updateImage with a decoded image', () => {
|
|
279
484
|
let source: ImageSource;
|
|
280
485
|
let transformRequest: Mock<(url: string, resourceType?: string) => any>;
|
|
@@ -309,13 +514,6 @@ describe('ImageSource', () => {
|
|
|
309
514
|
expect(firedMetadata).toBe(true);
|
|
310
515
|
});
|
|
311
516
|
|
|
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
517
|
test('updates coordinates alongside the image', () => {
|
|
320
518
|
source.updateImage({
|
|
321
519
|
image: new ImageBitmap(),
|
|
@@ -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
|
_request: AbortController;
|
|
153
|
+
private _imageDirty: boolean = false;
|
|
147
154
|
|
|
148
155
|
/** @internal */
|
|
149
156
|
constructor(id: string, options: ImageSourceSpecification | VideoSourceSpecification | CanvasSourceSpecification, dispatcher: Dispatcher, eventedParent: Evented) {
|
|
@@ -178,7 +185,7 @@ export class ImageSource extends Evented<SourceEventType> implements Source {
|
|
|
178
185
|
this._loaded = true;
|
|
179
186
|
|
|
180
187
|
if (image?.data) {
|
|
181
|
-
this.image
|
|
188
|
+
this._setImage(image.data);
|
|
182
189
|
if (newCoordinates) {
|
|
183
190
|
this.coordinates = newCoordinates;
|
|
184
191
|
}
|
|
@@ -216,11 +223,10 @@ export class ImageSource extends Evented<SourceEventType> implements Source {
|
|
|
216
223
|
if ('image' in options) {
|
|
217
224
|
// Use the already-decoded image directly, skipping the network request.
|
|
218
225
|
this._loaded = true;
|
|
219
|
-
this.
|
|
226
|
+
this._setImage(options.image);
|
|
220
227
|
if (options.coordinates) {
|
|
221
228
|
this.coordinates = options.coordinates;
|
|
222
229
|
}
|
|
223
|
-
this.texture = null;
|
|
224
230
|
this._finishLoading();
|
|
225
231
|
return this;
|
|
226
232
|
}
|
|
@@ -230,10 +236,22 @@ export class ImageSource extends Evented<SourceEventType> implements Source {
|
|
|
230
236
|
}
|
|
231
237
|
|
|
232
238
|
this.options.url = options.url;
|
|
233
|
-
this.load(options.coordinates)
|
|
239
|
+
this.load(options.coordinates);
|
|
234
240
|
return this;
|
|
235
241
|
}
|
|
236
242
|
|
|
243
|
+
/** Loaded tiles hold `this.texture`, so the wrapper has to outlive the images in it. */
|
|
244
|
+
private _setImage(image: ImageSourceImage): void {
|
|
245
|
+
this.image = image;
|
|
246
|
+
this._imageDirty = true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Teardown only: dropping the reference alone leaves the allocation to the GC. */
|
|
250
|
+
private _disposeTexture(): void {
|
|
251
|
+
this.texture?.destroy();
|
|
252
|
+
this.texture = null;
|
|
253
|
+
}
|
|
254
|
+
|
|
237
255
|
_finishLoading(): void {
|
|
238
256
|
if (this.map) {
|
|
239
257
|
this.setCoordinates(this.coordinates);
|
|
@@ -251,6 +269,9 @@ export class ImageSource extends Evented<SourceEventType> implements Source {
|
|
|
251
269
|
this._request.abort();
|
|
252
270
|
this._request = null;
|
|
253
271
|
}
|
|
272
|
+
this._disposeTexture();
|
|
273
|
+
this.image = null;
|
|
274
|
+
this.tiles = {};
|
|
254
275
|
}
|
|
255
276
|
|
|
256
277
|
/**
|
|
@@ -287,6 +308,7 @@ export class ImageSource extends Evented<SourceEventType> implements Source {
|
|
|
287
308
|
// Transform the corner coordinates into the coordinate space of our
|
|
288
309
|
// tile.
|
|
289
310
|
this.tileCoords = cornerCoords.map((coord) => this.tileID.getTilePoint(coord)._round());
|
|
311
|
+
this.perspectiveTransform = calculateRasterPerspectiveTransform(this.tileCoords);
|
|
290
312
|
this.flippedWindingOrder = hasWrongWindingOrder(this.tileCoords);
|
|
291
313
|
|
|
292
314
|
this.fire(new MapSourceDataEvent('data', {sourceDataType: 'content'}));
|
|
@@ -304,7 +326,11 @@ export class ImageSource extends Evented<SourceEventType> implements Source {
|
|
|
304
326
|
if (!this.texture) {
|
|
305
327
|
this.texture = new Texture(context, this.image, gl.RGBA);
|
|
306
328
|
this.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
|
|
329
|
+
} else if (this._imageDirty) {
|
|
330
|
+
this.texture.update(this.image);
|
|
331
|
+
this.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
|
|
307
332
|
}
|
|
333
|
+
this._imageDirty = false;
|
|
308
334
|
|
|
309
335
|
let newTilesLoaded = false;
|
|
310
336
|
for (const w in this.tiles) {
|
|
@@ -418,3 +444,91 @@ function hasWrongWindingOrder(coords: Point[]) {
|
|
|
418
444
|
|
|
419
445
|
return crossProduct < 0;
|
|
420
446
|
}
|
|
447
|
+
|
|
448
|
+
const perspectiveEpsilon = Number.EPSILON;
|
|
449
|
+
// Allows for accumulated rounding error in the three-term homogeneous dot products.
|
|
450
|
+
const perspectiveErrorFactor = 8;
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Calculates the inverse-homography denominator used to sample non-affine
|
|
454
|
+
* image-source texture coordinates. Image source tile coordinates are rounded
|
|
455
|
+
* to integers before this runs, so singularity checks use exact zero or
|
|
456
|
+
* Number.EPSILON scaled to the corner denominators.
|
|
457
|
+
*
|
|
458
|
+
* Based on Paul S. Heckbert, "Fundamentals of Texture Mapping and Image
|
|
459
|
+
* Warping", UCB/CSD-89-516, 1989, section 2.2.3 and appendix A.2.
|
|
460
|
+
*
|
|
461
|
+
* @see https://www2.eecs.berkeley.edu/Pubs/TechRpts/1989/5504.html
|
|
462
|
+
* @see https://www.cs.cmu.edu/~ph/texfund/texfund.pdf
|
|
463
|
+
*/
|
|
464
|
+
function calculateRasterPerspectiveTransform(cornerCoords: Point[]): RasterPerspectiveTransform {
|
|
465
|
+
if (isParallelogram(cornerCoords)) {
|
|
466
|
+
return identityPerspectiveTransform;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const [topLeft, topRight, bottomRight, bottomLeft] = cornerCoords;
|
|
470
|
+
const sx = topLeft.x - topRight.x + bottomRight.x - bottomLeft.x;
|
|
471
|
+
const sy = topLeft.y - topRight.y + bottomRight.y - bottomLeft.y;
|
|
472
|
+
const dx1 = topRight.x - bottomRight.x;
|
|
473
|
+
const dy1 = topRight.y - bottomRight.y;
|
|
474
|
+
const dx2 = bottomLeft.x - bottomRight.x;
|
|
475
|
+
const dy2 = bottomLeft.y - bottomRight.y;
|
|
476
|
+
const basis: mat2 = [dx1, dy1, dx2, dy2];
|
|
477
|
+
const determinant = mat2.determinant(basis);
|
|
478
|
+
|
|
479
|
+
if (Math.abs(determinant) < perspectiveEpsilon) {
|
|
480
|
+
return identityPerspectiveTransform;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const perspectiveX = (sx * dy2 - dx2 * sy) / determinant;
|
|
484
|
+
const perspectiveY = (dx1 * sy - sx * dy1) / determinant;
|
|
485
|
+
|
|
486
|
+
// A finite image of the unit square has positive homogeneous denominators
|
|
487
|
+
// at all four corners after normalizing the top-left denominator to one.
|
|
488
|
+
const forwardDenominators = [1, 1 + perspectiveX, 1 + perspectiveX + perspectiveY, 1 + perspectiveY];
|
|
489
|
+
const forwardDenominatorScale = Math.max(...forwardDenominators.map(value => Math.abs(value)));
|
|
490
|
+
const hasInvalidForwardDenominator = forwardDenominators.some(value =>
|
|
491
|
+
!Number.isFinite(value) || value <= perspectiveErrorFactor * perspectiveEpsilon * forwardDenominatorScale);
|
|
492
|
+
if (!Number.isFinite(forwardDenominatorScale) || forwardDenominatorScale === 0 || hasInvalidForwardDenominator) {
|
|
493
|
+
return identityPerspectiveTransform;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const a = topRight.x - topLeft.x + perspectiveX * topRight.x;
|
|
497
|
+
const b = bottomLeft.x - topLeft.x + perspectiveY * bottomLeft.x;
|
|
498
|
+
const d = topRight.y - topLeft.y + perspectiveX * topRight.y;
|
|
499
|
+
const e = bottomLeft.y - topLeft.y + perspectiveY * bottomLeft.y;
|
|
500
|
+
const inverseDenominator: RasterPerspectiveTransform = [0, 0, 0];
|
|
501
|
+
vec3.cross(inverseDenominator, [a, d, perspectiveX], [b, e, perspectiveY]);
|
|
502
|
+
const normalizationScale = Math.max(...inverseDenominator.map(value => Math.abs(value)));
|
|
503
|
+
const cornerDenominators = cornerCoords.map(({x, y}) =>
|
|
504
|
+
vec3.dot(inverseDenominator, [x, y, 1]));
|
|
505
|
+
const cornerDenominatorErrors = cornerCoords.map(({x, y}) =>
|
|
506
|
+
perspectiveErrorFactor * perspectiveEpsilon * (
|
|
507
|
+
Math.abs(inverseDenominator[0] * x) +
|
|
508
|
+
Math.abs(inverseDenominator[1] * y) +
|
|
509
|
+
Math.abs(inverseDenominator[2])));
|
|
510
|
+
const denominatorScale = Math.max(...cornerDenominators.map(value => Math.abs(value)));
|
|
511
|
+
const denominatorSign = Math.sign(cornerDenominators[0]);
|
|
512
|
+
|
|
513
|
+
// The denominator is affine within each rendered triangle. Reject a
|
|
514
|
+
// transform that reaches or crosses zero anywhere inside the quad.
|
|
515
|
+
const hasInvalidDenominator = cornerDenominators.some((value, index) =>
|
|
516
|
+
!Number.isFinite(value) ||
|
|
517
|
+
Math.abs(value) <= Math.max(perspectiveEpsilon * denominatorScale, cornerDenominatorErrors[index]) ||
|
|
518
|
+
Math.sign(value) !== denominatorSign);
|
|
519
|
+
if (!Number.isFinite(normalizationScale) || normalizationScale === 0 || !Number.isFinite(denominatorScale) ||
|
|
520
|
+
denominatorScale === 0 || hasInvalidDenominator) {
|
|
521
|
+
return identityPerspectiveTransform;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// The denominator is homogeneous, so normalize by its largest coefficient
|
|
525
|
+
// instead of assuming that its constant coefficient is nonzero.
|
|
526
|
+
const transform = inverseDenominator.map(value => value / normalizationScale) as RasterPerspectiveTransform;
|
|
527
|
+
return transform.every(Number.isFinite) ? transform : identityPerspectiveTransform;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function isParallelogram(cornerCoords: Point[]) {
|
|
531
|
+
const [tl, tr, br, bl] = cornerCoords;
|
|
532
|
+
return Math.abs(tl.x + br.x - tr.x - bl.x) < perspectiveEpsilon &&
|
|
533
|
+
Math.abs(tl.y + br.y - tr.y - bl.y) < perspectiveEpsilon;
|
|
534
|
+
}
|
|
@@ -170,6 +170,100 @@ describe('vector tile worker source', () => {
|
|
|
170
170
|
expect(res.rawTileData).toStrictEqual(rawTileData);
|
|
171
171
|
});
|
|
172
172
|
|
|
173
|
+
test('VectorTileWorkerSource.reloadTile includes rawTileData in response if loadTile was aborted', async () => {
|
|
174
|
+
const rawTileData = new ArrayBuffer(0);
|
|
175
|
+
|
|
176
|
+
const layerIndex = new StyleLayerIndex([{
|
|
177
|
+
id: 'test',
|
|
178
|
+
source: 'source',
|
|
179
|
+
'source-layer': 'test',
|
|
180
|
+
type: 'symbol',
|
|
181
|
+
layout: {
|
|
182
|
+
'icon-image': 'hello',
|
|
183
|
+
'text-font': ['StandardFont-Bold'],
|
|
184
|
+
'text-field': '{name}'
|
|
185
|
+
}
|
|
186
|
+
}]);
|
|
187
|
+
|
|
188
|
+
let sendAsyncShouldAbort = false;
|
|
189
|
+
const actor = {
|
|
190
|
+
sendAsync: (message: {type: string; data: unknown}, abortController: AbortController) => {
|
|
191
|
+
if (sendAsyncShouldAbort) {
|
|
192
|
+
return new Promise((_resolve, reject) => {
|
|
193
|
+
reject('aborted by test');
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return new Promise((resolve, reject) => {
|
|
198
|
+
const res = setTimeout(() => {
|
|
199
|
+
const response = message.type === 'getImages' ?
|
|
200
|
+
{'hello': {width: 1, height: 1, data: new Uint8Array([0])}} :
|
|
201
|
+
{'StandardFont-Bold': {width: 1, height: 1, data: new Uint8Array([0])}};
|
|
202
|
+
resolve(response);
|
|
203
|
+
}, 100);
|
|
204
|
+
abortController.signal.addEventListener('abort', () => {
|
|
205
|
+
clearTimeout(res);
|
|
206
|
+
reject('aborted by abortController');
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const source = new VectorTileWorkerSource(actor, layerIndex, ['hello']);
|
|
213
|
+
source.loadVectorTile = (_params, _rawData) => {
|
|
214
|
+
return {
|
|
215
|
+
vectorTile: {
|
|
216
|
+
layers: {
|
|
217
|
+
test: {
|
|
218
|
+
version: 2,
|
|
219
|
+
name: 'test',
|
|
220
|
+
extent: 8192,
|
|
221
|
+
length: 1,
|
|
222
|
+
feature: (featureIndex: number) => ({
|
|
223
|
+
extent: 8192,
|
|
224
|
+
type: 1,
|
|
225
|
+
id: featureIndex,
|
|
226
|
+
properties: {
|
|
227
|
+
name: 'test'
|
|
228
|
+
},
|
|
229
|
+
loadGeometry () {
|
|
230
|
+
return [[new Point(0, 0)]];
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
rawData: rawTileData
|
|
237
|
+
};
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
server.respondWith(request => {
|
|
241
|
+
request.respond(200, {'Content-Type': 'application/pbf'}, rawTileData as any);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
sendAsyncShouldAbort = true;
|
|
245
|
+
const loadTilePromise = source.loadTile({
|
|
246
|
+
source: 'source',
|
|
247
|
+
uid: 0,
|
|
248
|
+
tileID: {overscaledZ: 0, wrap: 0, canonical: {x: 0, y: 0, z: 0, w: 0}},
|
|
249
|
+
request: {url: 'http://localhost:2900/faketile.pbf'},
|
|
250
|
+
subdivisionGranularity: SubdivisionGranularitySetting.noSubdivision,
|
|
251
|
+
} as any as WorkerTileParameters);
|
|
252
|
+
server.respond();
|
|
253
|
+
await expect(loadTilePromise).rejects.toThrow(/aborted/);
|
|
254
|
+
|
|
255
|
+
sendAsyncShouldAbort = false;
|
|
256
|
+
const res = await source.reloadTile({
|
|
257
|
+
source: 'source',
|
|
258
|
+
uid: 0,
|
|
259
|
+
tileID: {overscaledZ: 0, wrap: 0, canonical: {x: 0, y: 0, z: 0, w: 0}},
|
|
260
|
+
subdivisionGranularity: SubdivisionGranularitySetting.noSubdivision,
|
|
261
|
+
} as any as WorkerTileParameters) as WorkerTileWithData;
|
|
262
|
+
expect(res).toBeDefined();
|
|
263
|
+
expect(res.rawTileData).toBeDefined();
|
|
264
|
+
expect(res.rawTileData).toStrictEqual(rawTileData);
|
|
265
|
+
});
|
|
266
|
+
|
|
173
267
|
test('VectorTileWorkerSource.loadTile reparses tile if reloadTile is called during reparsing', async () => {
|
|
174
268
|
const rawTileData = new ArrayBuffer(0);
|
|
175
269
|
const loadVectorData = (_params, _rawData) => {
|
|
@@ -193,7 +287,6 @@ describe('vector tile worker source', () => {
|
|
|
193
287
|
const parseWorkerTileMock = vi
|
|
194
288
|
.spyOn(WorkerTile.prototype, 'parse')
|
|
195
289
|
.mockImplementation(function(this: WorkerTile, _data, _layerIndex, _availableImages, _actor) {
|
|
196
|
-
this.status = 'parsing';
|
|
197
290
|
return new Promise((resolve) => {
|
|
198
291
|
setTimeout(() => resolve({} as WorkerTileResult), 20);
|
|
199
292
|
});
|
|
@@ -248,7 +341,6 @@ describe('vector tile worker source', () => {
|
|
|
248
341
|
const parseWorkerTileMock = vi
|
|
249
342
|
.spyOn(WorkerTile.prototype, 'parse')
|
|
250
343
|
.mockImplementation(function(this: WorkerTile, _data, _layerIndex, _availableImages, _actor) {
|
|
251
|
-
this.status = 'parsing';
|
|
252
344
|
return new Promise((resolve) => {
|
|
253
345
|
setTimeout(() => resolve({} as WorkerTileResult), 20);
|
|
254
346
|
});
|
|
@@ -3,7 +3,7 @@ import {VectorTile} from '@mapbox/vector-tile';
|
|
|
3
3
|
import {fromVectorTileJs, type VectorTileLayerLike, type VectorTileLike} from '@maplibre/vt-pbf';
|
|
4
4
|
import {type ExpiryData, getArrayBuffer} from '../util/ajax.ts';
|
|
5
5
|
import {WorkerTile} from './worker_tile.ts';
|
|
6
|
-
import {WorkerTileState
|
|
6
|
+
import {WorkerTileState} from './worker_tile_state.ts';
|
|
7
7
|
import {BoundedLRUCache} from '../tile/tile_cache.ts';
|
|
8
8
|
import {ensureError, extend} from '../util/util.ts';
|
|
9
9
|
import {RequestPerformance} from '../util/request_performance.ts';
|
|
@@ -106,17 +106,12 @@ export class VectorTileWorkerSource implements WorkerSource {
|
|
|
106
106
|
|
|
107
107
|
workerTile.vectorTile = vectorTile;
|
|
108
108
|
this.tileState.markLoaded(uid, workerTile);
|
|
109
|
+
const parsingState = {rawData, cacheControl, resourceTiming};
|
|
110
|
+
this.tileState.setParsing(uid, parsingState);
|
|
109
111
|
|
|
110
|
-
|
|
111
|
-
this.tileState.setParsing(uid, parseState);
|
|
112
|
-
try {
|
|
113
|
-
return await this._parseWorkerTile(workerTile, params, parseState);
|
|
114
|
-
} finally {
|
|
115
|
-
this.tileState.removeParsing(uid);
|
|
116
|
-
}
|
|
112
|
+
return await this._parseWorkerTile(workerTile, params);
|
|
117
113
|
} catch (err) {
|
|
118
114
|
this.tileState.finishLoading(uid);
|
|
119
|
-
workerTile.status = 'done';
|
|
120
115
|
this.tileState.markLoaded(uid, workerTile);
|
|
121
116
|
throw err;
|
|
122
117
|
}
|
|
@@ -128,16 +123,23 @@ export class VectorTileWorkerSource implements WorkerSource {
|
|
|
128
123
|
return extend({etagUnmodified: true as const}, cacheControl, resourceTiming);
|
|
129
124
|
}
|
|
130
125
|
|
|
131
|
-
async _parseWorkerTile(workerTile: WorkerTile, params: WorkerTileParameters
|
|
126
|
+
async _parseWorkerTile(workerTile: WorkerTile, params: WorkerTileParameters): Promise<WorkerTileResult> {
|
|
127
|
+
const parseState = this.tileState.getParsing(workerTile.uid);
|
|
128
|
+
|
|
132
129
|
let result = await workerTile.parse(workerTile.vectorTile, this.layerIndex, this.availableImages, this.actor, params.subdivisionGranularity);
|
|
133
130
|
|
|
131
|
+
// We need to pass rawTileData back to the main thread so that it can be stored in the Tile and FeatureIndex.
|
|
132
|
+
// After the main thread has successfully received and stored rawTileData,
|
|
133
|
+
// we no longer need to store it in the worker or transfer additional copies of it.
|
|
134
134
|
if (parseState) {
|
|
135
135
|
const {rawData, cacheControl, resourceTiming} = parseState;
|
|
136
136
|
// Overzoomed tiles are always re-encoded to MVT protobuf by _getOverzoomTile
|
|
137
137
|
const encoding = params.overzoomParameters ? 'mvt' : params.encoding;
|
|
138
|
-
//
|
|
138
|
+
// Return a copy of rawData to the main thread to avoid clearing the worker's buffer
|
|
139
139
|
result = extend({rawTileData: rawData.slice(0), encoding}, result, cacheControl, resourceTiming);
|
|
140
|
+
this.tileState.removeParsing(workerTile.uid);
|
|
140
141
|
}
|
|
142
|
+
// else: this seems like a missing case where cache control is lost? see #3309
|
|
141
143
|
|
|
142
144
|
return result;
|
|
143
145
|
}
|
|
@@ -209,27 +211,16 @@ export class VectorTileWorkerSource implements WorkerSource {
|
|
|
209
211
|
*/
|
|
210
212
|
async reloadTile(params: WorkerTileParameters): Promise<WorkerTileResult> {
|
|
211
213
|
const uid = params.uid;
|
|
212
|
-
|
|
213
214
|
const workerTile = this.tileState.getLoaded(uid);
|
|
214
215
|
if (!workerTile) throw new Error('Should not be trying to reload a tile that was never loaded or has been removed');
|
|
215
216
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
// if we are cancelling the original parse, make sure to pass the rawTileData from the original parse
|
|
220
|
-
const parseState = this.tileState.getParsing(uid);
|
|
221
|
-
try {
|
|
222
|
-
return await this._parseWorkerTile(workerTile, params, parseState);
|
|
223
|
-
} finally {
|
|
224
|
-
this.tileState.removeParsing(uid);
|
|
225
|
-
}
|
|
217
|
+
// If there was no vector tile data on the initial load, don't try to reparse the tile.
|
|
218
|
+
if (!workerTile.vectorTile) {
|
|
219
|
+
return;
|
|
226
220
|
}
|
|
227
221
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (workerTile.status === 'done' && workerTile.vectorTile) {
|
|
231
|
-
return await this._parseWorkerTile(workerTile, params);
|
|
232
|
-
}
|
|
222
|
+
workerTile.showCollisionBoxes = params.showCollisionBoxes;
|
|
223
|
+
return await this._parseWorkerTile(workerTile, params);
|
|
233
224
|
}
|
|
234
225
|
|
|
235
226
|
/**
|