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.
Files changed (150) hide show
  1. package/dist/maplibre-gl-dev.mjs +1108 -427
  2. package/dist/maplibre-gl-dev.mjs.map +1 -1
  3. package/dist/maplibre-gl-shared-dev.mjs +359 -291
  4. package/dist/maplibre-gl-shared-dev.mjs.map +1 -1
  5. package/dist/maplibre-gl-shared.mjs +2 -2
  6. package/dist/maplibre-gl-shared.mjs.map +1 -1
  7. package/dist/maplibre-gl-worker-dev.mjs +23 -49
  8. package/dist/maplibre-gl-worker-dev.mjs.map +1 -1
  9. package/dist/maplibre-gl-worker.mjs +2 -2
  10. package/dist/maplibre-gl-worker.mjs.map +1 -1
  11. package/dist/maplibre-gl.css +1 -1
  12. package/dist/maplibre-gl.d.ts +467 -151
  13. package/dist/maplibre-gl.mjs +49 -49
  14. package/dist/maplibre-gl.mjs.map +1 -1
  15. package/package.json +12 -22
  16. package/src/data/bucket/fill_extrusion_bucket.ts +1 -13
  17. package/src/data/bucket/round_polygon_corners.test.ts +84 -36
  18. package/src/data/bucket/round_polygon_corners.ts +90 -39
  19. package/src/data/extent_bounds.ts +28 -1
  20. package/src/data/feature_index.bench.ts +21 -0
  21. package/src/data/segment.test.ts +2 -2
  22. package/src/geo/edge_insets.test.ts +1 -1
  23. package/src/geo/lng_lat.test.ts +7 -7
  24. package/src/geo/lng_lat_bounds.test.ts +4 -4
  25. package/src/geo/mercator_coordinate.test.ts +2 -2
  26. package/src/geo/projection/camera_helper.ts +5 -0
  27. package/src/geo/projection/globe_transform.test.ts +154 -1
  28. package/src/geo/projection/globe_transform.ts +7 -3
  29. package/src/geo/projection/globe_utils.ts +189 -2
  30. package/src/geo/projection/mercator_camera_helper.ts +1 -1
  31. package/src/geo/projection/mercator_transform.ts +21 -5
  32. package/src/geo/projection/vertical_perspective_camera_helper.test.ts +24 -0
  33. package/src/geo/projection/vertical_perspective_camera_helper.ts +71 -41
  34. package/src/geo/projection/vertical_perspective_transform.ts +7 -1
  35. package/src/geo/transform_interface.ts +13 -1
  36. package/src/index.ts +7 -2
  37. package/src/render/image_atlas.test.ts +180 -0
  38. package/src/render/image_atlas.ts +52 -6
  39. package/src/render/image_manager.test.ts +111 -0
  40. package/src/render/image_manager.ts +143 -144
  41. package/src/render/painter.ts +6 -14
  42. package/src/render/pattern_atlas.test.ts +131 -0
  43. package/src/render/pattern_atlas.ts +144 -0
  44. package/src/render/update_pattern_positions_in_program.test.ts +1 -1
  45. package/src/shaders/glsl/_prelude.vertex.glsl +10 -11
  46. package/src/shaders/glsl/_prelude.vertex.glsl.g.ts +1 -1
  47. package/src/shaders/glsl/circle.vertex.glsl +4 -4
  48. package/src/shaders/glsl/circle.vertex.glsl.g.ts +1 -1
  49. package/src/shaders/glsl/color_relief.fragment.glsl +1 -2
  50. package/src/shaders/glsl/color_relief.fragment.glsl.g.ts +1 -1
  51. package/src/shaders/glsl/fill_extrusion.vertex.glsl +3 -3
  52. package/src/shaders/glsl/fill_extrusion.vertex.glsl.g.ts +1 -1
  53. package/src/shaders/glsl/fill_extrusion_pattern.vertex.glsl +5 -5
  54. package/src/shaders/glsl/fill_extrusion_pattern.vertex.glsl.g.ts +1 -1
  55. package/src/shaders/glsl/heatmap.vertex.glsl +4 -4
  56. package/src/shaders/glsl/heatmap.vertex.glsl.g.ts +1 -1
  57. package/src/shaders/glsl/hillshade_prepare.fragment.glsl +12 -12
  58. package/src/shaders/glsl/hillshade_prepare.fragment.glsl.g.ts +1 -1
  59. package/src/shaders/glsl/line.vertex.glsl +7 -7
  60. package/src/shaders/glsl/line.vertex.glsl.g.ts +1 -1
  61. package/src/shaders/glsl/line_gradient.vertex.glsl +6 -6
  62. package/src/shaders/glsl/line_gradient.vertex.glsl.g.ts +1 -1
  63. package/src/shaders/glsl/line_gradient_sdf.vertex.glsl +7 -7
  64. package/src/shaders/glsl/line_gradient_sdf.vertex.glsl.g.ts +1 -1
  65. package/src/shaders/glsl/line_pattern.vertex.glsl +7 -7
  66. package/src/shaders/glsl/line_pattern.vertex.glsl.g.ts +1 -1
  67. package/src/shaders/glsl/line_sdf.vertex.glsl +7 -7
  68. package/src/shaders/glsl/line_sdf.vertex.glsl.g.ts +1 -1
  69. package/src/shaders/glsl/raster.fragment.glsl +4 -4
  70. package/src/shaders/glsl/raster.fragment.glsl.g.ts +1 -1
  71. package/src/shaders/glsl/raster.vertex.glsl +11 -6
  72. package/src/shaders/glsl/raster.vertex.glsl.g.ts +1 -1
  73. package/src/shaders/glsl/symbol_icon.vertex.glsl +5 -5
  74. package/src/shaders/glsl/symbol_icon.vertex.glsl.g.ts +1 -1
  75. package/src/shaders/glsl/symbol_sdf.vertex.glsl +5 -5
  76. package/src/shaders/glsl/symbol_sdf.vertex.glsl.g.ts +1 -1
  77. package/src/shaders/glsl/symbol_text_and_icon.vertex.glsl +6 -6
  78. package/src/shaders/glsl/symbol_text_and_icon.vertex.glsl.g.ts +1 -1
  79. package/src/source/canvas_source.test.ts +31 -0
  80. package/src/source/canvas_source.ts +2 -1
  81. package/src/source/geojson_worker_source.test.ts +88 -0
  82. package/src/source/geojson_worker_source.ts +22 -39
  83. package/src/source/image_source.test.ts +246 -38
  84. package/src/source/image_source.ts +132 -19
  85. package/src/source/raster_dem_tile_source.test.ts +37 -3
  86. package/src/source/raster_dem_tile_source.ts +1 -2
  87. package/src/source/raster_dem_tile_worker_source.test.ts +1 -1
  88. package/src/source/raster_tile_source.test.ts +5 -26
  89. package/src/source/raster_tile_source.ts +4 -3
  90. package/src/source/vector_tile_worker_source.test.ts +94 -2
  91. package/src/source/vector_tile_worker_source.ts +18 -27
  92. package/src/source/video_source.test.ts +12 -8
  93. package/src/source/worker_tile.bench.ts +10 -0
  94. package/src/source/worker_tile.ts +0 -3
  95. package/src/style/create_style_layer.bench.ts +15 -0
  96. package/src/style/properties.bench.ts +81 -0
  97. package/src/style/style.test.ts +75 -10
  98. package/src/style/style.ts +37 -64
  99. package/src/style/style_image.ts +67 -9
  100. package/src/style/style_layer.bench.ts +65 -0
  101. package/src/style/style_layer.test.ts +1 -1
  102. package/src/style/style_layer.ts +2 -2
  103. package/src/style/validate_style.bench.ts +11 -0
  104. package/src/style/validate_style.test.ts +2 -2
  105. package/src/symbol/anchor.test.ts +2 -2
  106. package/src/symbol/collision_index.bench.ts +116 -0
  107. package/src/symbol/cross_tile_symbol_index.bench.ts +49 -0
  108. package/src/symbol/cross_tile_symbol_index.test.ts +31 -0
  109. package/src/symbol/cross_tile_symbol_index.ts +67 -37
  110. package/src/symbol/shaping.test.ts +2 -1
  111. package/src/symbol/symbol_layout.bench.ts +30 -0
  112. package/src/tile/tile_id.test.ts +1 -1
  113. package/src/tile/tile_manager.test.ts +69 -1
  114. package/src/tile/tile_manager.ts +16 -11
  115. package/src/ui/control/fullscreen_control.test.ts +10 -10
  116. package/src/ui/control/geolocate_control.test.ts +3 -5
  117. package/src/ui/control/globe_control.test.ts +6 -6
  118. package/src/ui/events.ts +1 -1
  119. package/src/ui/handler/scroll_zoom.test.ts +50 -0
  120. package/src/ui/handler/two_fingers_touch.test.ts +17 -6
  121. package/src/ui/handler_inertia.test.ts +91 -0
  122. package/src/ui/handler_inertia.ts +65 -14
  123. package/src/ui/handler_manager.test.ts +243 -3
  124. package/src/ui/handler_manager.ts +79 -16
  125. package/src/ui/map.ts +17 -3
  126. package/src/ui/map_tests/map_cross_window.test.ts +2 -2
  127. package/src/ui/map_tests/map_events.test.ts +3 -3
  128. package/src/ui/map_tests/map_images.test.ts +55 -0
  129. package/src/ui/map_tests/map_style.test.ts +9 -8
  130. package/src/ui/map_tests/map_webgl.test.ts +30 -0
  131. package/src/ui/marker.test.ts +177 -42
  132. package/src/ui/marker.ts +120 -10
  133. package/src/ui/popup.test.ts +14 -14
  134. package/src/util/evented.test.ts +33 -28
  135. package/src/util/evented.ts +69 -43
  136. package/src/util/image_request.ts +31 -1
  137. package/src/util/test/null_gl.ts +10 -0
  138. package/src/util/test/util.ts +2 -1
  139. package/src/util/util.test.ts +1 -1
  140. package/src/util/web_worker_transfer.test.ts +5 -5
  141. package/src/webgl/context.ts +20 -0
  142. package/src/webgl/draw/draw_background.ts +1 -1
  143. package/src/webgl/draw/draw_raster.ts +11 -6
  144. package/src/webgl/program/pattern.ts +3 -3
  145. package/src/webgl/program/raster_program.ts +11 -0
  146. package/src/webgl/program.ts +31 -16
  147. package/src/webgl/texture.test.ts +16 -1
  148. package/src/webgl/texture.ts +3 -0
  149. package/src/webgl/vertex_buffer.test.ts +17 -2
  150. package/src/webgl/vertex_buffer.ts +24 -13
