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.
Files changed (124) hide show
  1. package/dist/maplibre-gl-dev.mjs +522 -223
  2. package/dist/maplibre-gl-dev.mjs.map +1 -1
  3. package/dist/maplibre-gl-shared-dev.mjs +103 -58
  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 +214 -59
  13. package/dist/maplibre-gl.mjs +44 -44
  14. package/dist/maplibre-gl.mjs.map +1 -1
  15. package/package.json +10 -20
  16. package/src/data/feature_index.bench.ts +21 -0
  17. package/src/data/segment.test.ts +2 -2
  18. package/src/geo/edge_insets.test.ts +1 -1
  19. package/src/geo/lng_lat.test.ts +7 -7
  20. package/src/geo/lng_lat_bounds.test.ts +4 -4
  21. package/src/geo/mercator_coordinate.test.ts +2 -2
  22. package/src/geo/projection/camera_helper.ts +5 -0
  23. package/src/geo/projection/globe_transform.test.ts +25 -0
  24. package/src/geo/projection/globe_transform.ts +7 -3
  25. package/src/geo/projection/mercator_camera_helper.ts +1 -1
  26. package/src/geo/projection/mercator_transform.ts +21 -5
  27. package/src/geo/projection/vertical_perspective_camera_helper.ts +60 -31
  28. package/src/geo/projection/vertical_perspective_transform.ts +7 -1
  29. package/src/geo/transform_interface.ts +13 -1
  30. package/src/index.ts +7 -2
  31. package/src/render/image_atlas.test.ts +72 -0
  32. package/src/render/image_atlas.ts +17 -2
  33. package/src/render/image_manager.test.ts +46 -0
  34. package/src/render/image_manager.ts +9 -2
  35. package/src/render/painter.ts +1 -12
  36. package/src/render/update_pattern_positions_in_program.test.ts +1 -1
  37. package/src/shaders/glsl/_prelude.vertex.glsl +2 -3
  38. package/src/shaders/glsl/_prelude.vertex.glsl.g.ts +1 -1
  39. package/src/shaders/glsl/circle.vertex.glsl +4 -4
  40. package/src/shaders/glsl/circle.vertex.glsl.g.ts +1 -1
  41. package/src/shaders/glsl/fill_extrusion.vertex.glsl +3 -3
  42. package/src/shaders/glsl/fill_extrusion.vertex.glsl.g.ts +1 -1
  43. package/src/shaders/glsl/fill_extrusion_pattern.vertex.glsl +5 -5
  44. package/src/shaders/glsl/fill_extrusion_pattern.vertex.glsl.g.ts +1 -1
  45. package/src/shaders/glsl/heatmap.vertex.glsl +4 -4
  46. package/src/shaders/glsl/heatmap.vertex.glsl.g.ts +1 -1
  47. package/src/shaders/glsl/line.vertex.glsl +7 -7
  48. package/src/shaders/glsl/line.vertex.glsl.g.ts +1 -1
  49. package/src/shaders/glsl/line_gradient.vertex.glsl +6 -6
  50. package/src/shaders/glsl/line_gradient.vertex.glsl.g.ts +1 -1
  51. package/src/shaders/glsl/line_gradient_sdf.vertex.glsl +7 -7
  52. package/src/shaders/glsl/line_gradient_sdf.vertex.glsl.g.ts +1 -1
  53. package/src/shaders/glsl/line_pattern.vertex.glsl +7 -7
  54. package/src/shaders/glsl/line_pattern.vertex.glsl.g.ts +1 -1
  55. package/src/shaders/glsl/line_sdf.vertex.glsl +7 -7
  56. package/src/shaders/glsl/line_sdf.vertex.glsl.g.ts +1 -1
  57. package/src/shaders/glsl/raster.fragment.glsl +4 -4
  58. package/src/shaders/glsl/raster.fragment.glsl.g.ts +1 -1
  59. package/src/shaders/glsl/raster.vertex.glsl +11 -6
  60. package/src/shaders/glsl/raster.vertex.glsl.g.ts +1 -1
  61. package/src/shaders/glsl/symbol_icon.vertex.glsl +5 -5
  62. package/src/shaders/glsl/symbol_icon.vertex.glsl.g.ts +1 -1
  63. package/src/shaders/glsl/symbol_sdf.vertex.glsl +5 -5
  64. package/src/shaders/glsl/symbol_sdf.vertex.glsl.g.ts +1 -1
  65. package/src/shaders/glsl/symbol_text_and_icon.vertex.glsl +6 -6
  66. package/src/shaders/glsl/symbol_text_and_icon.vertex.glsl.g.ts +1 -1
  67. package/src/source/canvas_source.test.ts +31 -0
  68. package/src/source/canvas_source.ts +2 -1
  69. package/src/source/geojson_worker_source.test.ts +88 -0
  70. package/src/source/geojson_worker_source.ts +22 -39
  71. package/src/source/image_source.test.ts +205 -7
  72. package/src/source/image_source.ts +118 -4
  73. package/src/source/raster_dem_tile_worker_source.test.ts +1 -1
  74. package/src/source/vector_tile_worker_source.test.ts +94 -2
  75. package/src/source/vector_tile_worker_source.ts +18 -27
  76. package/src/source/video_source.test.ts +12 -8
  77. package/src/source/worker_tile.bench.ts +10 -0
  78. package/src/source/worker_tile.ts +0 -3
  79. package/src/style/create_style_layer.bench.ts +15 -0
  80. package/src/style/properties.bench.ts +81 -0
  81. package/src/style/style.test.ts +11 -10
  82. package/src/style/style_image.ts +67 -9
  83. package/src/style/style_layer.bench.ts +65 -0
  84. package/src/style/style_layer.test.ts +1 -1
  85. package/src/style/style_layer.ts +2 -2
  86. package/src/style/validate_style.bench.ts +11 -0
  87. package/src/style/validate_style.test.ts +2 -2
  88. package/src/symbol/anchor.test.ts +2 -2
  89. package/src/symbol/collision_index.bench.ts +116 -0
  90. package/src/symbol/cross_tile_symbol_index.bench.ts +49 -0
  91. package/src/symbol/shaping.test.ts +2 -1
  92. package/src/symbol/symbol_layout.bench.ts +30 -0
  93. package/src/tile/tile_id.test.ts +1 -1
  94. package/src/tile/tile_manager.ts +2 -2
  95. package/src/ui/control/fullscreen_control.test.ts +10 -10
  96. package/src/ui/control/geolocate_control.test.ts +3 -5
  97. package/src/ui/control/globe_control.test.ts +6 -6
  98. package/src/ui/events.ts +1 -1
  99. package/src/ui/handler/scroll_zoom.test.ts +50 -0
  100. package/src/ui/handler/two_fingers_touch.test.ts +3 -3
  101. package/src/ui/handler_manager.test.ts +243 -3
  102. package/src/ui/handler_manager.ts +79 -16
  103. package/src/ui/map.ts +17 -3
  104. package/src/ui/map_tests/map_cross_window.test.ts +2 -2
  105. package/src/ui/map_tests/map_events.test.ts +3 -3
  106. package/src/ui/map_tests/map_images.test.ts +29 -0
  107. package/src/ui/map_tests/map_style.test.ts +9 -8
  108. package/src/ui/map_tests/map_webgl.test.ts +29 -0
  109. package/src/ui/marker.test.ts +26 -42
  110. package/src/ui/popup.test.ts +14 -14
  111. package/src/util/evented.test.ts +33 -28
  112. package/src/util/evented.ts +69 -43
  113. package/src/util/test/null_gl.ts +10 -0
  114. package/src/util/test/util.ts +2 -1
  115. package/src/util/util.test.ts +1 -1
  116. package/src/util/web_worker_transfer.test.ts +5 -5
  117. package/src/webgl/context.ts +20 -0
  118. package/src/webgl/draw/draw_raster.ts +11 -6
  119. package/src/webgl/program/raster_program.ts +11 -0
  120. package/src/webgl/program.ts +31 -16
  121. package/src/webgl/texture.test.ts +16 -1
  122. package/src/webgl/texture.ts +3 -0
  123. package/src/webgl/vertex_buffer.test.ts +17 -2
  124. package/src/webgl/vertex_buffer.ts +24 -13
