maplibre-gl 6.0.0 → 6.2.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 (181) hide show
  1. package/build/readme.md +0 -2
  2. package/dist/maplibre-gl-dev.mjs +433 -573
  3. package/dist/maplibre-gl-dev.mjs.map +1 -1
  4. package/dist/maplibre-gl-shared-dev.mjs +984 -850
  5. package/dist/maplibre-gl-shared-dev.mjs.map +1 -1
  6. package/dist/maplibre-gl-shared.mjs +2 -2
  7. package/dist/maplibre-gl-shared.mjs.map +1 -1
  8. package/dist/maplibre-gl-worker-dev.mjs +14 -12
  9. package/dist/maplibre-gl-worker-dev.mjs.map +1 -1
  10. package/dist/maplibre-gl-worker.mjs +2 -2
  11. package/dist/maplibre-gl-worker.mjs.map +1 -1
  12. package/dist/maplibre-gl.css +1 -1
  13. package/dist/maplibre-gl.d.ts +119 -42
  14. package/dist/maplibre-gl.mjs +22 -22
  15. package/dist/maplibre-gl.mjs.map +1 -1
  16. package/package.json +20 -22
  17. package/src/css/maplibre-gl.css +4 -0
  18. package/src/data/bucket/circle_bucket.ts +3 -2
  19. package/src/data/bucket/fill_bucket.ts +3 -2
  20. package/src/data/bucket/fill_extrusion_bucket.test.ts +22 -0
  21. package/src/data/bucket/fill_extrusion_bucket.ts +17 -4
  22. package/src/data/bucket/line_bucket.test.ts +5 -5
  23. package/src/data/bucket/line_bucket.ts +3 -2
  24. package/src/data/bucket/round_polygon_corners.test.ts +159 -0
  25. package/src/data/bucket/round_polygon_corners.ts +121 -0
  26. package/src/data/bucket/symbol_bucket.test.ts +7 -7
  27. package/src/data/dem_data.test.ts +23 -23
  28. package/src/data/feature_position_map.test.ts +1 -1
  29. package/src/data/segment.test.ts +14 -14
  30. package/src/geo/bounds.test.ts +4 -4
  31. package/src/geo/lng_lat_bounds.test.ts +1 -1
  32. package/src/geo/projection/covering_tiles.bench.ts +39 -0
  33. package/src/geo/projection/covering_tiles.test.ts +3 -7
  34. package/src/geo/projection/covering_tiles.ts +37 -2
  35. package/src/geo/projection/globe_covering_tiles_details_provider.ts +4 -4
  36. package/src/geo/projection/globe_projection.test.ts +2 -2
  37. package/src/geo/projection/globe_projection.ts +6 -15
  38. package/src/geo/projection/globe_transform.test.ts +3 -6
  39. package/src/geo/projection/globe_transform.ts +4 -7
  40. package/src/geo/projection/mercator_covering_tiles_details_provider.ts +4 -4
  41. package/src/geo/projection/mercator_projection.ts +1 -13
  42. package/src/geo/projection/mercator_transform.test.ts +1 -1
  43. package/src/geo/projection/mercator_transform.ts +2 -1
  44. package/src/geo/projection/mercator_utils.test.ts +8 -8
  45. package/src/geo/projection/projection.ts +0 -27
  46. package/src/geo/projection/projection_data.ts +8 -0
  47. package/src/geo/projection/projection_factory.ts +3 -3
  48. package/src/geo/projection/vertical_perspective_projection.ts +3 -60
  49. package/src/geo/projection/vertical_perspective_transform.ts +16 -23
  50. package/src/geo/transform_interface.ts +1 -2
  51. package/src/index.test.ts +7 -12
  52. package/src/index.ts +4 -2
  53. package/src/render/glyph_manager.test.ts +7 -22
  54. package/src/render/painter.test.ts +2 -0
  55. package/src/render/painter.ts +8 -11
  56. package/src/render/subdivision.bench.ts +39 -0
  57. package/src/render/subdivision.test.ts +22 -23
  58. package/src/render/subdivision.ts +46 -0
  59. package/src/render/terrain.test.ts +68 -34
  60. package/src/render/terrain.ts +69 -26
  61. package/src/shaders/glsl/_projection_globe.fragment.glsl +12 -0
  62. package/src/shaders/glsl/_projection_globe.fragment.glsl.g.ts +2 -0
  63. package/src/shaders/glsl/_projection_globe.vertex.glsl +29 -10
  64. package/src/shaders/glsl/_projection_globe.vertex.glsl.g.ts +1 -1
  65. package/src/shaders/glsl/_projection_mercator.fragment.glsl +4 -0
  66. package/src/shaders/glsl/_projection_mercator.fragment.glsl.g.ts +2 -0
  67. package/src/shaders/glsl/line.fragment.glsl +2 -0
  68. package/src/shaders/glsl/line.fragment.glsl.g.ts +1 -1
  69. package/src/shaders/glsl/line_gradient.fragment.glsl +2 -0
  70. package/src/shaders/glsl/line_gradient.fragment.glsl.g.ts +1 -1
  71. package/src/shaders/glsl/line_gradient_sdf.fragment.glsl +2 -0
  72. package/src/shaders/glsl/line_gradient_sdf.fragment.glsl.g.ts +1 -1
  73. package/src/shaders/glsl/line_pattern.fragment.glsl +2 -0
  74. package/src/shaders/glsl/line_pattern.fragment.glsl.g.ts +1 -1
  75. package/src/shaders/glsl/line_sdf.fragment.glsl +2 -0
  76. package/src/shaders/glsl/line_sdf.fragment.glsl.g.ts +1 -1
  77. package/src/shaders/shaders.ts +4 -6
  78. package/src/source/canvas_source.test.ts +1 -1
  79. package/src/source/geojson_source.test.ts +32 -4
  80. package/src/source/geojson_source.ts +41 -0
  81. package/src/source/geojson_source_diff.test.ts +2 -2
  82. package/src/source/geojson_worker_source.test.ts +9 -5
  83. package/src/source/image_source.test.ts +137 -62
  84. package/src/source/image_source.ts +49 -12
  85. package/src/source/raster_dem_tile_source.test.ts +2 -2
  86. package/src/source/raster_tile_source.test.ts +64 -9
  87. package/src/source/raster_tile_source.ts +2 -1
  88. package/src/source/rtl_text_plugin_main_thread.test.ts +14 -14
  89. package/src/source/source.test.ts +8 -8
  90. package/src/source/vector_tile_source.test.ts +1 -1
  91. package/src/source/vector_tile_worker_source.test.ts +7 -5
  92. package/src/source/video_source.test.ts +1 -1
  93. package/src/source/worker.test.ts +1 -1
  94. package/src/source/worker_tile.test.ts +10 -8
  95. package/src/style/light.test.ts +9 -9
  96. package/src/style/light.ts +3 -3
  97. package/src/style/load_glyph_range.test.ts +8 -8
  98. package/src/style/load_sprite.test.ts +1 -1
  99. package/src/style/properties.test.ts +94 -2
  100. package/src/style/properties.ts +30 -1
  101. package/src/style/sky.test.ts +9 -9
  102. package/src/style/sky.ts +2 -2
  103. package/src/style/style.test.ts +38 -36
  104. package/src/style/style.ts +3 -3
  105. package/src/style/style_layer/color_relief_style_layer.test.ts +1 -1
  106. package/src/style/style_layer/fill_extrusion_style_layer.ts +6 -3
  107. package/src/style/style_layer/fill_extrusion_style_layer_properties.g.ts +14 -1
  108. package/src/style/style_layer/raster_style_layer.test.ts +2 -2
  109. package/src/style/style_layer.test.ts +5 -51
  110. package/src/style/validate_style.test.ts +1 -1
  111. package/src/symbol/collision_index.test.ts +1 -1
  112. package/src/symbol/grid_index.test.ts +1 -1
  113. package/src/tile/terrain_tile_manager.test.ts +2 -2
  114. package/src/tile/tile_id.test.ts +5 -5
  115. package/src/tile/tile_manager.test.ts +113 -72
  116. package/src/tile/tile_manager.ts +2 -2
  117. package/src/tile/tile_manager_in_view_tiles.test.ts +2 -2
  118. package/src/ui/camera.test.ts +138 -135
  119. package/src/ui/control/geolocate_control.test.ts +7 -30
  120. package/src/ui/handler/dblclick_zoom.test.ts +2 -2
  121. package/src/ui/handler/mouse.ts +15 -7
  122. package/src/ui/handler/mouse_handler_interface.test.ts +21 -1
  123. package/src/ui/handler/scroll_zoom.test.ts +2 -0
  124. package/src/ui/handler_manager.test.ts +7 -7
  125. package/src/ui/hash.test.ts +1 -0
  126. package/src/ui/map.ts +45 -20
  127. package/src/ui/map_tests/map_basic.test.ts +11 -23
  128. package/src/ui/map_tests/map_calculate_camera_options.test.ts +6 -6
  129. package/src/ui/map_tests/map_events.test.ts +5 -7
  130. package/src/ui/map_tests/map_global_state.test.ts +3 -3
  131. package/src/ui/map_tests/map_images.test.ts +4 -3
  132. package/src/ui/map_tests/map_is_moving.test.ts +1 -1
  133. package/src/ui/map_tests/map_is_rotating.test.ts +1 -1
  134. package/src/ui/map_tests/map_layer.test.ts +2 -1
  135. package/src/ui/map_tests/map_options.test.ts +1 -5
  136. package/src/ui/map_tests/map_pitch.test.ts +6 -6
  137. package/src/ui/map_tests/map_refresh_tiles.test.ts +2 -2
  138. package/src/ui/map_tests/map_render.test.ts +1 -1
  139. package/src/ui/map_tests/map_set_lod_params.test.ts +1 -1
  140. package/src/ui/map_tests/map_style.test.ts +9 -1
  141. package/src/ui/map_tests/map_terrain.test.ts +41 -1
  142. package/src/ui/map_tests/map_zoom.test.ts +15 -15
  143. package/src/ui/marker.test.ts +71 -14
  144. package/src/ui/marker.ts +30 -6
  145. package/src/ui/popup.test.ts +3 -3
  146. package/src/util/actor.test.ts +6 -6
  147. package/src/util/ajax.test.ts +27 -39
  148. package/src/util/browser.test.ts +7 -9
  149. package/src/util/create_tile_mesh.test.ts +2 -2
  150. package/src/util/dispatcher.test.ts +1 -1
  151. package/src/util/evented.test.ts +4 -1
  152. package/src/util/fast_maths.test.ts +4 -4
  153. package/src/util/image_request.test.ts +6 -4
  154. package/src/util/primitives/bounding_volume_cache.ts +3 -1
  155. package/src/util/struct_array.test.ts +2 -2
  156. package/src/util/task_queue.test.ts +1 -1
  157. package/src/util/test/util.ts +1 -0
  158. package/src/util/time_control.test.ts +2 -2
  159. package/src/util/transferable_grid_index.test.ts +1 -1
  160. package/src/util/util.test.ts +27 -13
  161. package/src/util/verticalize_punctuation.test.ts +4 -4
  162. package/src/util/web_worker_transfer.test.ts +5 -5
  163. package/src/webgl/draw/draw_custom.test.ts +13 -13
  164. package/src/webgl/draw/draw_debug.test.ts +1 -1
  165. package/src/webgl/draw/draw_fill.test.ts +13 -12
  166. package/src/webgl/draw/draw_symbol.test.ts +17 -16
  167. package/src/webgl/program/program_uniforms.ts +0 -3
  168. package/src/webgl/program/projection_program.ts +12 -10
  169. package/src/webgl/program/terrain_program.test.ts +2 -2
  170. package/src/webgl/program.ts +6 -4
  171. package/src/webgl/render_to_texture.test.ts +5 -5
  172. package/src/webgl/state.test.ts +1 -1
  173. package/src/webgl/texture.test.ts +3 -3
  174. package/src/webgl/vertex_buffer.test.ts +1 -1
  175. package/build/rolldown_plugins.ts +0 -18
  176. package/src/geo/projection/globe_projection_error_measurement.ts +0 -229
  177. package/src/shaders/glsl/projection_error_measurement.fragment.glsl +0 -5
  178. package/src/shaders/glsl/projection_error_measurement.fragment.glsl.g.ts +0 -2
  179. package/src/shaders/glsl/projection_error_measurement.vertex.glsl +0 -22
  180. package/src/shaders/glsl/projection_error_measurement.vertex.glsl.g.ts +0 -2
  181. package/src/webgl/program/projection_error_measurement_program.ts +0 -23