@@ -55,11 +55,10 @@ export class RasterDEMTileSource extends RasterTileSource implements Source {
55
55
 
56
56
  override async loadTile(tile: Tile): Promise<void> {
57
57
  const url = tile.tileID.canonical.url(this.tiles, this.map.getPixelRatio(), this.scheme);
58
- const request = await this.map._requestManager.transformRequest(url, ResourceType.Tile);
59
58
  tile.neighboringTiles = this._getNeighboringTiles(tile.tileID);
60
59
  tile.abortController = new AbortController();
61
60
  try {
62
- const response = await ImageRequest.getImage(request, tile.abortController, this.map._refreshExpiredTiles);
61
+ const response = await ImageRequest.transformAndGetImage(this.map._requestManager, url, ResourceType.Tile, tile.abortController, this.map._refreshExpiredTiles, {colorSpaceConversion: 'none'});
63
62
  delete tile.abortController;
64
63
  if (tile.aborted) {
65
64
  tile.state = 'unloaded';
@@ -14,7 +14,7 @@ describe('loadTile', () => {
14
14
  dim: 256
15
15
  } as any as WorkerDEMTileParameters);
16
16
  expect(Object.keys(source.loaded)).toEqual(['0']);
17
- expect(data instanceof DEMData).toBeTruthy();
17
+ expect(data).toBeInstanceOf(DEMData);
18
18
  });
19
19
  });