@@ -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,8 +3,9 @@ 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';
@@ -374,7 +375,7 @@ describe('Style.loadJSON', () => {
374
375
  }));
375
376
 
376
377
  await style.once('style.load');
377
- expect(style.tileManagers['mapLibre'] instanceof TileManager).toBeTruthy();
378
+ expect(style.tileManagers['mapLibre']).toBeInstanceOf(TileManager);
378
379
  });
379
380
 
380
381
  test('creates layers', async () => {
@@ -396,7 +397,7 @@ describe('Style.loadJSON', () => {
396
397
  });
397
398
 
398
399
  await style.once('style.load');
399
- expect(style.getLayer('fill') instanceof StyleLayer).toBeTruthy();
400
+ expect(style.getLayer('fill')).toBeInstanceOf(StyleLayer);
400
401
  });
401
402
 
402
403
  test('transforms sprite json and image URLs before request', async () => {
@@ -460,7 +461,7 @@ describe('Style.loadJSON', () => {
460
461
  const errorPromise = style.once('error');
461
462
 
462
463
  await style.once('style.load');
463
- style._layers.background.fire(new Event('error', {mapLibre: true}));
464
+ style._layers.background.fire(new ErrorEvent(new Error('test'), {mapLibre: true}));
464
465
 
465
466
  const e = await errorPromise;
466
467
  expect((e as any).layer).toEqual({id: 'background'});
@@ -1347,8 +1348,8 @@ describe('Style.addSource', () => {
1347
1348
  });
1348
1349
 
1349
1350
  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'));
1351
+ style.tileManagers['source-id'].fire(new ErrorEvent(new Error('test')));
1352
+ style.tileManagers['source-id'].fire(new MapSourceDataEvent('data'));
1352
1353
 
1353
1354
  await expect(Promise.all(promises)).resolves.toBeDefined();
1354
1355
  });
@@ -1446,8 +1447,8 @@ describe('Style.removeSource', () => {
1446
1447
 
1447
1448
  style.on('data', () => expect(false).toBeTruthy());
1448
1449
  style.on('error', () => expect(false).toBeTruthy());
1449
- tileManager.fire(new Event('data'));
1450
- tileManager.fire(new Event('error'));
1450
+ tileManager.fire(new MapSourceDataEvent('data'));
1451
+ tileManager.fire(new ErrorEvent(new Error('test')));
1451
1452
  });
1452
1453
  });
