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
@@ -5,6 +5,7 @@ import {isRasterStyleLayer} from './style_layer/raster_style_layer.ts';
5
5
  import {createStyleLayer} from './create_style_layer.ts';
6
6
  import {loadSprite} from './load_sprite.ts';
7
7
  import {ImageManager, type MissingImageRequestHandler} from '../render/image_manager.ts';
8
+ import {PatternAtlas} from '../render/pattern_atlas.ts';
8
9
  import {GlyphManager} from '../render/glyph_manager.ts';
9
10
  import {Light} from './light.ts';
10
11
  import {Sky} from './sky.ts';
@@ -205,6 +206,7 @@ export class Style extends Evented<MapEventType> {
205
206
  stylesheet: StyleSpecification;
206
207
  dispatcher: Dispatcher;
207
208
  imageManager: ImageManager;
209
+ patternAtlas: PatternAtlas;
208
210
  glyphManager: GlyphManager;
209
211
  lineAtlas: LineAtlas;
210
212
  light: Light;
@@ -229,10 +231,6 @@ export class Style extends Evented<MapEventType> {
229
231
  _glyphsDidChange: boolean;
230
232
  _updatedPaintProps: {[layer: string]: true};
231
233
  _layerOrderChanged: boolean;
232
- // image ids of images loaded from style's sprite
233
- _spritesImagesIds: {[spriteId: string]: string[]};
234
- // image ids of all images loaded (sprite + user)
235
- _availableImages: string[];
236
234
  _globalState: Record<string, any>;
237
235
  crossTileSymbolIndex: CrossTileSymbolIndex;
238
236
  pauseablePlacement: PauseablePlacement;
@@ -256,6 +254,7 @@ export class Style extends Evented<MapEventType> {
256
254
  this.imageManager = new ImageManager();
257
255
  this.imageManager.setEventedParent(this);
258
256
  this.imageManager.setMissingImageResolver(map._missingStyleImageResolver);
257
+ this.patternAtlas = new PatternAtlas(this.imageManager);
259
258
  const glyphLang = map._container?.lang || (typeof document !== 'undefined' && document.documentElement?.lang) || undefined;
260
259
  this.glyphManager = new GlyphManager(map._requestManager, options.localIdeographFontFamily, glyphLang);
261
260
  this.lineAtlas = new LineAtlas(256, 512);
@@ -293,12 +292,10 @@ export class Style extends Evented<MapEventType> {
293
292
  }
294
293
 
295
294
  private _setInitialValues() {
296
- this._spritesImagesIds = {};
297
295
  this._layers = {};
298
296
  this._order = [];
299
297
  this.tileManagers = {};
300
298
  this.zoomHistory = new ZoomHistory();
301
- this._availableImages = [];
302
299
  this._imagesListDirty = false;
303
300
  this._globalState = {};
304
301
  this._serializedLayers = {};
@@ -529,73 +526,46 @@ export class Style extends Evented<MapEventType> {
529
526
  }
530
527
  }
531
528
 
532
- _loadSprite(sprite: SpriteSpecification, isUpdate: boolean = false, completion: (err: Error) => void = undefined): void {
529
+ async _loadSprite(sprite: SpriteSpecification, isUpdate: boolean = false, completion: (err: Error) => void = undefined): Promise<void> {
533
530
  this.imageManager.setLoaded(false);
534
531
 
535
532
  const abortController = new AbortController();
536
533
  this._spriteRequest = abortController;
537
534
  let err: Error;
538
- loadSprite(sprite, this.map._requestManager, this.map.getPixelRatio(), this._spriteRequest).then((images) => {
539
- this._spriteRequest = null;
540
- if (images) {
541
- for (const spriteId in images) {
542
- this._spritesImagesIds[spriteId] = [];
543
-
544
- // remove old sprite's loaded images (for the same sprite id) that are not in new sprite
545
- const imagesToRemove = this._spritesImagesIds[spriteId] ? this._spritesImagesIds[spriteId].filter(id => !(id in images)) : [];
546
- for (const id of imagesToRemove) {
547
- this.imageManager.removeImage(id);
548
- this._changedImages[id] = true;
549
- }
550
-
551
- for (const id in images[spriteId]) {
552
- // don't prefix images of the "default" sprite
553
- const imageId = spriteId === 'default' ? id : `${spriteId}:${id}`;
554
- // save all the sprite's images' ids to be able to delete them in `removeSprite`
555
- this._spritesImagesIds[spriteId].push(imageId);
556
- if (imageId in this.imageManager.images) {
557
- this.imageManager.updateImage(imageId, images[spriteId][id], false);
558
- } else {
559
- this.imageManager.addImage(imageId, images[spriteId][id]);
560
- }
561
-
562
- if (isUpdate) {
563
- this._changedImages[imageId] = true;
564
- }
565
- }
535
+ try {
536
+ const images = await loadSprite(sprite, this.map._requestManager, this.map.getPixelRatio(), abortController);
537
+ if (!images) return;
538
+
539
+ for (const spriteId in images) {
540
+ const {loaded, removed} = this.imageManager.setSpriteImages(spriteId, images[spriteId]);
541
+ this._markImagesChanged(removed);
542
+ if (isUpdate) {
543
+ this._markImagesChanged(loaded);
566
544
  }
567
545
  }
568
- }).catch((error) => {
569
- this._spriteRequest = null;
546
+ } catch (error) {
570
547
  err = error;
571
548
  if (!abortController.signal.aborted) { // ignore abort
572
549
  this.fire(new ErrorEvent(err));
573
550
  }
574
- }).finally(() => {
551
+ } finally {
552
+ this._spriteRequest = null;
575
553
  this.imageManager.setLoaded(true);
576
- this._availableImages = this.imageManager.listImages();
577
554
 
578
555
  if (isUpdate) {
579
556
  this._changed = true;
580
557
  }
581
558
 
582
- this.dispatcher.broadcast(MessageType.setImages, this._availableImages);
559
+ this.dispatcher.broadcast(MessageType.setImages, this.imageManager.listImages());
583
560
  this.fire(new MapStyleDataEvent('data'));
584
561
 
585
- if (completion) {
586
- completion(err);
587
- }
588
- });
562
+ completion?.(err);
563
+ }
589
564
  }
590
565
 
591
566
  _unloadSprite(): void {
592
- for (const id of Object.values(this._spritesImagesIds).flat()) {
593
- this.imageManager.removeImage(id);
594
- this._changedImages[id] = true;
595
- }
567
+ this._markImagesChanged(this.imageManager.removeAllSpriteImages());
596
568
 
597
- this._spritesImagesIds = {};
598
- this._availableImages = this.imageManager.listImages();
599
569
  this._imagesListDirty = true;
600
570
  this._changed = true;
601
571
  this.fire(new MapStyleDataEvent('data'));
@@ -731,7 +701,7 @@ export class Style extends Evented<MapEventType> {
731
701
  const changed = this._changed;
732
702
  if (changed) {
733
703
  if (this._imagesListDirty) {
734
- this.dispatcher.broadcast(MessageType.setImages, this._availableImages);
704
+ this.dispatcher.broadcast(MessageType.setImages, this.imageManager.listImages());
735
705
  this._imagesListDirty = false;
736
706
  }
737
707
 
@@ -779,10 +749,11 @@ export class Style extends Evented<MapEventType> {
779
749
 
780
750
  // loop all layers and find layers that are not hidden at parameters.zoom
781
751
  // and set used to true in tileManagers dictionary for the sources of these layers
752
+ const availableImages = this.imageManager.listImages();
782
753
  for (const layerId of this._order) {
783
754
  const layer = this._layers[layerId];
784
755
 
785
- layer.recalculate(parameters, this._availableImages);
756
+ layer.recalculate(parameters, availableImages);
786
757
  if (!layer.isHidden(parameters.zoom) && layer.source) {
787
758
  this.tileManagers[layer.source].used = true;
788
759
  }
@@ -1005,8 +976,17 @@ export class Style extends Evented<MapEventType> {
1005
976
  this._afterImageUpdated(id);
1006
977
  }
1007
978
 
979
+ /**
980
+ * Queues the tiles that depend on these images to be reloaded on the next update, which is
981
+ * needed whenever an image appears, disappears or changes size.
982
+ */
983
+ _markImagesChanged(ids: string[]): void {
984
+ for (const id of ids) {
985
+ this._changedImages[id] = true;
986
+ }
987
+ }
988
+
1008
989
  _afterImageUpdated(id: string): void {
1009
- this._availableImages = this.imageManager.listImages();
1010
990
  this._changedImages[id] = true;
1011
991
  this._imagesListDirty = true;
1012
992
  this._changed = true;
@@ -1616,7 +1596,7 @@ export class Style extends Evented<MapEventType> {
1616
1596
 
1617
1597
  const sourceResults: QueryRenderedFeaturesResults[] = [];
1618
1598
 
1619
- params.availableImages = this._availableImages;
1599
+ params.availableImages = this.imageManager.listImages();
1620
1600
 
1621
1601
  // LayerSpecification is serialized StyleLayer, and this casting is safe.
1622
1602
  const serializedLayers = this._serializedAllLayers() as {[_: string]: StyleLayer};
@@ -2000,18 +1980,12 @@ export class Style extends Evented<MapEventType> {
2000
1980
  return;
2001
1981
  }
2002
1982
 
2003
- if (this._spritesImagesIds[id]) {
2004
- for (const imageId of this._spritesImagesIds[id]) {
2005
- this.imageManager.removeImage(imageId);
2006
- this._changedImages[imageId] = true;
2007
- }
2008
- }
1983
+ const changedImages = this.imageManager.removeSpriteImages(id);
1984
+ this._markImagesChanged(changedImages);
2009
1985
 
2010
1986
  internalSpriteRepresentation.splice(internalSpriteRepresentation.findIndex(sprite => sprite.id === id), 1);
2011
1987
  this.stylesheet.sprite = internalSpriteRepresentation.length > 0 ? internalSpriteRepresentation : undefined;
2012
1988
 
2013
- delete this._spritesImagesIds[id];
2014
- this._availableImages = this.imageManager.listImages();
2015
1989
  this._imagesListDirty = true;
2016
1990
  this._changed = true;
2017
1991
  this.fire(new MapStyleDataEvent('data'));
@@ -2082,8 +2056,7 @@ export class Style extends Evented<MapEventType> {
2082
2056
  if (this.imageManager) {
2083
2057
  this.imageManager.setEventedParent(null);
2084
2058
  this.imageManager.destroy();
2085
- this._availableImages = [];
2086
- this._spritesImagesIds = {};
2059
+ this.patternAtlas.destroy();
2087
2060
  }
2088
2061
 
2089
2062
  // Destroy glyphManager
@@ -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
+ });
@@ -236,6 +236,37 @@ describe('CrossTileSymbolIndex.addLayer', () => {
236
236
  expect(childInstances[0].crossTileID).toBe(1);
237
237
 
238
238
  });
239
+
240
+ test('claims the lowest-index match among coincident symbols', () => {
241
+ const index = new CrossTileSymbolIndex();
242
+
243
+ const mainID = new OverscaledTileID(6, 0, 6, 8, 8);
244
+ const childID = new OverscaledTileID(7, 0, 7, 16, 16);
245
+
246
+ // more instances than KDBUSH_THRESHHOLD to exercise the KDBush-indexed path
247
+ const count = KDBUSH_THRESHHOLD + 2;
248
+ const mainInstances: any[] = [];
249
+ const childInstances: any[] = [];
250
+ for (let i = 0; i < count; i++) {
251
+ // all symbols are coincident and share the same key
252
+ mainInstances.push(makeSymbolInstance(1000, 1000, 'Dense'));
253
+ childInstances.push(makeSymbolInstance(2000, 2000, 'Dense'));
254
+ }
255
+
256
+ const mainTile = makeTile(mainID, mainInstances);
257
+ const childTile = makeTile(childID, childInstances);
258
+
259
+ index.addLayer(styleLayer, [mainTile], 0);
260
+ index.addLayer(styleLayer, [mainTile, childTile], 0);
261
+
262
+ // Each child symbol should claim the lowest-index unclaimed parent
263
+ // symbol, so parents are claimed in symbol-instance order. (A
264
+ // lexicographic ordering of the candidate indexes would instead claim
265
+ // parent 10 for child 2 once 0 and 1 are taken, because '10' < '2'.)
266
+ for (let i = 0; i < 12; i++) {
267
+ expect(childInstances[i].crossTileID).toBe(mainInstances[i].crossTileID);
268
+ }
269
+ });
239
270
  });
240
271
 
241
272
  test('CrossTileSymbolIndex.pruneUnusedLayers', () => {