20
20
 
@@ -445,36 +445,18 @@ describe('RasterTileSource', () => {
445
445
  expect(expiryDataSpy).toHaveBeenCalledTimes(1);
446
446
  });
447
447
 
448
- test('passes a live AbortController to ImageRequest when the tile is aborted during an async transformRequest', async () => {
449
- let transformStarted: () => void;
450
- const transformCalled = new Promise<void>((resolve) => {
451
- transformStarted = resolve;
452
- });
448
+ test('does not request a tile that was aborted while its request was being transformed', async () => {
453
449
  let releaseTransform: (params: {url: string}) => void;
454
450
  const source = createSource({
455
451
  tiles: ['http://example.com/{z}/{x}/{y}.png']
456
452
  }, (url, type) => {
457
453
  if (type !== 'Tile') return {url};
458
- transformStarted();
459
454
  return new Promise<{url: string}>((resolve) => {
460
455
  releaseTransform = resolve;
461
456
  });
462
457
  });
463
458
  source.tiles = ['http://example.com/{z}/{x}/{y}.png'];
464
459
 
465
- const image = {width: 256, height: 256} as ImageBitmap;
466
- let requestController: AbortController;
467
- let tileControllerAtRequestTime: AbortController;
468
- const getImageSpy = vi.spyOn(ImageRequest, 'getImage').mockImplementation(async (_request, abortController) => {
469
- requestController = abortController;
470
- tileControllerAtRequestTime = tile.abortController;
471
- return {data: image};
472
- });
473
- source.map.painter = {
474
- context: {gl: {}},
475
- getTileTexture: () => ({update: vi.fn()})
476
- } as any;
477
-
478
460
  const tile = {
479
461
  tileID: new OverscaledTileID(10, 0, 10, 5, 5),
480
462
  state: 'loading',
@@ -482,16 +464,13 @@ describe('RasterTileSource', () => {
482
464
  } as any as Tile;
483
465
 
484
466
  const loadPromise = source.loadTile(tile);
485
- await transformCalled; // loadTile is suspended in the transform
486
- await source.abortTile(tile); // the abort lands during that suspension
467
+ tile.aborted = true;
468
+ await source.abortTile(tile);
487
469
  releaseTransform({url: 'http://example.com/10/5/5.png'});
488
470
  await expect(loadPromise).resolves.toBeUndefined();
489
471
 
490
- expect(getImageSpy).toHaveBeenCalledTimes(1);
491
- // The request must carry the tile's own live controller, so an abort
492
- // arriving while it is in flight reaches exactly this request.
493
- expect(requestController).toBeInstanceOf(AbortController);
494
- expect(requestController).toBe(tileControllerAtRequestTime);
472
+ expect(server.requests).toHaveLength(0);
473
+ expect(tile.state).toBe('unloaded');
495
474
  });
496
475
 
497
476
  test('does not throw when tile is aborted', async () => {
@@ -206,11 +206,12 @@ export class RasterTileSource extends Evented<SourceEventType> implements Source
206
206
  const url = tile.tileID.canonical.url(this.tiles, this.map.getPixelRatio(), this.scheme);
207
207
  const premultiply = this._premultiplyAlpha;
208
208
  const imageBitmapOptions = premultiply ? undefined : {premultiplyAlpha: 'none'} as const;
209
- const request = await this.map._requestManager.transformRequest(url, ResourceType.Tile);
210
209
  tile.abortController = new AbortController();
211
210
  try {
212
- const response = await ImageRequest.getImage(
213
- request,
211
+ const response = await ImageRequest.transformAndGetImage(
212
+ this.map._requestManager,
213
+ url,
214
+ ResourceType.Tile,
214
215
  tile.abortController,
215
216
  this.map._refreshExpiredTiles,
216
217
  imageBitmapOptions
@@ -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, type ParsingState} from './worker_tile_state.ts';
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
- const parseState = {rawData, cacheControl, resourceTiming}; // Keep data so reloadTile can access if parse is canceled.
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, parseState?: ParsingState): Promise<WorkerTileResult> {
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
- // Transferring a copy of rawTileData because the worker needs to retain its copy.
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
- workerTile.showCollisionBoxes = params.showCollisionBoxes;
217
-
218
- if (workerTile.status === 'parsing') {
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
- // If there was no vector tile data on the initial load, don't try and reparse the tile.
229
- // this seems like a missing case where cache control is lost? see #3309
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
  /**
@@ -1,14 +1,13 @@
1
- import {describe, test, expect, vi} from 'vitest';
2
- import {VideoSource} from './video_source.ts';
3
- import {extend} from '../util/util.ts';
1
+ import {describe, expect, test, vi} from 'vitest';
4
2
  import {getMockDispatcher, waitForEvent} from '../util/test/util.ts';
5
-
6
- import type {Coordinates} from './image_source.ts';
3
+ import {extend} from '../util/util.ts';
4
+ import {VideoSource} from './video_source.ts';
5
+ import {MercatorTransform} from '../geo/projection/mercator_transform.ts';
7
6
  import {Tile} from '../tile/tile.ts';
8
7
  import {OverscaledTileID} from '../tile/tile_id.ts';
9
8
  import {Evented} from '../util/evented.ts';
10
- import {type IReadonlyTransform} from '../geo/transform_interface.ts';
11
- import {MercatorTransform} from '../geo/projection/mercator_transform.ts';
9
+ import type {IReadonlyTransform} from '../geo/transform_interface.ts';
10
+ import type {Coordinates} from './image_source.ts';
12
11
 
13
12
  class StubMap extends Evented {
14
13
  transform: IReadonlyTransform;
@@ -117,7 +116,7 @@ describe('VideoSource', () => {
117
116
  expect(tile.state).toBe('loaded');
118
117
  });
119
118
 
120
- test('onRemove removes playing listener and pauses video', () => {
119
+ test('onRemove removes playing listener, pauses video and deletes the texture', () => {
121
120
  const video = window.document.createElement('video');
122
121
  const removeListenerSpy = vi.spyOn(video, 'removeEventListener');
123
122
  const pauseSpy = vi.spyOn(video, 'pause').mockImplementation(() => {});
@@ -129,9 +128,14 @@ describe('VideoSource', () => {
129
128
  });
130
129
  source.video = video;
131
130
 
131
+ const texture = {destroy: vi.fn()} as any;
132
+ source.texture = texture;
133
+
132
134
  source.onRemove();
133
135
 
134
136
  expect(removeListenerSpy).toHaveBeenCalledWith('playing', expect.any(Function));
135
137
  expect(pauseSpy).toHaveBeenCalled();
138
+ expect(texture.destroy).toHaveBeenCalledTimes(1);
139
+ expect(source.texture).toBeNull();
136
140
  });
137
141
  });
@@ -0,0 +1,10 @@
1
+ import {bench} from 'vitest';
2
+ import {fixtureTiles, parseTile} from '../../test/bench/lib/parse_tile.ts';
3
+
4
+ await Promise.all(fixtureTiles.map(tile => parseTile(tile, false)));
5
+
6
+ bench('WorkerTile.parse', async () => {
7
+ for (const tile of fixtureTiles) {
8
+ await parseTile(tile, false);
9
+ }
10
+ });
@@ -37,7 +37,6 @@ export class WorkerTile {
37
37
  collectResourceTiming: boolean;
38
38
  returnDependencies: boolean;
39
39
 
40
- status: 'parsing' | 'done';
41
40
  data: VectorTileLike;
42
41
  collisionBoxArray: CollisionBoxArray;
43
42
 
@@ -61,7 +60,6 @@ export class WorkerTile {
61
60
  }
62
61
 
63
62
  async parse(data: VectorTileLike, layerIndex: StyleLayerIndex, availableImages: string[], actor: IActor, subdivisionGranularity: SubdivisionGranularitySetting): Promise<WorkerTileResult> {
64
- this.status = 'parsing';
65
63
  this.data = data;
66
64
 
67
65
  this.collisionBoxArray = new CollisionBoxArray();
@@ -192,7 +190,6 @@ export class WorkerTile {
192
190
  }
193
191
  }
194
192
 
195
- this.status = 'done';
196
193
  return {
197
194
  buckets: Object.values(buckets).filter(b => !b.isEmpty()),
198
195
  featureIndex,
@@ -0,0 +1,15 @@
1
+ import {bench} from 'vitest';
2
+ import {derefLayers} from '@maplibre/maplibre-gl-style-spec';
3
+ import {createStyleLayer} from './create_style_layer.ts';
4
+ import brightV9 from '../../test/integration/assets/styles/bright-v9.json' with {type: 'json'};
5
+
6
+ import type {StyleSpecification} from '@maplibre/maplibre-gl-style-spec';
7
+
8
+ const style = brightV9 as unknown as StyleSpecification;
9
+ const layers = derefLayers(style.layers);
10
+
11
+ bench('createStyleLayer', () => {
12
+ for (const layer of layers) {
13
+ createStyleLayer(layer, {});
14
+ }
15
+ });
@@ -0,0 +1,81 @@
1
+ import {bench, describe} from 'vitest';
2
+ import {latest as spec, convertFunction, isFunction, createFunction, createPropertyExpression} from '@maplibre/maplibre-gl-style-spec';
3
+ import brightV9 from '../../test/integration/assets/styles/bright-v9.json' with {type: 'json'};
4
+
5
+ import type {StyleSpecification, StylePropertyExpression, StylePropertySpecification} from '@maplibre/maplibre-gl-style-spec';
6
+
7
+ type Sample = {
8
+ propertySpec: StylePropertySpecification;
9
+ rawValue: unknown;
10
+ rawExpression: unknown;
11
+ compiledFunction: StylePropertyExpression;
12
+ compiledExpression: StylePropertyExpression;
13
+ };
14
+
15
+ function sample(rawValue: unknown, propertySpec: StylePropertySpecification): Sample {
16
+ const rawExpression = convertFunction(rawValue, propertySpec);
17
+ const compiledFunction = createFunction(rawValue, propertySpec) as StylePropertyExpression;
18
+ const compiledExpression = createPropertyExpression(rawExpression, 'expression', propertySpec);
19
+ if (compiledExpression.result === 'error') {
20
+ throw new Error(compiledExpression.value.map(err => `${err.key}: ${err.message}`).join(', '));
21
+ }
22
+ return {
23
+ propertySpec,
24
+ rawValue,
25
+ rawExpression,
26
+ compiledFunction,
27
+ compiledExpression: compiledExpression.value
28
+ };
29
+ }
30
+
31
+ function collect(style: StyleSpecification): Sample[] {
32
+ const samples: Sample[] = [];
33
+
34
+ for (const layer of style.layers) {
35
+ if (!layer.type) {
36
+ continue;
37
+ }
38
+
39
+ for (const key in layer.paint) {
40
+ if (isFunction(layer.paint[key])) {
41
+ samples.push(sample(layer.paint[key], spec[`paint_${layer.type}`][key]));
42
+ }
43
+ }
44
+
45
+ for (const key in layer.layout) {
46
+ if (isFunction(layer.layout[key])) {
47
+ samples.push(sample(layer.layout[key], spec[`layout_${layer.type}`][key]));
48
+ }
49
+ }
50
+ }
51
+
52
+ return samples;
53
+ }
54
+
55
+ const samples = collect(brightV9 as unknown as StyleSpecification);
56
+
57
+ describe('style property functions', () => {
58
+ bench('createFunction', () => {
59
+ for (const {rawValue, propertySpec} of samples) {
60
+ createFunction(rawValue, propertySpec);
61
+ }
62
+ });
63
+
64
+ bench('evaluate function', () => {
65
+ for (const {compiledFunction} of samples) {
66
+ compiledFunction.evaluate({zoom: 0});
67
+ }
68
+ });
69
+
70
+ bench('createPropertyExpression', () => {
71
+ for (const {rawExpression, propertySpec} of samples) {
72
+ createPropertyExpression(rawExpression, 'expression', propertySpec);
73
+ }
74
+ });
75
+
76
+ bench('evaluate expression', () => {
77
+ for (const {compiledExpression} of samples) {
78
+ compiledExpression.evaluate({zoom: 0});
79
+ }
80
+ });
81
+ });
@@ -3,13 +3,15 @@ import {Style} from './style.ts';
3
3
  import {TileManager} from '../tile/tile_manager.ts';
4
4
  import {StyleLayer} from './style_layer.ts';
5
5
  import {extend} from '../util/util.ts';
6
- import {Event} from '../util/evented.ts';
6
+ import {ErrorEvent, Event} from '../util/evented.ts';
7
7
  import {type AJAXError} from '../util/ajax.ts';
8
+ import {MapSourceDataEvent} from '../ui/events.ts';
8
9
  import {RGBAImage} from '../util/image.ts';
9
10
  import {rtlMainThreadPluginFactory} from '../source/rtl_text_plugin_main_thread.ts';
10
11
  import {browser} from '../util/browser.ts';
11
12
  import {OverscaledTileID} from '../tile/tile_id.ts';
12
13
  import {fakeServer, type FakeServer} from 'nise';
14
+ import {ImageRequest} from '../util/image_request.ts';
13
15
 
14
16
  import {type EvaluationParameters} from './evaluation_parameters.ts';
15
17
  import {Color, type Feature, type LayerSpecification, type GeoJSONSourceSpecification, type FilterSpecification, type SourceSpecification, type StyleSpecification, type SymbolLayerSpecification, type SkySpecification, type CameraFunctionSpecification} from '@maplibre/maplibre-gl-style-spec';
@@ -72,6 +74,7 @@ let mockConsoleError: MockInstance;
72
74
  beforeEach(() => {
73
75
  global.fetch = null;
74
76
  server = fakeServer.create();
77
+ ImageRequest.resetRequestQueue();
75
78
  mockConsoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
76
79
  });
77
80
 
@@ -374,7 +377,7 @@ describe('Style.loadJSON', () => {
374
377
  }));
375
378
 
376
379
  await style.once('style.load');
377
- expect(style.tileManagers['mapLibre'] instanceof TileManager).toBeTruthy();
380
+ expect(style.tileManagers['mapLibre']).toBeInstanceOf(TileManager);
378
381
  });
379
382
 
380
383
  test('creates layers', async () => {
@@ -396,7 +399,7 @@ describe('Style.loadJSON', () => {
396
399
  });
397
400
 
398
401
  await style.once('style.load');
399
- expect(style.getLayer('fill') instanceof StyleLayer).toBeTruthy();
402
+ expect(style.getLayer('fill')).toBeInstanceOf(StyleLayer);
400
403
  });
401
404
 
402
405
  test('transforms sprite json and image URLs before request', async () => {
@@ -460,7 +463,7 @@ describe('Style.loadJSON', () => {
460
463
  const errorPromise = style.once('error');
461
464
 
462
465
  await style.once('style.load');
463
- style._layers.background.fire(new Event('error', {mapLibre: true}));
466
+ style._layers.background.fire(new ErrorEvent(new Error('test'), {mapLibre: true}));
464
467
 
465
468
  const e = await errorPromise;
466
469
  expect((e as any).layer).toEqual({id: 'background'});
@@ -1347,8 +1350,8 @@ describe('Style.addSource', () => {
1347
1350
  });
1348
1351
 
1349
1352
  style.addSource('source-id', source); // fires data twice
1350
- style.tileManagers['source-id'].fire(new Event('error'));
1351
- style.tileManagers['source-id'].fire(new Event('data'));
1353
+ style.tileManagers['source-id'].fire(new ErrorEvent(new Error('test')));
1354
+ style.tileManagers['source-id'].fire(new MapSourceDataEvent('data'));
1352
1355
 
1353
1356
  await expect(Promise.all(promises)).resolves.toBeDefined();
1354
1357
  });
@@ -1446,8 +1449,8 @@ describe('Style.removeSource', () => {
1446
1449
 
1447
1450
  style.on('data', () => expect(false).toBeTruthy());
1448
1451
  style.on('error', () => expect(false).toBeTruthy());
1449
- tileManager.fire(new Event('data'));
1450
- tileManager.fire(new Event('error'));
1452
+ tileManager.fire(new MapSourceDataEvent('data'));
1453
+ tileManager.fire(new ErrorEvent(new Error('test')));
1451
1454
  });
1452
1455
  });
1453
1456
 
@@ -1704,6 +1707,68 @@ describe('Style.setSprite', () => {
1704
1707
  });
1705
1708
  });
1706
1709
 
1710
+ describe('Style._loadSprite', () => {
1711
+ const FIRST_SPRITE_URL = 'http://example.com/sprite1';
1712
+ const SECOND_SPRITE_URL = 'http://example.com/sprite2';
1713
+
1714
+ const spriteJSON = (...imageIds: string[]) => JSON.stringify(
1715
+ Object.fromEntries(imageIds.map(id => [id, {width: 1, height: 1, x: 0, y: 0, pixelRatio: 1}]))
1716
+ );
1717
+
1718
+ /** Loads a sprite through `setSprite` and resolves once it has been applied to the image manager */
1719
+ function loadSprite(style: Style, url: string): Promise<void> {
1720
+ return new Promise((resolve, reject) => {
1721
+ style.setSprite(url, {}, (err) => err ? reject(err) : resolve());
1722
+ sleep(0).then(() => server.respond());
1723
+ });
1724
+ }
1725
+
1726
+ async function createLoadedStyle(): Promise<Style> {
1727
+ const style = new Style(getStubMap());
1728
+ style.loadJSON(createStyleJSON());
1729
+ await style.once('style.load');
1730
+ return style;
1731
+ }
1732
+
1733
+ beforeEach(() => {
1734
+ vi.spyOn(browser, 'getImageData');
1735
+ server.respondWith('GET', /sprite\d\.png$/, new ArrayBuffer(8) as any);
1736
+ server.respondWith('GET', `${FIRST_SPRITE_URL}.json`, spriteJSON('sharedImage', 'firstSpriteOnlyImage'));
1737
+ server.respondWith('GET', `${SECOND_SPRITE_URL}.json`, spriteJSON('sharedImage', 'secondSpriteOnlyImage'));
1738
+ });
1739
+
1740
+ test('removes the images of the previous sprite that are absent from the new one', async () => {
1741
+ const style = await createLoadedStyle();
1742
+
1743
+ await loadSprite(style, FIRST_SPRITE_URL);
1744
+ expect(style.imageManager.listImages().sort()).toEqual(['firstSpriteOnlyImage', 'sharedImage']);
1745
+
1746
+ await loadSprite(style, SECOND_SPRITE_URL);
1747
+ expect(style.imageManager.listImages().sort()).toEqual(['secondSpriteOnlyImage', 'sharedImage']);
1748
+ });
1749
+
1750
+ test('leaves no image behind when the sprite is unset after having been replaced', async () => {
1751
+ const style = await createLoadedStyle();
1752
+ await loadSprite(style, FIRST_SPRITE_URL);
1753
+ await loadSprite(style, SECOND_SPRITE_URL);
1754
+
1755
+ style.setSprite(undefined);
1756
+
1757
+ expect(style.imageManager.listImages()).toEqual([]);
1758
+ });
1759
+
1760
+ test('only marks the images shared with the previous sprite as updated, so that returning to a sprite does not accumulate per-frame patching work', async () => {
1761
+ const style = await createLoadedStyle();
1762
+ await loadSprite(style, FIRST_SPRITE_URL);
1763
+ const updateVersionAfterFirstLoad = style.imageManager.updateVersion;
1764
+
1765
+ await loadSprite(style, SECOND_SPRITE_URL);
1766
+ await loadSprite(style, FIRST_SPRITE_URL);
1767
+
1768
+ expect(style.imageManager.updateVersion - updateVersionAfterFirstLoad).toBe(2);
1769
+ });
1770
+ });
1771
+
1707
1772
  describe('Style.setGeoJSONSourceData', () => {
1708
1773
  const geoJSON = {type: 'FeatureCollection', features: []} as GeoJSON.GeoJSON;
1709
1774
 
@@ -2297,7 +2362,7 @@ describe('Style.addLayer', () => {
2297
2362
  type: 'background'
2298
2363
  });
2299
2364
 
2300
- style._layers.background.fire(new Event('error', {mapLibre: true}));
2365
+ style._layers.background.fire(new ErrorEvent(new Error('test'), {mapLibre: true}));
2301
2366
 
2302
2367
  const e = await errorPromise;
2303
2368
  expect((e as any).layer).toEqual({id: 'background'});
@@ -2614,7 +2679,7 @@ describe('Style.removeLayer', () => {
2614
2679
  // Bind a listener to prevent fallback Evented error reporting.
2615
2680
  layer.on('error', () => {});
2616
2681
 
2617
- layer.fire(new Event('error', {mapLibre: true}));
2682
+ layer.fire(new ErrorEvent(new Error('test'), {mapLibre: true}));
2618
2683
 
2619
2684
  expect(styleErrorListener).not.toHaveBeenCalled();
2620
2685
  });