1453
1454
 
@@ -2297,7 +2298,7 @@ describe('Style.addLayer', () => {
2297
2298
  type: 'background'
2298
2299
  });
2299
2300
 
2300
- style._layers.background.fire(new Event('error', {mapLibre: true}));
2301
+ style._layers.background.fire(new ErrorEvent(new Error('test'), {mapLibre: true}));
2301
2302
 
2302
2303
  const e = await errorPromise;
2303
2304
  expect((e as any).layer).toEqual({id: 'background'});
@@ -2614,7 +2615,7 @@ describe('Style.removeLayer', () => {
2614
2615
  // Bind a listener to prevent fallback Evented error reporting.
2615
2616
  layer.on('error', () => {});
2616
2617
 
2617
- layer.fire(new Event('error', {mapLibre: true}));
2618
+ layer.fire(new ErrorEvent(new Error('test'), {mapLibre: true}));
2618
2619
 
2619
2620
  expect(styleErrorListener).not.toHaveBeenCalled();
2620
2621
  });
@@ -27,6 +27,7 @@ export type StyleImageData = {
27
27
  data: RGBAImage;
28
28
  version?: number;
29
29
  hasRenderCallback?: boolean;
30
+ isWebGLImage?: boolean;
30
31
  userImage?: StyleImageInterface;
31
32
  spriteData?: SpriteOnDemandStyleImage;
32
33
  };