@@ -49,11 +49,7 @@ describe('mapOptions', () => {
49
49
  let idleTriggered = false;
50
50
  const fadeDuration = 100;
51
51
  const spy = vi.spyOn(Style.prototype, 'update').mockImplementation((parameters: EvaluationParameters) => {
52
- if (!idleTriggered) {
53
- expect(parameters.fadeDuration).toBe(0);
54
- } else {
55
- expect(parameters.fadeDuration).toBe(fadeDuration);
56
- }
52
+ expect(parameters.fadeDuration).toBe(idleTriggered ? fadeDuration : 0);
57
53
  });
58
54
  const style = createStyle();
59
55
  const map = createMap({style, fadeDuration});
@@ -31,7 +31,7 @@ test('ignore minPitchs over maxPitch', () => {
31
31
  const map = createMap({pitch: 0, maxPitch: 10});
32
32
  expect(() => {
33
33
  map.setMinPitch(20);
34
- }).toThrow();
34
+ }).toThrow('minPitch must be between 0 and the current maxPitch, inclusive');
35
35
  map.setPitch(0);
36
36
  expect(map.getPitch()).toBe(0);
37
37
  });
@@ -89,7 +89,7 @@ test('ignore maxPitchs over minPitch', () => {
89
89
  const map = createMap({minPitch: 10});
90
90
  expect(() => {
91
91
  map.setMaxPitch(0);
92
- }).toThrow();
92
+ }).toThrow('maxPitch must be greater than the current minPitch');
93
93
  map.setPitch(10);
