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
@@ -31,6 +31,12 @@ import type {Terrain} from '../render/terrain.ts';
31
31
 
32
32
  const isMoving = (p: EventsInProgress) => p.zoom || p.drag || p.roll || p.pitch || p.rotate;
33
33
 
34
+ /**
35
+ * A terrain gesture's anchor plane must stay below this fraction of the camera's
36
+ * altitude over the center-elevation plane, or the ray-plane solve degenerates.
37
+ */
38
+ const TERRAIN_ANCHOR_MAX_CAMERA_ALTITUDE_FRACTION = 0.9;
39
+
34
40
  class RenderFrameEvent extends Event {
35
41
  type: 'renderFrame';
36
42
  timeStamp: number;
@@ -162,6 +168,11 @@ export class HandlerManager {
162
168
  _updatingCamera: boolean;
163
169
  _changes: Array<[HandlerResult, EventsInProgress, {[handlerName: string]: Event}]>;
164
170
  _terrainMovement: boolean;
171
+ /**
172
+ * Elevation in meters of the terrain point grabbed at gesture start; kept for the
173
+ * same lifetime as the elevation freeze. Null when that terrain was not available.
174
+ */
175
+ _terrainGestureAnchorElevation: number | null = null;
165
176
  _zoom: {handlerName: string};
166
177
  _previousActiveHandlers: {[x: string]: Handler};
167
178
  _listeners: Array<[Window | Document | HTMLElement, string, {
@@ -542,17 +553,10 @@ export class HandlerManager {
542
553
  // stop any ongoing camera animations (easeTo, flyTo)
543
554
  this._camera.stop(true);
544
555
 
545
- let {panDelta, zoomDelta, bearingDelta, pitchDelta, rollDelta, around, pinchAround} = combinedResult;
546
-
547
- if (pinchAround !== undefined) {
548
- around = pinchAround;
549
- }
550
-
551
- around ||= this._camera.transform.centerPoint;
556
+ const {panDelta, zoomDelta, bearingDelta, pitchDelta, rollDelta} = combinedResult;
552
557
 
553
- if (terrain && !tr.isPointOnMapSurface(around)) {
554
- around = tr.centerPoint;
555
- }
558
+ let {around, aroundOnSurface} = this._resolveAround(combinedResult, terrain, tr);
559
+ const aroundElevation = terrain ? this._terrainGestureElevation(terrain, around, aroundOnSurface, tr, combinedEventsInProgress) : undefined;
556
560
 
557
561
  const deltasForHelper: MapControlsDeltas = {
558
562
  panDelta,
@@ -561,17 +565,14 @@ export class HandlerManager {
561
565
  pitchDelta,
562
566
  bearingDelta,
563
567
  around,
568
+ aroundElevation,
564
569
  };
565
570
 
566
571
  // Pre-zoom location under the mouse cursor is required for accurate mercator panning and zooming
567
572
  if (this._camera.cameraHelper.useGlobeControls && !tr.isPointOnMapSurface(around)) {
568
573
  around = tr.centerPoint;
569
574
  }
570
- // If we are rotating about the center point, avoid numerical issues near the horizon by using the transform's
571
- // center directly, instead of computing it from the screen point
572
- const preZoomAroundLoc = around.distSqr(tr.centerPoint) < 1.0e-2 ?
573
- tr.center :
574
- tr.screenPointToLocation(panDelta ? around.sub(panDelta) : around);
575
+ const preZoomAroundLoc = this._computePreZoomAroundLoc(tr, around, panDelta, aroundElevation);
575
576
 
576
577
  this._handleMapControls({
577
578
  terrain,
@@ -590,6 +591,65 @@ export class HandlerManager {
590
591
 
591
592
  }
592
593
 
594
+ /**
595
+ * The gesture's anchor point: the pinch midpoint when pinching, otherwise the
596
+ * pan's anchor, clamped to the center point when it does not lie on the map.
597
+ */
598
+ _resolveAround(combinedResult: HandlerResult, terrain: Terrain | null, tr: ITransform): {around: Point; aroundOnSurface: boolean} {
599
+ let around = combinedResult.pinchAround !== undefined ? combinedResult.pinchAround : combinedResult.around;
600
+ around ||= this._camera.transform.centerPoint;
601
+ if (terrain && !tr.isPointOnMapSurface(around)) {
602
+ return {around: tr.centerPoint, aroundOnSurface: false};
603
+ }
604
+ return {around, aroundOnSurface: true};
605
+ }
606
+
607
+ /**
608
+ * The elevation of the plane a terrain gesture is solved on: the elevation of the
609
+ * terrain point grabbed at gesture start, captured here on the gesture's first
610
+ * frame. Undefined means the gesture is solved at the center's elevation instead —
611
+ * when the grabbed terrain is not loaded, the anchor is the center point (a
612
+ * center-point anchor is skipped by the camera helper and would lose the pan), or
613
+ * the anchor plane is too close to the camera for a stable ray-plane solve.
614
+ */
615
+ _terrainGestureElevation(terrain: Terrain, around: Point, aroundOnSurface: boolean, tr: ITransform, combinedEventsInProgress: EventsInProgress): number | undefined {
616
+ if (!aroundOnSurface) {
617
+ return undefined;
618
+ }
619
+ if (!this._terrainMovement && (combinedEventsInProgress.drag || combinedEventsInProgress.zoom)) {
620
+ const anchor = terrain.pointCoordinate(around);
621
+ this._terrainGestureAnchorElevation = anchor ? anchor.z : null;
622
+ }
623
+ if (this._terrainGestureAnchorElevation === null) {
624
+ return undefined;
625
+ }
626
+ const elevation = this._terrainGestureAnchorElevation;
627
+ if (around.distSqr(tr.centerPoint) < 1.0e-2) {
628
+ return undefined;
629
+ }
630
+ if (elevation - tr.elevation >= TERRAIN_ANCHOR_MAX_CAMERA_ALTITUDE_FRACTION * (tr.getCameraAltitude() - tr.elevation)) {
631
+ return undefined;
632
+ }
633
+ return elevation;
634
+ }
635
+
636
+ /**
637
+ * The location that was under `around` before this frame's deltas — the point the
638
+ * camera helper re-anchors after zooming — solved at `aroundElevation` when a
639
+ * terrain gesture provides one. When rotating about the center point, the
640
+ * transform's center is used directly to avoid numerical issues near the horizon.
641
+ */
642
+ _computePreZoomAroundLoc(tr: ITransform, around: Point, panDelta: Point | undefined, aroundElevation: number | undefined): LngLat {
643
+ if (around.distSqr(tr.centerPoint) < 1.0e-2) {
644
+ return tr.center;
645
+ }
646
+ const aroundPreviousPoint = panDelta ? around.sub(panDelta) : around;
647
+ if (aroundElevation !== undefined) {
648
+ return tr.screenPointToLocationAtElevation(aroundPreviousPoint, aroundElevation);
649
+ }
650
+ return tr.screenPointToLocation(aroundPreviousPoint);
651
+ }
652
+
593
653
  _handleMapControls({
594
654
  terrain,
595
655
  tr,
@@ -623,7 +683,9 @@ export class HandlerManager {
623
683
  return;
624
684
  }
625
685
 
626
- if (combinedEventsInProgress.drag && this._terrainMovement && panDelta) {
686
+ if (deltasForHelper.aroundElevation === undefined &&
687
+ combinedEventsInProgress.drag && this._terrainMovement && panDelta) {
688
+ // no usable anchor: drag by pixel-delta on the center-elevation plane
627
689
  tr.setCenter(tr.screenPointToLocation(tr.centerPoint.sub(panDelta)));
628
690
  return;
629
691
  }
@@ -685,6 +747,7 @@ export class HandlerManager {
685
747
  if (finishedMoving && this._terrainMovement) {
686
748
  this._camera.elevationFreeze = false;
687
749
  this._terrainMovement = false;
750
+ this._terrainGestureAnchorElevation = null;
688
751
  const tr = this._camera.getTransformForUpdate();
689
752
  if (this._map.getCenterClampedToGround()) {
690
753
  tr.recalculateZoomAndCenter(this._map.terrain);
package/src/ui/map.ts CHANGED
@@ -31,6 +31,7 @@ import {defaultLocale} from './default_locale.ts';
31
31
  import {isAbortError} from '../util/abort_error.ts';
32
32
  import {coveringTiles, type CoveringTilesOptions, createCalculateTileZoomFunction} from '../geo/projection/covering_tiles.ts';
33
33
  import {CanonicalTileID, type OverscaledTileID} from '../tile/tile_id.ts';
34
+ import {isStyleImageWebGLData} from '../style/style_image.ts';
34
35
 
35
36
  import type {PaddingOptions} from '../geo/edge_insets.ts';
36
37
  import type {Source} from '../source/source.ts';
@@ -3208,9 +3209,11 @@ export class Map extends Evented<MapEventType> {
3208
3209
  } else {
3209
3210
  const {width, height, data} = image as ImageData;
3210
3211
  const userImage = (image as any as StyleImageInterface);
3212
+ const isWebGLImage = isStyleImageWebGLData(userImage.data);
3211
3213
 
3212
3214
  return {
3213
- data: new RGBAImage({width, height}, new Uint8Array(data)),
3215
+ // A WebGL image paints its own slot, so its pixels are only ever transparent padding.
3216
+ data: isWebGLImage ? new RGBAImage({width, height}) : new RGBAImage({width, height}, new Uint8Array(data)),
3214
3217
  pixelRatio,
3215
3218
  stretchX,
3216
3219
  stretchY,
@@ -3219,6 +3222,7 @@ export class Map extends Evented<MapEventType> {
3219
3222
  textFitHeight,
3220
3223
  sdf,
3221
3224
  version,
3225
+ isWebGLImage,
3222
3226
  userImage
3223
3227
  };
3224
3228
  }
@@ -3267,8 +3271,13 @@ export class Map extends Evented<MapEventType> {
3267
3271
  'The width and height of the updated image must be that same as the previous version of the image')));
3268
3272
  }
3269
3273
 
3270
- const copy = !(image instanceof HTMLImageElement || isImageBitmap(image));
3271
- existingImage.data.replace(data, copy);
3274
+ existingImage.isWebGLImage = isStyleImageWebGLData(data);
3275
+ if (existingImage.isWebGLImage) {
3276
+ existingImage.userImage = image as StyleImageInterface;
3277
+ } else {
3278
+ const copy = !(image instanceof HTMLImageElement || isImageBitmap(image));
3279
+ existingImage.data.replace(data as Uint8Array | Uint8ClampedArray, copy);
3280
+ }
3272
3281
 
3273
3282
  this.style.updateImage(id, existingImage);
3274
3283
  return this;
@@ -4146,6 +4155,11 @@ export class Map extends Evented<MapEventType> {
4146
4155
 
4147
4156
  if (this._lostContextStyle.images && this.style) {
4148
4157
  this.style.imageManager.images = this._lostContextStyle.images;
4158
+ // The atlas textures died with the old context, so images that render themselves with WebGL owe every atlas a fresh render.
4159
+ for (const id in this._lostContextStyle.images) {
4160
+ const image = this._lostContextStyle.images[id];
4161
+ if (image.isWebGLImage) this.style.imageManager.updateImage(id, image, false);
4162
+ }
4149
4163
  }
4150
4164
 
4151
4165
  this._lostContextStyle = {style: null, images: null};
@@ -36,8 +36,8 @@ describe('Map cross-window support', () => {
36
36
  expect(iframeDocument).not.toBe(window.document);
37
37
  expect(iframeWindow).not.toBe(window);
38
38
 
39
- expect(container instanceof HTMLElement).toBe(false);
40
- expect(container instanceof iframeDocument.defaultView.HTMLElement).toBe(true);
39
+ expect(container).not.toBeInstanceOf( HTMLElement);
40
+ expect(container).toBeInstanceOf(iframeDocument.defaultView.HTMLElement);
41
41
 
42
42
  const map = new Map({
43
43
  container,
@@ -3,9 +3,9 @@ import simulate from '../../../test/unit/lib/simulate_interaction.ts';
3
3
  import {type StyleLayer} from '../../style/style_layer.ts';
4
4
  import {createMap, beforeMapTest, createStyle, sleep, createTerrain} from '../../util/test/util.ts';
5
5
  import {type MapGeoJSONFeature} from '../../util/vectortile_to_geojson.ts';
6
- import {type MapLibreEvent} from '../events.ts';
6
+ import {type MapLibreEvent, MapSourceDataEvent} from '../events.ts';
7
7
  import {Map} from '../map.ts';
8
- import {Event as EventedEvent, ErrorEvent} from '../../util/evented.ts';
8
+ import {ErrorEvent} from '../../util/evented.ts';
9
9
  import {type StyleSpecification} from '@maplibre/maplibre-gl-style-spec';
10
10
 
11
11
  type IsAny<T> = 0 extends T & 1 ? T : never;
@@ -980,7 +980,7 @@ describe('map events', () => {
980
980
  test('fires sourcedataabort event on dataabort event', async () => {
981
981
  const map = createMap();
982
982
  const sourcePromise = map.once('sourcedataabort');
983
- map.fire(new EventedEvent('dataabort'));
983
+ map.fire(new MapSourceDataEvent('dataabort'));
984
984
  await expect(sourcePromise).resolves.toBeDefined();
985
985
  });
986
986
 
@@ -215,6 +215,35 @@ test('map getImage matches addImage, StyleImageInterface SDF', () => {
215
215
  expect(gotImage.sdf).toBe(true);
216
216
  });
217
217
 
218
+ test('map addImage packs a placeholder for a WebGL image, which brings its own pixels', () => {
219
+ const map = createMap();
220
+ const id = 'add-get-webgl-style-image';
221
+ const onAdd = vi.fn();
222
+ const inputImage: StyleImageInterface = {width: 3, height: 2, data: {renderWithWebGL: vi.fn()}, onAdd};
223
+
224
+ map.addImage(id, inputImage);
225
+
226
+ const gotImage = map.getImage(id);
227
+ expect(gotImage.data.width).toBe(3);
228
+ expect(gotImage.data.height).toBe(2);
229
+ expect([...gotImage.data.data]).toEqual(new Array(3 * 2 * 4).fill(0));
230
+ expect(gotImage.isWebGLImage).toBe(true);
231
+ expect(gotImage.userImage).toBe(inputImage);
232
+ expect(onAdd).toHaveBeenCalledWith(map, id);
233
+ });
234
+
235
+ test('map updateImage swaps a WebGL image\'s renderWithWebGL callback instead of copying pixels', () => {
236
+ const map = createMap();
237
+ const replacement: StyleImageInterface = {width: 1, height: 1, data: {renderWithWebGL: vi.fn()}};
238
+
239
+ map.addImage('webgl', {width: 1, height: 1, data: {renderWithWebGL: vi.fn()}});
240
+ const version = map.getImage('webgl').version;
241
+ map.updateImage('webgl', replacement);
242
+
243
+ expect(map.getImage('webgl').userImage).toBe(replacement);
244
+ expect(map.getImage('webgl').version).toBe(version + 1);
245
+ });
246
+
218
247
  test('map does not fire `styleimagemissing` for empty icon values', async () => {
219
248
  const map = createMap();
220
249
 
@@ -1,7 +1,8 @@
1
1
  import {describe, beforeEach, afterEach, test, expect, vi} from 'vitest';
2
2
  import {Map, type MapOptions} from '../map.ts';
3
3
  import {createMap, beforeMapTest, createStyle, createStyleSource, sleep} from '../../util/test/util.ts';
4
- import {Event as EventedEvent} from '../../util/evented.ts';
4
+ import {ErrorEvent} from '../../util/evented.ts';
5
+ import {MapSourceDataEvent, MapStyleDataEvent} from '../events.ts';
5
6
  import {fixedLngLat, fixedNum} from '../../../test/unit/lib/fixed.ts';
6
7
  import {extend} from '../../util/util.ts';
7
8
  import {fakeServer, type FakeServer} from 'nise';
@@ -42,9 +43,9 @@ describe('setStyle', () => {
42
43
  map.on('data', recordEvent);
43
44
  map.on('dataloading', recordEvent);
44
45
 
45
- map.style.fire(new EventedEvent('error'));
46
- map.style.fire(new EventedEvent('data'));
47
- map.style.fire(new EventedEvent('dataloading'));
46
+ map.style.fire(new ErrorEvent(new Error('test')));
47
+ map.style.fire(new MapSourceDataEvent('data'));
48
+ map.style.fire(new MapSourceDataEvent('dataloading'));
48
49
 
49
50
  expect(events).toEqual([
50
51
  'error',
@@ -65,10 +66,10 @@ describe('setStyle', () => {
65
66
  map.on('sourcedata', recordEvent);
66
67
  map.on('sourcedataloading', recordEvent);
67
68
 
68
- map.style.fire(new EventedEvent('data', {dataType: 'style'}));
69
- map.style.fire(new EventedEvent('dataloading', {dataType: 'style'}));
70
- map.style.fire(new EventedEvent('data', {dataType: 'source'}));
71
- map.style.fire(new EventedEvent('dataloading', {dataType: 'source'}));
69
+ map.style.fire(new MapStyleDataEvent('data'));
70
+ map.style.fire(new MapStyleDataEvent('dataloading'));
71
+ map.style.fire(new MapSourceDataEvent('data'));
72
+ map.style.fire(new MapSourceDataEvent('dataloading'));
72
73
 
73
74
  expect(events).toEqual([
74
75
  'styledata',
@@ -93,6 +93,35 @@ test('does not fire "webglcontextrestored" after remove has been called', async
93
93
  expect(spy).not.toHaveBeenCalled();
94
94
  });
95
95
 
96
+ test('a WebGL style image is told to release its GPU resources on context loss, and survives it', async () => {
97
+ const map = createMap();
98
+ const canvas = map.getCanvas();
99
+ await map.once('load');
100
+
101
+ const userImage = {
102
+ width: 2, height: 2,
103
+ data: {renderWithWebGL: vi.fn()},
104
+ onRemove: vi.fn()
105
+ };
106
+ map.addImage('gpu-image', userImage);
107
+
108
+ const contextLostPromise = map.once('webglcontextlost');
109
+ canvas.dispatchEvent(new window.Event('webglcontextlost'));
110
+ await contextLostPromise;
111
+
112
+ expect(userImage.onRemove).toHaveBeenCalled();
113
+
114
+ const contextRestoredPromise = map.once('webglcontextrestored');
115
+ canvas.dispatchEvent(new window.Event('webglcontextrestored'));
116
+ await contextRestoredPromise;
117
+
118
+ expect(map.hasImage('gpu-image')).toBe(true);
119
+ expect(map.getImage('gpu-image').userImage).toBe(userImage);
120
+ expect(map.style.imageManager.updatedImages['gpu-image']).toBe(true);
121
+
122
+ map.remove();
123
+ });
124
+
96
125
  test('WebGL2 context creation error fires ErrorEvent with structured GPUInitializationError', () => {
97
126
  HTMLCanvasElement.prototype.getContext = function (type: string) {
98
127
  if (type === 'webgl2') {
@@ -96,20 +96,20 @@ describe('marker', () => {
96
96
  .addTo(map);
97
97
 
98
98
  const markerElement = marker.getElement();
99
- expect(markerElement.classList.contains('some')).toBeTruthy();
100
- expect(markerElement.classList.contains('classes')).toBeTruthy();
99
+ expect(markerElement.classList).toContain('some');
100
+ expect(markerElement.classList).toContain('classes');
101
101
 
102
102
  marker.addClassName('addedClass');
103
- expect(markerElement.classList.contains('addedClass')).toBeTruthy();
103
+ expect(markerElement.classList).toContain('addedClass');
104
104
 
105
105
  marker.removeClassName('addedClass');
106
- expect(!markerElement.classList.contains('addedClass')).toBeTruthy();
106
+ expect(markerElement.classList).not.toContain('addedClass');
107
107
 
108
108
  marker.toggleClassName('toggle');
109
- expect(markerElement.classList.contains('toggle')).toBeTruthy();
109
+ expect(markerElement.classList).toContain('toggle');
110
110
 
111
111
  marker.toggleClassName('toggle');
112
- expect(!markerElement.classList.contains('toggle')).toBeTruthy();
112
+ expect(markerElement.classList).not.toContain('toggle');
113
113
 
114
114
  expect(() => marker.addClassName('should throw exception')).toThrow(window.DOMException);
115
115
  expect(() => marker.removeClassName('should throw exception')).toThrow(window.DOMException);
@@ -123,19 +123,19 @@ describe('marker', () => {
123
123
  test('Marker provides LngLat accessors', () => {
124
124
  expect(new Marker().getLngLat()).toBeUndefined();
125
125
 
126
- expect(new Marker().setLngLat([1, 2]).getLngLat() instanceof LngLat).toBeTruthy();
126
+ expect(new Marker().setLngLat([1, 2]).getLngLat()).toBeInstanceOf(LngLat);
127
127
  expect(new Marker().setLngLat([1, 2]).getLngLat()).toEqual(new LngLat(1, 2));
128
128
 
129
- expect(new Marker().setLngLat(new LngLat(1, 2)).getLngLat() instanceof LngLat).toBeTruthy();
129
+ expect(new Marker().setLngLat(new LngLat(1, 2)).getLngLat()).toBeInstanceOf(LngLat);
130
130
  expect(new Marker().setLngLat(new LngLat(1, 2)).getLngLat()).toEqual(new LngLat(1, 2));
131
131
 
132
132
  });
133
133
 
134
134
  test('Marker provides offset accessors', () => {
135
- expect(new Marker().setOffset([1, 2]).getOffset() instanceof Point).toBeTruthy();
135
+ expect(new Marker().setOffset([1, 2]).getOffset()).toBeInstanceOf(Point);
136
136
  expect(new Marker().setOffset([1, 2]).getOffset()).toEqual(new Point(1, 2));
137
137
 
138
- expect(new Marker().setOffset(new Point(1, 2)).getOffset() instanceof Point).toBeTruthy();
138
+ expect(new Marker().setOffset(new Point(1, 2)).getOffset()).toBeInstanceOf(Point);
139
139
  expect(new Marker().setOffset(new Point(1, 2)).getOffset()).toEqual(new Point(1, 2));
140
140
 
141
141
  });
@@ -343,7 +343,7 @@ describe('marker', () => {
343
343
  .setLngLat([0, 0])
344
344
  .addTo(map);
345
345
 
346
- expect(marker.getElement().classList.contains('maplibregl-marker-anchor-center')).toBeTruthy();
346
+ expect(marker.getElement().classList).toContain('maplibregl-marker-anchor-center');
347
347
  expect(marker.getElement().style.transform).toMatch(/translate\(-50%,-50%\)/);
348
348
 
349
349
  map.remove();
@@ -355,7 +355,7 @@ describe('marker', () => {
355
355
  .setLngLat([0, 0])
356
356
  .addTo(map);
357
357
 
358
- expect(marker.getElement().classList.contains('maplibregl-marker-anchor-top')).toBeTruthy();
358
+ expect(marker.getElement().classList).toContain('maplibregl-marker-anchor-top');
359
359
  expect(marker.getElement().style.transform).toMatch(/translate\(-50%,0\)/);
360
360
 
361
361
  map.remove();
@@ -407,51 +407,35 @@ describe('marker', () => {
407
407
  Object.defineProperty(marker.getPopup()._container, 'offsetHeight', {value: 100});
408
408
 
409
409
  // marker should default to above since it has enough space
410
- expect(
411
- marker.getPopup()._container.classList.contains('maplibregl-popup-anchor-bottom')
412
- ).toBeTruthy();
410
+ expect(marker.getPopup()._container.classList).toContain('maplibregl-popup-anchor-bottom');
413
411
 
414
412
  // move marker to the top forcing the popup to below
415
413
  marker.setLngLat(map.unproject([mapHeight / 2, markerTop]));
416
- expect(
417
- marker.getPopup()._container.classList.contains('maplibregl-popup-anchor-top')
418
- ).toBeTruthy();
414
+ expect(marker.getPopup()._container.classList).toContain('maplibregl-popup-anchor-top');
419
415
 
420
416
  // move marker to the right forcing the popup to the left
421
417
  marker.setLngLat(map.unproject([mapHeight - markerRight, mapHeight / 2]));
422
- expect(
423
- marker.getPopup()._container.classList.contains('maplibregl-popup-anchor-right')
424
- ).toBeTruthy();
418
+ expect(marker.getPopup()._container.classList).toContain('maplibregl-popup-anchor-right');
425
419
 
426
420
  // move marker to the left forcing the popup to the right
427
421
  marker.setLngLat(map.unproject([markerRight, mapHeight / 2]));
428
- expect(
429
- marker.getPopup()._container.classList.contains('maplibregl-popup-anchor-left')
430
- ).toBeTruthy();
422
+ expect(marker.getPopup()._container.classList).toContain('maplibregl-popup-anchor-left');
431
423
 
432
424
  // move marker to the top left forcing the popup to the bottom right
433
425
  marker.setLngLat(map.unproject([markerRight, markerTop]));
434
- expect(
435
- marker.getPopup()._container.classList.contains('maplibregl-popup-anchor-top-left')
436
- ).toBeTruthy();
426
+ expect(marker.getPopup()._container.classList).toContain('maplibregl-popup-anchor-top-left');
437
427
 
438
428
  // move marker to the top right forcing the popup to the bottom left
439
429
  marker.setLngLat(map.unproject([mapHeight - markerRight, markerTop]));
440
- expect(
441
- marker.getPopup()._container.classList.contains('maplibregl-popup-anchor-top-right')
442
- ).toBeTruthy();
430
+ expect(marker.getPopup()._container.classList).toContain('maplibregl-popup-anchor-top-right');
443
431
 
444
432
  // move marker to the bottom left forcing the popup to the top right
445
433
  marker.setLngLat(map.unproject([markerRight, mapHeight]));
446
- expect(
447
- marker.getPopup()._container.classList.contains('maplibregl-popup-anchor-bottom-left')
448
- ).toBeTruthy();
434
+ expect(marker.getPopup()._container.classList).toContain('maplibregl-popup-anchor-bottom-left');
449
435
 
450
436
  // move marker to the bottom right forcing the popup to the top left
451
437
  marker.setLngLat(map.unproject([mapHeight - markerRight, mapHeight]));
452
- expect(
453
- marker.getPopup()._container.classList.contains('maplibregl-popup-anchor-bottom-right')
454
- ).toBeTruthy();
438
+ expect(marker.getPopup()._container.classList).toContain('maplibregl-popup-anchor-bottom-right');
455
439
 
456
440
  map.remove();
457
441
  });
@@ -1352,7 +1336,7 @@ describe('marker', () => {
1352
1336
  map.fire('terrain');
1353
1337
  await sleep(100);
1354
1338
 
1355
- expect(marker.getElement().classList.contains('maplibregl-marker-covered')).toBe(true);
1339
+ expect(marker.getElement().classList).toContain('maplibregl-marker-covered');
1356
1340
  map.remove();
1357
1341
  });
1358
1342
 
@@ -1368,13 +1352,13 @@ describe('marker', () => {
1368
1352
  map.fire('terrain');
1369
1353
  await sleep(100);
1370
1354
 
1371
- expect(marker.getElement().classList.contains('maplibregl-marker-covered')).toBe(true);
1355
+ expect(marker.getElement().classList).toContain('maplibregl-marker-covered');
1372
1356
 
1373
1357
  map.terrain.depthAtPoint = () => .95; // Terrain no longer blocking marker
1374
1358
  map.fire('moveend');
1375
1359
  await sleep(100);
1376
1360
 
1377
- expect(marker.getElement().classList.contains('maplibregl-marker-covered')).toBe(false);
1361
+ expect(marker.getElement().classList).not.toContain('maplibregl-marker-covered');
1378
1362
  map.remove();
1379
1363
  });
1380
1364
 
@@ -1389,7 +1373,7 @@ describe('marker', () => {
1389
1373
  map.setProjection({type: 'globe'});
1390
1374
  await sleep(100);
1391
1375
 
1392
- expect(marker.getElement().classList.contains('maplibregl-marker-covered')).toBe(true);
1376
+ expect(marker.getElement().classList).toContain('maplibregl-marker-covered');
1393
1377
  map.remove();
1394
1378
  });
1395
1379
 
@@ -1404,12 +1388,12 @@ describe('marker', () => {
1404
1388
  map.setProjection({type: 'globe'});
1405
1389
  await sleep(100);
1406
1390
 
1407
- expect(marker.getElement().classList.contains('maplibregl-marker-covered')).toBe(true);
1391
+ expect(marker.getElement().classList).toContain('maplibregl-marker-covered');
1408
1392
 
1409
1393
  marker.setLngLat([0, 0]);
1410
1394
  await sleep(100);
1411
1395
 
1412
- expect(marker.getElement().classList.contains('maplibregl-marker-covered')).toBe(false);
1396
+ expect(marker.getElement().classList).not.toContain('maplibregl-marker-covered');
1413
1397
  map.remove();
1414
1398
  });
1415
1399
  });
@@ -32,7 +32,7 @@ describe('popup', () => {
32
32
  .addTo(map);
33
33
 
34
34
  expect(popup.isOpen()).toBeTruthy();
35
- expect(popup.getElement().classList.contains('maplibregl-popup')).toBeTruthy();
35
+ expect(popup.getElement().classList).toContain('maplibregl-popup');
36
36
  });
37
37
 
38
38
  test('Popup.addTo adds a .maplibregl-popup element', () => {
@@ -258,10 +258,10 @@ describe('popup', () => {
258
258
  test('Popup provides LngLat accessors', () => {
259
259
  expect(new Popup().getLngLat()).toBeUndefined();
260
260
 
261
- expect(new Popup().setLngLat([1, 2]).getLngLat() instanceof LngLat).toBeTruthy();
261
+ expect(new Popup().setLngLat([1, 2]).getLngLat()).toBeInstanceOf(LngLat);
262
262
  expect(new Popup().setLngLat([1, 2]).getLngLat()).toEqual(new LngLat(1, 2));
263
263
 
264
- expect(new Popup().setLngLat(new LngLat(1, 2)).getLngLat() instanceof LngLat).toBeTruthy();
264
+ expect(new Popup().setLngLat(new LngLat(1, 2)).getLngLat()).toBeInstanceOf(LngLat);
265
265
  expect(new Popup().setLngLat(new LngLat(1, 2)).getLngLat()).toEqual(new LngLat(1, 2));
266
266
 
267
267
  });
@@ -382,7 +382,7 @@ describe('popup', () => {
382
382
  .setText('Test')
383
383
  .addTo(map);
384
384
 
385
- expect(popup.getElement().classList.contains('maplibregl-popup-anchor-top-left')).toBeTruthy();
385
+ expect(popup.getElement().classList).toContain('maplibregl-popup-anchor-top-left');
386
386
  });
387
387
 
388
388
  const cases = [
@@ -413,7 +413,7 @@ describe('popup', () => {
413
413
  vi.spyOn(map, 'project').mockReturnValue(point);
414
414
  popup.setLngLat([0, 0]);
415
415
 
416
- expect(popup.getElement().classList.contains(`maplibregl-popup-anchor-${anchor}`)).toBeTruthy();
416
+ expect(popup.getElement().classList).toContain(`maplibregl-popup-anchor-${anchor}`);
417
417
  });
418
418
 
419
419
  const transformCases = cases.map(([anchor, _point, transform]) => [anchor, transform] as const);
@@ -449,7 +449,7 @@ describe('popup', () => {
449
449
  vi.spyOn(map, 'project').mockReturnValue(point);
450
450
  popup.setLngLat([0, 0]);
451
451
 
452
- expect(popup.getElement().classList.contains('maplibregl-popup-anchor-top')).toBeTruthy();
452
+ expect(popup.getElement().classList).toContain('maplibregl-popup-anchor-top');
453
453
  });
454
454
 
455
455
  test('Popup is offset via a PointLike offset option', () => {
@@ -563,22 +563,22 @@ describe('popup', () => {
563
563
  .addTo(map);
564
564
 
565
565
  const popupContainer = popup.getElement();
566
- expect(popupContainer.classList.contains('some')).toBeTruthy();
567
- expect(popupContainer.classList.contains('classes')).toBeTruthy();
566
+ expect(popupContainer.classList).toContain('some');
567
+ expect(popupContainer.classList).toContain('classes');
568
568
 
569
569
  const addClassNameMethodPopupInstance = popup.addClassName('addedClass');
570
- expect(popupContainer.classList.contains('addedClass')).toBeTruthy();
570
+ expect(popupContainer.classList).toContain('addedClass');
571
571
  expect(addClassNameMethodPopupInstance).toBeInstanceOf(Popup);
572
572
 
573
573
  const removeClassNameMethodPopupInstance = popup.removeClassName('addedClass');
574
- expect(!popupContainer.classList.contains('addedClass')).toBeTruthy();
574
+ expect(popupContainer.classList).not.toContain('addedClass');
575
575
  expect(removeClassNameMethodPopupInstance).toBeInstanceOf(Popup);
576
576
 
577
577
  popup.toggleClassName('toggle');
578
- expect(popupContainer.classList.contains('toggle')).toBeTruthy();
578
+ expect(popupContainer.classList).toContain('toggle');
579
579
 
580
580
  popup.toggleClassName('toggle');
581
- expect(!popupContainer.classList.contains('toggle')).toBeTruthy();
581
+ expect(popupContainer.classList).not.toContain('toggle');
582
582
 
583
583
  expect(() => popup.addClassName('should throw exception')).toThrow(window.DOMException);
584
584
  expect(() => popup.removeClassName('should throw exception')).toThrow(window.DOMException);
@@ -1063,8 +1063,8 @@ describe('popup', () => {
1063
1063
  .trackPointer()
1064
1064
  .addTo(map);
1065
1065
 
1066
- expect(popup.getElement().classList.contains('maplibregl-popup-track-pointer')).toBeTruthy();
1067
- expect(map._canvasContainer.classList.contains('maplibregl-track-pointer')).toBeTruthy();
1066
+ expect(popup.getElement().classList).toContain('maplibregl-popup-track-pointer');
1067
+ expect(map._canvasContainer.classList).toContain('maplibregl-track-pointer');
1068
1068
 
1069
1069
  popup.remove();
1070
1070
  });