@@ -91,6 +92,48 @@ export type StyleImageMetadata = {
91
92
  */
92
93
  export type StyleImage = StyleImageData & StyleImageMetadata;
93
94
 
95
+ /**
96
+ * Where a {@link StyleImageWebGLData.renderWithWebGL} callback writes its pixels.
97
+ */
98
+ export type StyleImageWebGLTarget = {
99
+ gl: WebGL2RenderingContext;
100
+ /**
101
+ * The icon atlas to write into. MapLibre does not bind it for you, so start with
102
+ * `gl.bindTexture(gl.TEXTURE_2D, texture)` or attach it to your own framebuffer.
103
+ */
104
+ texture: WebGLTexture;
105
+ x: number;
106
+ y: number;
107
+ width: number;
108
+ height: number;
109
+ };
110
+
111
+ /**
112
+ * What a {@link StyleImageInterface} gives as its `data` when it renders itself with WebGL rather
113
+ * than handing over an array of pixels.
114
+ *
115
+ * @see [Animate an icon on the GPU.](https://maplibre.org/maplibre-gl-js/docs/examples/animate-an-icon-on-the-gpu/)
116
+ */
117
+ export type StyleImageWebGLData = {
118
+ /**
119
+ * Render exactly `width` x `height` premultiplied-alpha pixels at (`x`, `y`) of
120
+ * `target.texture`. That rectangle is the only part of the shared atlas that belongs to this
121
+ * image; drawing outside it corrupts the others.
122
+ *
123
+ * This is the image's counterpart to {@link CustomLayerInterface.render}, and the context
124
+ * arrives in the same state a custom layer's is given: cull face, active texture and the pixel
125
+ * store settings at their WebGL defaults, and no vertex array bound. Everything is yours to
126
+ * change, and MapLibre restores its own state afterwards. The scissor test is the one
127
+ * exception: MapLibre never touches it, so an image that enables it has to disable it again.
128
+ *
129
+ * Called before the first frame the image is used in, again whenever
130
+ * {@link StyleImageInterface.render} returns `true`, and again for each atlas holding a slot
131
+ * this image has never rendered into, so one change may mean several calls with different
132
+ * targets.
133
+ */
134
+ renderWithWebGL: (target: StyleImageWebGLTarget) => void;
135
+ };
136
+
94
137
  /**
95
138
  * Interface for dynamically generated style images. This is a specification for
96
139
  * implementers to model: it is not an exported method or class.
@@ -148,7 +191,13 @@ export type StyleImage = StyleImageData & StyleImageMetadata;
148
191
  export interface StyleImageInterface {
149
192
  width: number;
150
193
  height: number;
151
- data: Uint8Array | Uint8ClampedArray;
194
+ /**
195
+ * The image's pixels, in the same format as `ImageData`, or a {@link StyleImageWebGLData}
196
+ * callback that renders them with WebGL. A WebGL image renders straight into its slot of the
197
+ * shared icon atlas. Nothing new is possible that pixels could not express, but an image that
198
+ * changes often, such as an animated icon, gets much cheaper: no CPU pixel work and no upload.
199
+ */
200
+ data: Uint8Array | Uint8ClampedArray | StyleImageWebGLData;
152
201
  /**
153
202
  * This method is called once before every frame where the icon will be used.
154
203
  * The method can optionally update the image's `data` member with a new image.
@@ -156,6 +205,9 @@ export interface StyleImageInterface {
156
205
  * If the method updates the image it must return `true` to commit the change.
157
206
  * If the method returns `false` or nothing the image is assumed to not have changed.
158
207
  *
208
+ * An image whose `data` renders with WebGL has nothing to update here; returning `true` is how
209
+ * it asks for {@link StyleImageWebGLData.renderWithWebGL} to be called again.
210
+ *
159
211
  * If updates are infrequent it maybe easier to use {@link Map.updateImage} to update
160
212
  * the image instead of implementing this method.
161
213
  *
@@ -171,18 +223,24 @@ export interface StyleImageInterface {
171
223
  /**
172
224
  * Optional method called when the icon is removed from the map with {@link Map.removeImage}.
173
225
  * This gives the image a chance to clean up resources and event listeners.
226
+ *
227
+ * This also fires when the WebGL context is lost, after which the same image is added back
228
+ * without a matching `onAdd`, so the image has to be able to build again whatever it
229
+ * released here.
174
230
  */
175
231
  onRemove?: () => void;
176
232
  }
177
233
 
234
+ export function isStyleImageWebGLData(data: StyleImageInterface['data']): data is StyleImageWebGLData {
235
+ return typeof (data as StyleImageWebGLData)?.renderWithWebGL === 'function';
236
+ }
237
+
178
238
  export function renderStyleImage(image: StyleImage): boolean {
179
239
  const {userImage} = image;
180
- if (userImage?.render) {
181
- const updated = userImage.render();
182
- if (updated) {
183
- image.data.replace(new Uint8Array(userImage.data.buffer));
184
- return true;
185
- }
186
- }
187
- return false;
240
+ if (!userImage?.render) return false;
241
+ const updated = userImage.render();
242
+ if (!updated) return false;
243
+
244
+ if (!isStyleImageWebGLData(userImage.data)) image.data.replace(new Uint8Array(userImage.data.buffer));
245
+ return true;
188
246
  }