94
94
  expect(map.getPitch()).toBe(10);
95
95
  });
@@ -140,8 +140,8 @@ test('fire move and pitch events when pitch is changed due to minPitch change',
140
140
  map.on('pitch', handleEvent);
141
141
  map.on('pitchend', handleEvent);
142
142
  map.setMinPitch(11);
143
- expect(map.getPitch()).toEqual(11);
144
- expect(map.getMinPitch()).toEqual(11);
143
+ expect(map.getPitch()).toBe(11);
144
+ expect(map.getMinPitch()).toBe(11);
145
145
  expect(handleEvent).toHaveBeenCalledTimes(6);
146
146
  });
147
147
 
@@ -155,7 +155,7 @@ test('fire move and pitch events when pitch is changed due to maxPitch change',
155
155
  map.on('pitch', handleEvent);
156
156
  map.on('pitchend', handleEvent);
157
157
  map.setMaxPitch(10);
158
- expect(map.getPitch()).toEqual(10);
159
- expect(map.getMaxPitch()).toEqual(10);
158
+ expect(map.getPitch()).toBe(10);
159
+ expect(map.getMaxPitch()).toBe(10);
160
160
  expect(handleEvent).toHaveBeenCalledTimes(6);
161
161
  });
@@ -30,7 +30,7 @@ describe('Map::refreshTiles', () => {
30
30
  map.style.tileManagers['source-id1'].refreshTiles = spy;
31
31
 
32
32
  map.refreshTiles('source-id1', [{x: 1024, y: 1023, z: 11}]);
33
- expect(spy).toHaveBeenCalledOnce();
33
+ expect(spy).toHaveBeenCalledTimes(1);
34
34
  expect(spy.mock.calls[0][0]).toEqual([new CanonicalTileID(11, 1024, 1023)]);
35
35
  });
36
36
 
@@ -43,7 +43,7 @@ describe('Map::refreshTiles', () => {
43
43
  map.style.tileManagers['source-id1'].reload = spy;
44
44
 
45
45
  map.refreshTiles('source-id1');
46
- expect(spy).toHaveBeenCalledOnce();
46
+ expect(spy).toHaveBeenCalledTimes(1);
47
47
  expect(spy.mock.calls[0][0]).toBe(true);
48
48
  });
49
49
  });
@@ -87,5 +87,5 @@ test('redraw', async () => {
87
87
  const renderPromise = map.once('render');
88
88
 
89
89
  map.redraw();
90
- await renderPromise;
90
+ await expect(renderPromise).resolves.toBeDefined();
91
91
  });
@@ -42,7 +42,7 @@ test('set tile LOD params for a non-existent source', async () => {
42
42
 
43
43
  expect(map.getSource('source-id1').calculateTileZoom).toBeUndefined();
44
44
  expect(map.getSource('source-id2').calculateTileZoom).toBeUndefined();
45
- expect(() => {map.setSourceTileLodParams(1, 1, 'non-existent-source-id');}).toThrowError();
45
+ expect(() => {map.setSourceTileLodParams(1, 1, 'non-existent-source-id');}).toThrow('There is no source with ID "non-existent-source-id", cannot set LOD parameters');
46
46
  expect(map.getSource('source-id1').calculateTileZoom).toBeUndefined();
47
47
  expect(map.getSource('source-id2').calculateTileZoom).toBeUndefined();
48
48
  });