@@ -0,0 +1,65 @@
1
+ import {bench, describe} from 'vitest';
2
+ import {readFileSync} from 'fs';
3
+ import {VectorTile} from '@mapbox/vector-tile';
4
+ import {PbfReader} from 'pbf';
5
+ import {featureFilter} from '@maplibre/maplibre-gl-style-spec';
6
+ import filters from '../../test/bench/data/filters.json' with {type: 'json'};
7
+
8
+ import type {VectorTileFeature} from '@mapbox/vector-tile';
9
+ import type {FeatureFilter, FilterSpecification} from '@maplibre/maplibre-gl-style-spec';
10
+
11
+ type FilteredLayer = {
12
+ features: VectorTileFeature[];
13
+ filters: FeatureFilter[];
14
+ };
15
+
16
+ function readLayers(): FilteredLayer[] {
17
+ const data = readFileSync(new URL('../../test/bench/data/785.vector.pbf', import.meta.url));
18
+ const tile = new VectorTile(new PbfReader(new Uint8Array(data)));
19
+ const layers: FilteredLayer[] = [];
20
+
21
+ for (const name in tile.layers) {
22
+ const layer = tile.layers[name];
23
+ if (!layer.length) {
24
+ continue;
25
+ }
26
+
27
+ const features = [];
28
+ for (let i = 0; i < layer.length; i++) {
29
+ features.push(layer.feature(i));
30
+ }
31
+
32
+ const layerFilters = [];
33
+ for (const filter of filters) {
34
+ if (filter.layer === name) {
35
+ layerFilters.push(featureFilter(filter.filter as FilterSpecification, 'filter'));
36
+ }
37
+ }
38
+
39
+ layers.push({features, filters: layerFilters});
40
+ }
41
+
42
+ return layers;
43
+ }
44
+
45
+ const layers = readLayers();
46
+
47
+ describe('featureFilter', () => {
48
+ bench('create', () => {
49
+ for (const filter of filters) {
50
+ featureFilter(filter.filter as FilterSpecification, 'filter');
51
+ }
52
+ });
53
+
54
+ bench('evaluate', () => {
55
+ for (const layer of layers) {
56
+ for (const filter of layer.filters) {
57
+ for (const feature of layer.features) {
58
+ if (typeof filter.filter({zoom: 0}, feature) !== 'boolean') {
59
+ throw new Error('Expected boolean result from filter');
60
+ }
61
+ }
62
+ }
63
+ }
64
+ });
65
+ });
@@ -13,7 +13,7 @@ describe('StyleLayer', () => {
13
13
  test('instantiates the correct subclass', () => {
14
14
  const layer = createStyleLayer({type: 'fill'} as LayerSpecification, {});
15
15
 
16
- expect(layer instanceof FillStyleLayer).toBeTruthy();
16
+ expect(layer).toBeInstanceOf(FillStyleLayer);
17
17
  });
18
18
  });
19
19
 
@@ -2,7 +2,7 @@ import {filterObject} from '../util/util.ts';
2
2
 
3
3
  import {createVisibilityExpression, featureFilter, supportsPropertyExpression} from '@maplibre/maplibre-gl-style-spec';
4
4
  import {validateStyle, validateAndEmit, type Validator} from './validate_style.ts';
5
- import {Evented, ErrorEvent} from '../util/evented.ts';
5
+ import {Evented, ErrorEvent, type ErrorEventType} from '../util/evented.ts';
6
6
  import {Layout, Transitionable, type Transitioning, type Properties, PossiblyEvaluated, PossiblyEvaluatedPropertyValue, TRANSITION_SUFFIX} from './properties.ts';
7
7
 
8
8
  import type {Bucket, BucketParameters} from '../data/bucket.ts';
@@ -83,7 +83,7 @@ const ERROR_LAYOUT_NOT_PAINT = ' is a LAYOUT property not a PAINT property. Use
83
83
  /**
84
84
  * A base class for style layers
85
85
  */
86
- export abstract class StyleLayer extends Evented {
86
+ export abstract class StyleLayer extends Evented<ErrorEventType> {
87
87
  id: string;
88
88
  metadata: unknown;
89
89
  type: LayerSpecification['type'] | CustomLayerInterface['type'];
@@ -0,0 +1,11 @@
1
+ import {bench} from 'vitest';
2
+ import {validateStyle} from './validate_style.ts';
3
+ import brightV9 from '../../test/integration/assets/styles/bright-v9.json' with {type: 'json'};
4
+
5
+ import type {StyleSpecification} from '@maplibre/maplibre-gl-style-spec';
6
+
7
+ const style = brightV9 as unknown as StyleSpecification;
8
+
9
+ bench('validateStyle', () => {
10
+ validateStyle(style);
11
+ });
@@ -1,9 +1,9 @@
1
1
  import {describe, test, expect, vi, afterEach} from 'vitest';
2
2
  import {validateAndEmit, validateStyle, validateStyleAndEmit} from './validate_style.ts';
3
- import {Evented} from '../util/evented.ts';
3
+ import {Evented, type ErrorEventType} from '../util/evented.ts';
4
4
  import type {StyleSpecification} from '@maplibre/maplibre-gl-style-spec';
5
5
 
6
- class TestEmitter extends Evented {}
6
+ class TestEmitter extends Evented<ErrorEventType> {}
7
7
 
8
8
  afterEach(() => {
9
9
  vi.restoreAllMocks();
@@ -3,8 +3,8 @@ import {Anchor} from './anchor.ts';
3
3
 
4
4
  describe('Anchor', () => {
5
5
  test('constructor', () => {
6
- expect(new Anchor(0, 0, 0) instanceof Anchor).toBeTruthy();
7
- expect(new Anchor(0, 0, 0, 0) instanceof Anchor).toBeTruthy();
6
+ expect(new Anchor(0, 0, 0)).toBeInstanceOf(Anchor);
7
+ expect(new Anchor(0, 0, 0, 0)).toBeInstanceOf(Anchor);
8
8
  });
9
9
  test('clone', () => {
10
10
  const a = new Anchor(1, 2, 3);
@@ -0,0 +1,116 @@
1
+ import {bench, describe} from 'vitest';
2
+ import Point from '@mapbox/point-geometry';
3
+ import {CollisionIndex} from './collision_index.ts';
4
+ import {EXTENT} from '../data/extent.ts';
5
+ import {OverscaledTileID} from '../tile/tile_id.ts';
6
+ import {MercatorTransform} from '../geo/projection/mercator_transform.ts';
7
+ import {GlobeTransform} from '../geo/projection/globe_transform.ts';
8
+
9
+ import type {mat4} from 'gl-matrix';
10
+ import type {ITransform} from '../geo/transform_interface.ts';
11
+ import type {UnwrappedTileID} from '../tile/tile_id.ts';
12
+ import type {SingleCollisionBox} from '../data/bucket/symbol_bucket.ts';
13
+ import type {OverlapMode} from '../style/style_layer/overlap_mode.ts';
14
+
15
+ type TestSymbol = {
16
+ collisionBox: SingleCollisionBox;
17
+ overlapMode: OverlapMode;
18
+ textPixelRatio: number;
19
+ tileID: OverscaledTileID;
20
+ unwrappedTileID: UnwrappedTileID;
21
+ pitchWithMap: boolean;
22
+ rotateWithMap: boolean;
23
+ translation: [number, number];
24
+ shift?: Point;
25
+ simpleProjectionMatrix?: mat4;
26
+ };
27
+
28
+ // https://stackoverflow.com/a/47593316
29
+ function splitmix32(a: number): () => number {
30
+ return function() {
31
+ a |= 0;
32
+ a = a + 0x9e3779b9 | 0;
33
+ let t = a ^ a >>> 16;
34
+ t = Math.imul(t, 0x21f0aaad);
35
+ t = t ^ t >>> 15;
36
+ t = Math.imul(t, 0x735a2d97);
37
+ return ((t = t ^ t >>> 15) >>> 0) / 4294967296;
38
+ };
39
+ }
40
+
41
+ const symbolCount = 20000;
42
+
43
+ function createSymbols(transform: ITransform, calculatePosMatrix: (tileID: UnwrappedTileID) => mat4 | undefined): TestSymbol[] {
44
+ transform.resize(1024, 1024, true);
45
+
46
+ const tileID = new OverscaledTileID(0, 0, 0, 0, 0);
47
+ const unwrappedTileID = tileID.toUnwrapped();
48
+ const rng = splitmix32(0xdeadbeef);
49
+ const rndRange = (min: number, max: number) => rng() * (max - min) + min;
50
+ const symbols: TestSymbol[] = [];
51
+
52
+ for (let i = 0; i < symbolCount; i++) {
53
+ symbols.push({
54
+ collisionBox: {
55
+ anchorPointX: rndRange(4, EXTENT - 4),
56
+ anchorPointY: rndRange(4, EXTENT - 4),
57
+ x1: rndRange(-20, -2),
58
+ y1: rndRange(-20, -2),
59
+ x2: rndRange(2, 20),
60
+ y2: rndRange(2, 20)
61
+ },
62
+ overlapMode: 'never',
63
+ textPixelRatio: 1,
64
+ tileID,
65
+ unwrappedTileID,
66
+ pitchWithMap: rng() > 0.5,
67
+ rotateWithMap: rng() > 0.5,
68
+ translation: [
69
+ rndRange(-20, 20),
70
+ rndRange(-20, 20)
71
+ ],
72
+ shift: rng() > 0.5 ? new Point(rndRange(-20, 20), rndRange(-20, 20)) : undefined,
73
+ simpleProjectionMatrix: calculatePosMatrix(unwrappedTileID),
74
+ });
75
+ }
76
+
77
+ return symbols;
78
+ }
79
+
80
+ function placeAll(transform: ITransform, symbols: TestSymbol[]): void {
81
+ const collisionIndex = new CollisionIndex(transform);
82
+ collisionIndex.grid.hitTest = () => true;
83
+
84
+ for (const symbol of symbols) {
85
+ collisionIndex.placeCollisionBox(
86
+ symbol.collisionBox,
87
+ symbol.overlapMode,
88
+ symbol.textPixelRatio,
89
+ symbol.tileID,
90
+ symbol.unwrappedTileID,
91
+ symbol.pitchWithMap,
92
+ symbol.rotateWithMap,
93
+ symbol.translation,
94
+ null,
95
+ null,
96
+ symbol.shift,
97
+ symbol.simpleProjectionMatrix,
98
+ );
99
+ }
100
+ }
101
+
102
+ const mercatorTransform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
103
+ const mercatorSymbols = createSymbols(mercatorTransform, (tileID) => mercatorTransform.calculatePosMatrix(tileID, false));
104
+
105
+ const globeTransform = new GlobeTransform();
106
+ const globeSymbols = createSymbols(globeTransform, () => undefined);
107
+
108
+ describe('placeCollisionBox', () => {
109
+ bench('mercator', () => {
110
+ placeAll(mercatorTransform, mercatorSymbols);
111
+ });
112
+
113
+ bench('globe', () => {
114
+ placeAll(globeTransform, globeSymbols);
115
+ });
116
+ });
@@ -0,0 +1,49 @@
1
+ import {bench} from 'vitest';
2
+ import {CrossTileSymbolIndex} from './cross_tile_symbol_index.ts';
3
+ import {OverscaledTileID} from '../tile/tile_id.ts';
4
+
5
+ import type {Tile} from '../tile/tile.ts';
6
+ import type {StyleLayer} from '../style/style_layer.ts';
7
+
8
+ type TestSymbolInstance = {
9
+ anchorX: number;
10
+ anchorY: number;
11
+ key: string;
12
+ crossTileID: number;
13
+ };
14
+
15
+ const styleLayer = {id: 'test'} as StyleLayer;
16
+ const symbolCount = 3000;
17
+
18
+ function createTile(tileID: OverscaledTileID, symbolInstances: TestSymbolInstance[]): Tile {
19
+ const bucket = {
20
+ symbolInstances: {
21
+ get(i: number) { return symbolInstances[i]; },
22
+ length: symbolInstances.length
23
+ },
24
+ layerIds: ['test']
25
+ };
26
+ return {
27
+ tileID,
28
+ getBucket: () => bucket,
29
+ latestFeatureIndex: {}
30
+ } as unknown as Tile;
31
+ }
32
+
33
+ function createInstances(): TestSymbolInstance[] {
34
+ return Array.from({length: symbolCount}, (_, i) => ({
35
+ anchorX: 0,
36
+ anchorY: 0,
37
+ key: '',
38
+ crossTileID: i + 1
39
+ }));
40
+ }
41
+
42
+ const mainTile = createTile(new OverscaledTileID(6, 0, 6, 8, 8), createInstances());
43
+ const childTile = createTile(new OverscaledTileID(7, 0, 7, 16, 16), createInstances());
44
+
45
+ bench('CrossTileSymbolIndex.addLayer', () => {
46
+ const index = new CrossTileSymbolIndex();
47
+ index.addLayer(styleLayer, [mainTile], 0);
48
+ index.addLayer(styleLayer, [childTile], 0);
49
+ });