@@ -78,12 +78,14 @@ describe('setStyle', () => {
78
78
  ]);
79
79
  });
80
80
 
81
- test('can be called more than once', () => {
81
+ test('can be called more than once', async () => {
82
82
  const map = createMap();
83
83
 
84
84
  map.setStyle({version: 8, sources: {}, layers: []}, {diff: false});
85
85
  map.setStyle({version: 8, sources: {}, layers: []}, {diff: false});
86
+ await map.once('style.load');
86
87
 
88
+ expect(map.getStyle()).toEqual({version: 8, sources: {}, layers: []});
87
89
  });
88
90
 
89
91
  test('setStyle back to the first style should work', async () => {
@@ -477,12 +479,16 @@ describe('getStyle', () => {
477
479
  type: 'background'
478
480
  } as LayerSpecification;
479
481
  map.addLayer(layer);
482
+
483
+ expect(map.getLayer('background')).toBeDefined();
480
484
  });
481
485
 
482
486
  test('a source can be added even if a map is created without a style', () => {
483
487
  const map = createMap({deleteStyle: true});
484
488
  const source = createStyleSource();
485
489
  map.addSource('fill', source);
490
+
491
+ expect(map.getSource('fill')).toBeDefined();
486
492
  });
487
493
 
488
494
  test('a layer can be added with an embedded source specification', () => {
@@ -496,6 +502,8 @@ describe('getStyle', () => {
496
502
  type: 'symbol',
497
503
  source
498
504
  });
505
+
506
+ expect(map.getLayer('foo')).toBeDefined();
499
507
  });
500
508
 
501
509
  test('returns the style with added source and layer', async () => {
@@ -1,9 +1,10 @@
1
1
  import {describe, beforeEach, afterEach, test, expect, vi} from 'vitest';
2
- import {createMap, beforeMapTest} from '../../util/test/util.ts';
2
+ import {createMap, beforeMapTest, waitForEvent} from '../../util/test/util.ts';
3
3
  import {LngLat} from '../../geo/lng_lat.ts';
4
4
  import {fakeServer, type FakeServer} from 'nise';
5
5
  import {type Terrain} from '../../render/terrain.ts';
6
6
  import {MercatorTransform} from '../../geo/projection/mercator_transform.ts';
7
+ import {AttributionControl, defaultAttributionControlOptions} from '../control/attribution_control.ts';
7
8
  import {type Map} from '../map.ts';
8
9
 
9
10
  let server: FakeServer;
@@ -67,6 +68,45 @@ describe('setTerrain', () => {
67
68
  expect(errorSpy).not.toHaveBeenCalled();
68
69
  expect(map.getTerrain()).toEqual({source: 'dem', exaggeration: 2});
69
70
  });
71
+
72
+ test('drops the previous source attribution when switching terrain to a new source', async () => {
73
+ const attribution = new AttributionControl();
74
+ map.addControl(attribution);
75
+ await map.once('style.load');
76
+ const demALoaded = waitForEvent(map, 'sourcedata', (e) => e.sourceId === 'dem-a' && e.sourceDataType === 'metadata');
77
+ const demBLoaded = waitForEvent(map, 'sourcedata', (e) => e.sourceId === 'dem-b' && e.sourceDataType === 'metadata');
78
+ map.addSource('dem-a', {type: 'raster-dem', tiles: ['http://example.com/{z}/{x}/{y}.png'], tileSize: 256, attribution: 'DEM A'});
79
+ map.addSource('dem-b', {type: 'raster-dem', tiles: ['http://example.com/{z}/{x}/{y}.png'], tileSize: 256, attribution: 'DEM B'});
80
+ await Promise.all([demALoaded, demBLoaded]);
81
+
82
+ map.setTerrain({source: 'dem-a'});
83
+ const terrainEvent = map.once('terrain');
84
+ map.setTerrain({source: 'dem-b'});
85
+ await terrainEvent;
86
+
87
+ const innerContainer = map.getContainer().querySelector('.maplibregl-ctrl-attrib-inner');
88
+ expect(innerContainer.innerHTML).toBe(`DEM B | ${defaultAttributionControlOptions.customAttribution}`);
89
+ });
90
+
91
+ test('invalidates cached elevation when terrain source data changes without a tile', async () => {
92
+ await map.once('load');
93
+ map.addSource('terrainrgb', {
94
+ type: 'raster-dem',
95
+ tiles: ['http://example.com/{z}/{x}/{y}.png']
96
+ });
97
+ map.setTerrain({source: 'terrainrgb'});
98
+ if (!map.terrain) throw new Error('Expected terrain to be set');
99
+ const resetElevationCache = vi.spyOn(map.terrain, 'resetElevationCache');
100
+
101
+ map._terrainDataCallback({
102
+ dataType: 'source',
103
+ sourceId: 'terrainrgb',
104
+ sourceDataType: 'content',
105
+ source: {type: 'raster-dem'}
106
+ } as any);
107
+
108
+ expect(resetElevationCache).toHaveBeenCalledTimes(1);
109
+ });
70
110
  });
71
111
 
72
112
  describe('getTerrain', () => {
@@ -32,7 +32,7 @@ test('ignore minZooms over maxZoom', () => {
32
32
  const map = createMap({zoom: 2, maxZoom: 5});
33
33
  expect(() => {
34
34
  map.setMinZoom(6);
35
- }).toThrow();
35
+ }).toThrow('minZoom must be between -2 and the current maxZoom, inclusive');
36
36
  map.setZoom(0);
37
37
  expect(map.getZoom()).toBe(0);
38
38
  });
@@ -62,7 +62,7 @@ test('ignore maxZooms over minZoom', () => {
62
62
  const map = createMap({minZoom: 5});
63
63
  expect(() => {
64
64
  map.setMaxZoom(4);
65
- }).toThrow();
65
+ }).toThrow('maxZoom must be greater than the current minZoom');
66
66
  map.setZoom(5);
67
67
  expect(map.getZoom()).toBe(5);
68
68
  });
@@ -102,16 +102,16 @@ test('transformCameraUpdate is called after changing min or max zoom', async ()
102
102
  map.setMaxZoom(18);
103
103
  map.setZoom(18);
104
104
  await map.once('style.load');
105
- expect(map.getZoom()).toEqual(18);
106
- expect(map.getMinZoom()).toEqual(5);
107
- expect(map.getMaxZoom()).toEqual(18);
105
+ expect(map.getZoom()).toBe(18);
106
+ expect(map.getMinZoom()).toBe(5);
107
+ expect(map.getMaxZoom()).toBe(18);
108
108
  const transformCameraUpdate = vi.fn(t => t);
109
109
  map.setTransformCameraUpdate(transformCameraUpdate);
110
110
 
111
111
  map.setMaxZoom(16);
112
- expect(map.getZoom()).toEqual(16);
113
- expect(map.getMinZoom()).toEqual(5);
114
- expect(map.getMaxZoom()).toEqual(16);
112
+ expect(map.getZoom()).toBe(16);
113
+ expect(map.getMinZoom()).toBe(5);
114
+ expect(map.getMaxZoom()).toBe(16);
115
115
  expect(transformCameraUpdate.mock.calls[0][0]).toMatchObject({
116
116
  zoom: 16,
117
117
  minZoom: 5,
@@ -127,9 +127,9 @@ test('transformCameraUpdate is called after changing min or max zoom', async ()
127
127
 
128
128
  map.setMinZoom(6);
129
129
  map.setTransformCameraUpdate(transformCameraUpdate);
130
- expect(map.getZoom()).toEqual(6);
131
- expect(map.getMinZoom()).toEqual(6);
132
- expect(map.getMaxZoom()).toEqual(16);
130
+ expect(map.getZoom()).toBe(6);
131
+ expect(map.getMinZoom()).toBe(6);
132
+ expect(map.getMaxZoom()).toBe(16);
133
133
  expect(transformCameraUpdate.mock.calls[2][0]).toMatchObject({
134
134
  zoom: 6,
135
135
  minZoom: 6,
@@ -147,8 +147,8 @@ test('fire move and zoom events when zoom is changed due to minZoom change', ()
147
147
  map.on('zoom', handleEvent);
148
148
  map.on('zoomend', handleEvent);
149
149
  map.setMinZoom(11);
150
- expect(map.getZoom()).toEqual(11);
151
- expect(map.getMinZoom()).toEqual(11);
150
+ expect(map.getZoom()).toBe(11);
151
+ expect(map.getMinZoom()).toBe(11);
152
152
  expect(handleEvent).toHaveBeenCalledTimes(6);
153
153
  });
154
154
 
@@ -162,7 +162,7 @@ test('fire move and zoom events when zoom is changed due to maxZoom change', ()
162
162
  map.on('zoom', handleEvent);
163
163
  map.on('zoomend', handleEvent);
164
164
  map.setMaxZoom(10);
165
- expect(map.getZoom()).toEqual(10);
166
- expect(map.getMaxZoom()).toEqual(10);
165
+ expect(map.getZoom()).toBe(10);
166
+ expect(map.getMaxZoom()).toBe(10);
167
167
  expect(handleEvent).toHaveBeenCalledTimes(6);
168
168
  });
@@ -35,7 +35,7 @@ describe('marker', () => {
35
35
 
36
36
  test('Marker uses a default marker element with custom color', () => {
37
37
  const marker = new Marker({color: '#123456'});
38
- expect(marker.getElement().innerHTML.includes('#123456')).toBeTruthy();
38
+ expect(marker.getElement().innerHTML).toContain('#123456');
39
39
  });
40
40
 
41
41
  test('Marker uses a default marker element with custom scale', () => {
@@ -53,22 +53,16 @@ describe('marker', () => {
53
53
  .addTo(map);
54
54
 
55
55
  // initial dimensions of svg element
56
- expect(
57
- defaultMarker.getElement().children[0].getAttribute('height').includes('41')
58
- ).toBeTruthy();
59
- expect(defaultMarker.getElement().children[0].getAttribute('width').includes('27')).toBeTruthy();
56
+ expect(defaultMarker.getElement().children[0].getAttribute('height')).toContain('41');
57
+ expect(defaultMarker.getElement().children[0].getAttribute('width')).toContain('27');
60
58
 
61
59
  // (41 * 0.8) = 32.8, (27 * 0.8) = 21.6
62
- expect(
63
- smallerMarker.getElement().children[0].getAttribute('height').includes('32.8')
64
- ).toBeTruthy();
65
- expect(
66
- smallerMarker.getElement().children[0].getAttribute('width').includes('21.6')
67
- ).toBeTruthy();
60
+ expect(smallerMarker.getElement().children[0].getAttribute('height')).toContain('32.8');
61
+ expect(smallerMarker.getElement().children[0].getAttribute('width')).toContain('21.6');
68
62
 
69
63
  // (41 * 2) = 82, (27 * 2) = 54
70
- expect(largerMarker.getElement().children[0].getAttribute('height').includes('82')).toBeTruthy();
71
- expect(largerMarker.getElement().children[0].getAttribute('width').includes('54')).toBeTruthy();
64
+ expect(largerMarker.getElement().children[0].getAttribute('height')).toContain('82');
65
+ expect(largerMarker.getElement().children[0].getAttribute('width')).toContain('54');
72
66
 
73
67
  });
74
68
 
@@ -290,11 +284,57 @@ describe('marker', () => {
290
284
  expect(markerWithHtmlElement.getElement().getAttribute('aria-label')).toBe('custom aria label');
291
285
  });
292
286
 
293
- test('Marker should have a role attribute to satisfy accessibility requirements for the aria-label', () => {
287
+ test('default non-interactive Marker uses role=img for the aria-label', () => {
294
288
  const map = createMap({locale: {'Marker.Title': 'alt title'}});
295
289
  const marker = new Marker().setLngLat([0, 0]).addTo(map);
296
290
 
291
+ expect(marker.getElement().getAttribute('role')).toBe('img');
292
+ map.remove();
293
+ });
294
+
295
+ test('default Marker uses role=button when interactive via popup', () => {
296
+ const map = createMap();
297
+ const marker = new Marker()
298
+ .setLngLat([0, 0])
299
+ .setPopup(new Popup().setText('popup'))
300
+ .addTo(map);
301
+
297
302
  expect(marker.getElement().getAttribute('role')).toBe('button');
303
+
304
+ marker.setPopup();
305
+ expect(marker.getElement().getAttribute('role')).toBe('img');
306
+ map.remove();
307
+ });
308
+
309
+ test('default Marker uses role=button when draggable', () => {
310
+ const map = createMap();
311
+ const marker = new Marker({draggable: true}).setLngLat([0, 0]).addTo(map);
312
+
313
+ expect(marker.getElement().getAttribute('role')).toBe('button');
314
+
315
+ marker.setDraggable(false);
316
+ expect(marker.getElement().getAttribute('role')).toBe('img');
317
+ map.remove();
318
+ });
319
+
320
+ test('custom Marker element does not get automatic accessibility attributes', () => {
321
+ const map = createMap();
322
+ const element = document.createElement('div');
323
+ const marker = new Marker({element}).setLngLat([0, 0]).addTo(map);
324
+
325
+ expect(marker.getElement().hasAttribute('aria-label')).toBe(false);
326
+ expect(marker.getElement().hasAttribute('role')).toBe(false);
327
+ map.remove();
328
+ });
329
+
330
+ test('explicit role on the default Marker element is preserved', () => {
331
+ const map = createMap();
332
+ const marker = new Marker().setLngLat([0, 0]);
333
+ marker.getElement().setAttribute('role', 'presentation');
334
+ marker.addTo(map);
335
+
336
+ expect(marker.getElement().getAttribute('role')).toBe('presentation');
337
+ map.remove();
298
338
  });
299
339
 
300
340
  test('Marker anchor defaults to center', () => {
@@ -491,6 +531,23 @@ describe('marker', () => {
491
531
  map.remove();
492
532
  });
493
533
 
534
+ test('Marker.setDraggable toggles the draggable cursor class', () => {
535
+ const map = createMap();
536
+ const marker = new Marker({draggable: true})
537
+ .setLngLat([0, 0])
538
+ .addTo(map);
539
+
540
+ expect(marker.getElement().classList).toContain('maplibregl-marker-draggable');
541
+
542
+ marker.setDraggable(false);
543
+ expect(marker.getElement().classList).not.toContain('maplibregl-marker-draggable');
544
+
545
+ marker.setDraggable(true);
546
+ expect(marker.getElement().classList).toContain('maplibregl-marker-draggable');
547
+
548
+ map.remove();
549
+ });
550
+
494
551
  test('Marker with draggable:true fires dragstart, drag, and dragend events at appropriate times in response to mouse-triggered drag with map-inherited clickTolerance', () => {
495
552
  const map = createMap();
496
553
  const marker = new Marker({draggable: true})
package/src/ui/marker.ts CHANGED
@@ -227,6 +227,7 @@ export class Marker extends Evented<MarkerEventType> {
227
227
  _opacityWhenCovered: string;
228
228
  _opacityTimeout: ReturnType<typeof setTimeout>;
229
229
  _subpixelPositioning: boolean;
230
+ _roleManaged: boolean;
230
231
 
231
232
  /**
232
233
  * @param options - the options
@@ -241,6 +242,7 @@ export class Marker extends Evented<MarkerEventType> {
241
242
  this._clickTolerance = options?.clickTolerance || 0;
242
243
  this._subpixelPositioning = options?.subpixelPositioning || false;
243
244
  this._isDragging = false;
245
+ this._roleManaged = false;
244
246
  this._state = 'inactive';
245
247
  this._rotation = options?.rotation || 0;
246
248
  this._rotationAlignment = options?.rotationAlignment || 'auto';
@@ -393,15 +395,11 @@ export class Marker extends Evented<MarkerEventType> {
393
395
  this.remove();
394
396
  this._map = map;
395
397
 
396
- if (!this._element.hasAttribute('aria-label')) {
398
+ if (this._defaultMarker && !this._element.hasAttribute('aria-label')) {
397
399
  this._element.setAttribute('aria-label', map._getUIString('Marker.Title'));
398
400
  }
399
401
 
400
- // aria-label is set either by user or above default, so set role
401
- // since div is interactive and cannot have aria-label without a role
402
- if (!this._element.hasAttribute('role')) {
403
- this._element.setAttribute('role', 'button');
404
- }
402
+ this._updateAccessibilityRole();
405
403
 
406
404
  map.getCanvasContainer().appendChild(this._element);
407
405
  map.on('move', this._update);
@@ -553,6 +551,7 @@ export class Marker extends Evented<MarkerEventType> {
553
551
  this._element.addEventListener('keypress', this._onKeyPress);
554
552
  }
555
553
 
554
+ this._updateAccessibilityRole();
556
555
  return this;
557
556
  }
558
557
 
@@ -857,6 +856,7 @@ export class Marker extends Evented<MarkerEventType> {
857
856
  */
858
857
  setDraggable(shouldBeDraggable?: boolean): this {
859
858
  this._draggable = !!shouldBeDraggable; // convert possible undefined value to false
859
+ this._element.classList.toggle('maplibregl-marker-draggable', this._draggable);
860
860
 
861
861
  // handle case where map may not exist yet
862
862
  // e.g. when setDraggable is called before addTo
@@ -870,6 +870,7 @@ export class Marker extends Evented<MarkerEventType> {
870
870
  }
871
871
  }
872
872
 
873
+ this._updateAccessibilityRole();
873
874
  return this;
874
875
  }
875
876
 
@@ -881,6 +882,29 @@ export class Marker extends Evented<MarkerEventType> {
881
882
  return this._draggable;
882
883
  }
883
884
 
885
+ /**
886
+ * Keep the default marker role aligned with interactivity.
887
+ * Default markers need a role because `aria-label` is set in {@link Marker.addTo}.
888
+ * Non-interactive markers use `role=img`; interactive ones (draggable or with a popup) use `role=button`.
889
+ * Click listeners are application-owned and do not automatically change the role.
890
+ * Custom marker elements are left alone so applications own their a11y tree.
891
+ * Explicit roles set by the application are preserved.
892
+ */
893
+ _updateAccessibilityRole(): void {
894
+ if (!this._defaultMarker) {
895
+ return;
896
+ }
897
+
898
+ // Preserve an explicit role chosen by the application unless we previously managed it.
899
+ if (this._element.hasAttribute('role') && !this._roleManaged) {
900
+ return;
901
+ }
902
+
903
+ const role = (this._draggable || !!this._popup) ? 'button' : 'img';
904
+ this._element.setAttribute('role', role);
905
+ this._roleManaged = true;
906
+ }
907
+
884
908
  /**
885
909
  * Sets the `rotation` property of the marker.
886
910
  * @param rotation - The rotation angle of the marker (clockwise, in degrees), relative to its respective {@link Marker.setRotationAlignment} setting.
@@ -333,7 +333,7 @@ describe('popup', () => {
333
333
  expect(popup._pos).toEqual(map.project([5, 0]));
334
334
  });
335
335
 
336
- test('Popup wraps position after map move if it would otherwise go offscreen (right)', () => {
336
+ test('Popup wraps position after map move if it would otherwise go offscreen (left)', () => {
337
337
  const map = createMap({width: 1024}); // longitude bounds: [-360, 360]
338
338
 
339
339
  const popup = new Popup()
@@ -613,7 +613,7 @@ describe('popup', () => {
613
613
  ).toContain('maplibregl-popup-track-pointer');
614
614
  });
615
615
 
616
- test('Pointer-tracked popup with content set later is tagged with right class ', () => {
616
+ test('Pointer-tracked popup with content set later is tagged with right class', () => {
617
617
  const map = createMap();
618
618
  const popup = new Popup()
619
619
  .trackPointer()
@@ -626,7 +626,7 @@ describe('popup', () => {
626
626
  ).toContain('maplibregl-popup-track-pointer');
627
627
  });
628
628
 
629
- test('Pointer-tracked popup that is set afterwards is tagged with right class ', () => {
629
+ test('Pointer-tracked popup that is set afterwards is tagged with right class', () => {
630
630
  const map = createMap();
631
631
  const popup = new Popup()
632
632
  .addTo(map);
@@ -144,7 +144,7 @@ describe('Actor', () => {
144
144
  const worker = await workerFactory() as any as WorkerGlobalScopeInterface & ActorTarget;
145
145
  const actor = new Actor(worker, '1');
146
146
 
147
- const spy = vi.fn().mockReturnValue(Promise.resolve({}));
147
+ const spy = vi.fn().mockResolvedValue({});
148
148
  worker.worker.actor.registerMessageHandler(MessageType.getClusterExpansionZoom, spy);
149
149
 
150
150
  worker.worker.actor.invoker.trigger = async () => {
@@ -209,7 +209,7 @@ describe('Actor', () => {
209
209
 
210
210
  worker.worker.actor.mapId = '2';
211
211
 
212
- const spy = vi.fn().mockReturnValue(Promise.resolve({}));
212
+ const spy = vi.fn().mockResolvedValue({});
213
213
  worker.worker.actor.registerMessageHandler(MessageType.getClusterExpansionZoom, spy);
214
214
 
215
215
  actor.sendAsync({type: MessageType.getClusterExpansionZoom, data: {} as any, targetMapId: '1'});
@@ -223,7 +223,7 @@ describe('Actor', () => {
223
223
  const worker = await workerFactory() as any as WorkerGlobalScopeInterface & ActorTarget;
224
224
  const actor = new Actor(worker, '1');
225
225
 
226
- const spy = vi.fn().mockReturnValue(Promise.resolve({}));
226
+ const spy = vi.fn().mockResolvedValue({});
227
227
  worker.worker.actor.registerMessageHandler(MessageType.getClusterExpansionZoom, spy);
228
228
 
229
229
  actor.target.postMessage({type: 'getClusterExpansionZoom', data: {} as any, origin: 'https://example.com'});
@@ -237,7 +237,7 @@ describe('Actor', () => {
237
237
  const worker = await workerFactory() as any as WorkerGlobalScopeInterface & ActorTarget;
238
238
  const actor = new Actor(worker, '1');
239
239
 
240
- const spy = vi.fn().mockReturnValue(Promise.resolve({}));
240
+ const spy = vi.fn().mockResolvedValue({});
241
241
  worker.worker.actor.registerMessageHandler(MessageType.getClusterExpansionZoom, spy);
242
242
 
243
243
  actor.target.postMessage({type: MessageType.getClusterExpansionZoom, data: {} as any, origin: 'null'});
@@ -251,7 +251,7 @@ describe('Actor', () => {
251
251
  const worker = await workerFactory() as any as WorkerGlobalScopeInterface & ActorTarget;
252
252
  const actor = new Actor(worker, '1');
253
253
 
254
- const spy = vi.fn().mockReturnValue(Promise.resolve({}));
254
+ const spy = vi.fn().mockResolvedValue({});
255
255
  worker.worker.actor.registerMessageHandler(MessageType.getClusterExpansionZoom, spy);
256
256
 
257
257
  actor.target.postMessage({type: MessageType.getClusterExpansionZoom, data: {} as any, origin: 'file://'});
@@ -265,7 +265,7 @@ describe('Actor', () => {
265
265
  const worker = await workerFactory() as any as WorkerGlobalScopeInterface & ActorTarget;
266
266
  const actor = new Actor(worker, '1');
267
267
 
268
- const spy = vi.fn().mockReturnValue(Promise.resolve({}));
268
+ const spy = vi.fn().mockResolvedValue({});
269
269
  worker.worker.actor.registerMessageHandler(MessageType.getClusterExpansionZoom, spy);
270
270
 
271
271
  actor.target.postMessage({type: MessageType.getClusterExpansionZoom, data: {} as any, origin: 'resource://android'});
@@ -20,6 +20,12 @@ function readAsText(blob) {
20
20
  });
21
21
  }
22
22
 
23
+ async function expectRejection(promise: Promise<unknown>): Promise<AJAXError> {
24
+ const error = await promise.catch((e) => e);
25
+ expect(error).toBeInstanceOf(Error);
26
+ return error as AJAXError;
27
+ }
28
+
23
29
  const originalFetch = global.fetch;
24
30
 
25
31
  describe('ajax', () => {
@@ -37,18 +43,14 @@ describe('ajax', () => {
37
43
  request.respond(404, undefined, '404 Not Found');
38
44
  });
39
45
 
40
- try {
41
- const promise = getArrayBuffer({url: 'http://example.com/test.bin'}, new AbortController());
42
- server.respond();
43
- await promise;
44
- } catch (error) {
45
- const ajaxError = error as AJAXError;
46
- const body = await readAsText(ajaxError.body);
47
- expect(ajaxError.status).toBe(404);
48
- expect(ajaxError.statusText).toBe('Not Found');
49
- expect(ajaxError.url).toBe('http://example.com/test.bin');
50
- expect(body).toBe('404 Not Found');
51
- }
46
+ const promise = getArrayBuffer({url: 'http://example.com/test.bin'}, new AbortController());
47
+ server.respond();
48
+
49
+ const ajaxError = await expectRejection(promise);
50
+ expect(ajaxError.status).toBe(404);
51
+ expect(ajaxError.statusText).toBe('Not Found');
52
+ expect(ajaxError.url).toBe('http://example.com/test.bin');
53
+ await expect(readAsText(ajaxError.body)).resolves.toBe('404 Not Found');
52
54
  });
53
55
 
54
56
  test('getJSON', async () => {
@@ -68,11 +70,8 @@ describe('ajax', () => {
68
70
  });
69
71
  const promise = getJSON({url: ''}, new AbortController());
70
72
  server.respond();
71
- try {
72
- await promise;
73
- } catch (error) {
74
- expect(error).toBeTruthy();
75
- }
73
+
74
+ await expect(promise).rejects.toThrow(SyntaxError);
76
75
  });
77
76
 
78
77
  test('getJSON, 404', async () => {
@@ -82,16 +81,11 @@ describe('ajax', () => {
82
81
  const promise = getJSON({url: 'http://example.com/test.json'}, new AbortController());
83
82
  server.respond();
84
83
 
85
- try {
86
- await promise;
87
- } catch (error) {
88
- const ajaxError = error as AJAXError;
89
- const body = await readAsText(ajaxError.body);
90
- expect(ajaxError.status).toBe(404);
91
- expect(ajaxError.statusText).toBe('Not Found');
92
- expect(ajaxError.url).toBe('http://example.com/test.json');
93
- expect(body).toBe('404 Not Found');
94
- }
84
+ const ajaxError = await expectRejection(promise);
85
+ expect(ajaxError.status).toBe(404);
86
+ expect(ajaxError.statusText).toBe('Not Found');
87
+ expect(ajaxError.url).toBe('http://example.com/test.json');
88
+ await expect(readAsText(ajaxError.body)).resolves.toBe('404 Not Found');
95
89
  });
96
90
 
97
91
  test('getJSON, aborted', async () => {
@@ -103,12 +97,9 @@ describe('ajax', () => {
103
97
  abortController.abort();
104
98
  server.respond();
105
99
 
106
- try {
107
- await promise;
108
- } catch (error) {
109
- expect(ensureError(error).name).toBe('AbortError');
110
- expect(isAbortError(error)).toBe(true);
111
- }
100
+ const error = await expectRejection(promise);
101
+ expect(ensureError(error).name).toBe('AbortError');
102
+ expect(isAbortError(error)).toBe(true);
112
103
  });
113
104
 
114
105
  test('getJSON with fetch, aborted', async () => {
@@ -126,12 +117,9 @@ describe('ajax', () => {
126
117
  abortController.abort();
127
118
  server.respond();
128
119
 
129
- try {
130
- await promise;
131
- } catch (error) {
132
- expect(ensureError(error).name).toBe('AbortError');
133
- expect(isAbortError(error)).toBe(true);
134
- }
120
+ const error = await expectRejection(promise);
121
+ expect(ensureError(error).name).toBe('AbortError');
122
+ expect(isAbortError(error)).toBe(true);
135
123
  });
136
124
 
137
125
  test('getReferrer method (outside Worker), same origin https URL', () => {