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
@@ -1,16 +1,15 @@
1
- import {describe, beforeEach, test, expect, vi} from 'vitest';
1
+ import {describe, beforeEach, afterEach, test, expect, vi, type Mock} from 'vitest';
2
2
  import {ImageSource} from './image_source.ts';
3
- import {Evented} from '../util/evented.ts';
4
- import {type IReadonlyTransform} from '../geo/transform_interface.ts';
5
3
  import {extend, MAX_TILE_ZOOM} from '../util/util.ts';
6
4
  import {type FakeServer, fakeServer} from 'nise';
7
- import {type RequestManager} from '../util/request_manager.ts';
8
- import {sleep, stubAjaxGetImage, waitForEvent} from '../util/test/util.ts';
5
+ import {beforeMapTest, createMap, sleep, stubAjaxGetImage, waitForEvent} from '../util/test/util.ts';
9
6
  import {Tile} from '../tile/tile.ts';
10
7
  import {OverscaledTileID} from '../tile/tile_id.ts';
11
- import {type Texture} from '../webgl/texture.ts';
8
+ import {ImageRequest} from '../util/image_request.ts';
9
+ import type {Texture} from '../webgl/texture.ts';
12
10
  import type {ImageSourceSpecification} from '@maplibre/maplibre-gl-style-spec';
13
- import {MercatorTransform} from '../geo/projection/mercator_transform.ts';
11
+ import type {MapSourceDataEvent} from '../ui/events.ts';
12
+ import type {Map} from '../ui/map.ts';
14
13
 
15
14
  function createSource(options) {
16
15
  options = extend({
@@ -20,36 +19,23 @@ function createSource(options) {
20
19
  return new ImageSource('id', options, {} as any, options.eventedParent);
21
20
  }
22
21
 
23
- class StubMap extends Evented {
24
- transform: IReadonlyTransform;
25
- painter: any;
26
- _requestManager: RequestManager;
27
-
28
- constructor() {
29
- super();
30
- this.transform = new MercatorTransform();
31
- this._requestManager = {
32
- transformRequest: (url) => {
33
- return {url};
34
- }
35
- } as any as RequestManager;
36
- this.painter = {
37
- context: {
38
- gl: {}
39
- }
40
- };
41
- }
42
- }
43
-
44
22
  describe('ImageSource', () => {
45
23
  stubAjaxGetImage(undefined);
46
24
  let server: FakeServer;
25
+ let map: Map;
47
26
 
48
27
  beforeEach(() => {
28
+ beforeMapTest();
49
29
  global.fetch = null;
50
30
  server = fakeServer.create();
51
31
  server.respondWith(new ArrayBuffer(1));
52
32
  server.respondWith('/missing-image.png', [404, {}, '']);
33
+ map = createMap({style: null});
34
+ });
35
+
36
+ afterEach(() => {
37
+ map.remove();
38
+ vi.restoreAllMocks();
53
39
  });
54
40
 
55
41
  test('constructor', () => {
@@ -65,33 +51,66 @@ describe('ImageSource', () => {
65
51
  source.on('dataloading', (e) => {
66
52
  expect(e.dataType).toBe('source');
67
53
  });
68
- source.onAdd(new StubMap() as any);
54
+ source.onAdd(map);
69
55
  await sleep(0);
70
56
  server.respond();
71
57
  await sleep(0);
72
58
  expect(source.image).toBeTruthy();
73
59
  });
74
60
 
61
+ test('passes a live AbortController to ImageRequest when the source is aborted during an async transformRequest', async () => {
62
+ const source = createSource({url: '/image.png'});
63
+ let transformStarted: () => void;
64
+ const transformCalled = new Promise<void>((resolve) => {
65
+ transformStarted = resolve;
66
+ });
67
+ let releaseTransform: (params: {url: string}) => void;
68
+ map.setTransformRequest(() => {
69
+ transformStarted();
70
+ return new Promise<{url: string}>((resolve) => {
71
+ releaseTransform = resolve;
72
+ });
73
+ });
74
+ const image = {width: 1, height: 1} as ImageBitmap;
75
+ let requestController: AbortController;
76
+ let sourceControllerAtRequestTime: AbortController;
77
+ const getImageSpy = vi.spyOn(ImageRequest, 'getImage').mockImplementation(async (_request, abortController) => {
78
+ requestController = abortController;
79
+ sourceControllerAtRequestTime = source._request;
80
+ return {data: image};
81
+ });
82
+
83
+ source.onAdd(map);
84
+ await transformCalled; // load() is suspended in the transform
85
+ source.onRemove(); // the abort lands during that suspension
86
+ const loaded = waitForEvent(source, 'data', (e: MapSourceDataEvent) => e.sourceDataType === 'metadata');
87
+ releaseTransform({url: '/image.png'});
88
+ await loaded;
89
+
90
+ expect(getImageSpy).toHaveBeenCalledTimes(1);
91
+ // The request must carry the source's own live controller, so an abort
92
+ // arriving while it is in flight reaches exactly this request.
93
+ expect(requestController).toBeInstanceOf(AbortController);
94
+ expect(requestController).toBe(sourceControllerAtRequestTime);
95
+ });
96
+
75
97
  test('transforms url request', () => {
98
+ const transformRequest = vi.fn((url: string, _resourceType?: string) => ({url}));
76
99
  const source = createSource({url: '/image.png'});
77
- const map = new StubMap() as any;
78
- const spy = vi.spyOn(map._requestManager, 'transformRequest');
100
+ map.setTransformRequest(transformRequest);
79
101
  source.onAdd(map);
80
102
  server.respond();
81
- expect(spy).toHaveBeenCalledTimes(1);
82
- expect(spy.mock.calls[0][0]).toBe('/image.png');
83
- expect(spy.mock.calls[0][1]).toBe('Image');
103
+ expect(transformRequest).toHaveBeenCalledTimes(1);
104
+ expect(transformRequest.mock.calls[0][0]).toBe('/image.png');
105
+ expect(transformRequest.mock.calls[0][1]).toBe('Image');
84
106
  });
85
107
 
86
108
  test('can asynchronously transform request', async () => {
87
109
  const source = createSource({url: '/image.png'});
88
- const map = new StubMap() as any;
89
- map._requestManager = {
90
- transformRequest: async (url) => ({
91
- url,
92
- headers: {Authorization: 'Bearer token'}
93
- })
94
- };
110
+ map.setTransformRequest(async (url) => ({
111
+ url,
112
+ headers: {Authorization: 'Bearer token'}
113
+ }));
95
114
  const promise = source.once('data');
96
115
  source.onAdd(map);
97
116
  await sleep(0);
@@ -102,24 +121,23 @@ describe('ImageSource', () => {
102
121
  });
103
122
 
104
123
  test('updates url from updateImage', () => {
124
+ const transformRequest = vi.fn((url: string, _resourceType?: string) => ({url}));
105
125
  const source = createSource({url: '/image.png'});
106
- const map = new StubMap() as any;
107
- const spy = vi.spyOn(map._requestManager, 'transformRequest');
126
+ map.setTransformRequest(transformRequest);
108
127
  source.onAdd(map);
109
128
  server.respond();
110
- expect(spy).toHaveBeenCalledTimes(1);
111
- expect(spy.mock.calls[0][0]).toBe('/image.png');
112
- expect(spy.mock.calls[0][1]).toBe('Image');
129
+ expect(transformRequest).toHaveBeenCalledTimes(1);
130
+ expect(transformRequest.mock.calls[0][0]).toBe('/image.png');
131
+ expect(transformRequest.mock.calls[0][1]).toBe('Image');
113
132
  source.updateImage({url: '/image2.png'});
114
133
  server.respond();
115
- expect(spy).toHaveBeenCalledTimes(2);
116
- expect(spy.mock.calls[1][0]).toBe('/image2.png');
117
- expect(spy.mock.calls[1][1]).toBe('Image');
134
+ expect(transformRequest).toHaveBeenCalledTimes(2);
135
+ expect(transformRequest.mock.calls[1][0]).toBe('/image2.png');
136
+ expect(transformRequest.mock.calls[1][1]).toBe('Image');
118
137
  });
119
138
 
120
139
  test('sets coordinates', () => {
121
140
  const source = createSource({url: '/image.png'});
122
- const map = new StubMap() as any;
123
141
  source.onAdd(map);
124
142
  server.respond();
125
143
  const beforeSerialized = source.serialize();
@@ -131,7 +149,6 @@ describe('ImageSource', () => {
131
149
 
132
150
  test('sets coordinates via updateImage', async () => {
133
151
  const source = createSource({url: '/image.png'});
134
- const map = new StubMap() as any;
135
152
  source.onAdd(map);
136
153
  server.respond();
137
154
  const beforeSerialized = source.serialize();
@@ -150,7 +167,7 @@ describe('ImageSource', () => {
150
167
  test('fires data event when content is loaded', async () => {
151
168
  const source = createSource({url: '/image.png'});
152
169
  const promise = waitForEvent(source, 'data', (e) => e.dataType === 'source' && e.sourceDataType === 'content');
153
- source.onAdd(new StubMap() as any);
170
+ source.onAdd(map);
154
171
  await sleep(0);
155
172
  server.respond();
156
173
  await promise;
@@ -160,7 +177,7 @@ describe('ImageSource', () => {
160
177
  test('fires data event when metadata is loaded', async () => {
161
178
  const source = createSource({url: '/image.png'});
162
179
  const promise = waitForEvent(source, 'data', (e) => e.dataType === 'source' && e.sourceDataType === 'metadata');
163
- source.onAdd(new StubMap() as any);
180
+ source.onAdd(map);
164
181
  await sleep(0);
165
182
  server.respond();
166
183
  await expect(promise).resolves.toBeDefined();
@@ -170,7 +187,7 @@ describe('ImageSource', () => {
170
187
  const source = createSource({url: '/image.png'});
171
188
  const tile = new Tile(new OverscaledTileID(1, 0, 1, 0, 0), 512);
172
189
  const promise = waitForEvent(source, 'data', (e) => e.dataType === 'source' && e.sourceDataType === 'idle');
173
- source.onAdd(new StubMap() as any);
190
+ source.onAdd(map);
174
191
  server.respond();
175
192
 
176
193
  source.tiles[String(tile.tileID.wrap)] = tile;
@@ -192,7 +209,6 @@ describe('ImageSource', () => {
192
209
  });
193
210
 
194
211
  test('allows using updateImage before initial image is loaded', async () => {
195
- const map = new StubMap() as any;
196
212
  const source = createSource({url: '/image.png', eventedParent: map});
197
213
 
198
214
  // Suppress errors because we're aborting when updating.
@@ -208,7 +224,6 @@ describe('ImageSource', () => {
208
224
  });
209
225
 
210
226
  test('cancels request if updateImage is used', async () => {
211
- const map = new StubMap() as any;
212
227
  const source = createSource({url: '/image.png', eventedParent: map});
213
228
 
214
229
  // Suppress errors because we're aborting.
@@ -223,7 +238,6 @@ describe('ImageSource', () => {
223
238
  });
224
239
 
225
240
  test('marks the source as loaded when the request has received a response', async () => {
226
- const map = new StubMap() as any;
227
241
  const source = createSource({url: '/image.png', eventedParent: map});
228
242
 
229
243
  expect(source.loaded()).toBe(false);
@@ -248,7 +262,6 @@ describe('ImageSource', () => {
248
262
  });
249
263
 
250
264
  test('does not throw when updateImage is called while a request is pending', async () => {
251
- const map = new StubMap() as any;
252
265
  const source = createSource({url: '/image.png', eventedParent: map});
253
266
 
254
267
  const errorHandler = vi.fn();
@@ -262,10 +275,75 @@ describe('ImageSource', () => {
262
275
  expect(errorHandler).not.toHaveBeenCalled();
263
276
  });
264
277
 
278
+ describe('updateImage with a decoded image', () => {
279
+ let source: ImageSource;
280
+ let transformRequest: Mock<(url: string, resourceType?: string) => any>;
281
+
282
+ beforeEach(() => {
283
+ transformRequest = vi.fn((url: string, _resourceType?: string) => ({url}));
284
+ map.setTransformRequest(transformRequest);
285
+ // Suppress errors from aborting the initial (never-responded) request.
286
+ map.on('error', () => {});
287
+ source = createSource({url: '/image.png', eventedParent: map});
288
+ // onAdd starts the initial load synchronously up to its first await, so
289
+ // this._request is set and transformRequest is called once. Clear that call
290
+ // so tests can assert the image path issues no further request.
291
+ source.onAdd(map);
292
+ transformRequest.mockClear();
293
+ });
294
+
295
+ test('sets the image directly without a network request and fires metadata', () => {
296
+ const handler = vi.fn();
297
+ source.on('data', handler);
298
+ const bitmap = new ImageBitmap();
299
+ const result = source.updateImage({image: bitmap});
300
+
301
+ expect(result).toBe(source);
302
+ // The image path must not trigger a request.
303
+ expect(transformRequest).not.toHaveBeenCalled();
304
+ expect(source.image).toBe(bitmap);
305
+ expect(source.loaded()).toBe(true);
306
+ const firedMetadata = handler.mock.calls.some(
307
+ ([e]) => e.dataType === 'source' && e.sourceDataType === 'metadata'
308
+ );
309
+ expect(firedMetadata).toBe(true);
310
+ });
311
+
312
+ test('resets the texture so the new image is uploaded on the next prepare', () => {
313
+ source.texture = {} as Texture;
314
+ source.updateImage({image: new ImageBitmap()});
315
+
316
+ expect(source.texture).toBeNull();
317
+ });
318
+
319
+ test('updates coordinates alongside the image', () => {
320
+ source.updateImage({
321
+ image: new ImageBitmap(),
322
+ coordinates: [[0, 0], [-1, 0], [-1, -1], [0, -1]]
323
+ });
324
+
325
+ expect(source.serialize().coordinates).toEqual([[0, 0], [-1, 0], [-1, -1], [0, -1]]);
326
+ });
327
+
328
+ test('cancels a pending request', () => {
329
+ const spy = vi.spyOn(source._request, 'abort');
330
+ source.updateImage({image: new ImageBitmap()});
331
+ expect(spy).toHaveBeenCalled();
332
+ });
333
+
334
+ test('accepts an ImageData instance', () => {
335
+ const imageData = new ImageData(1, 1);
336
+ source.updateImage({image: imageData});
337
+
338
+ expect(transformRequest).not.toHaveBeenCalled();
339
+ expect(source.image).toBe(imageData);
340
+ expect(source.loaded()).toBe(true);
341
+ });
342
+ });
343
+
265
344
  describe('terrainTileRanges', () => {
266
345
  test('sets tile ranges for all zoom levels', () => {
267
346
  const source = createSource({url: '/image.png'});
268
- const map = new StubMap() as any;
269
347
  source.onAdd(map);
270
348
  server.respond();
271
349
  source.setCoordinates([[-10, 10], [10, 10], [10, -10], [-10, -10]]);
@@ -277,7 +355,6 @@ describe('ImageSource', () => {
277
355
 
278
356
  test('calculates tile ranges properly', () => {
279
357
  const source = createSource({url: '/image.png'});
280
- const map = new StubMap() as any;
281
358
  source.onAdd(map);
282
359
  server.respond();
283
360
  source.setCoordinates([[11.39585,47.30074],[11.46585,47.30074],[11.46585,47.25074],[11.39585,47.25074]]);
@@ -317,7 +394,6 @@ describe('ImageSource', () => {
317
394
 
318
395
  test('calculates tile ranges for an image exceeds the world bounds - east', () => {
319
396
  const source = createSource({url: '/image.png'});
320
- const map = new StubMap() as any;
321
397
  source.onAdd(map);
322
398
  server.respond();
323
399
  source.setCoordinates([[-180, 60], [270, 60], [270, -60], [-180, -60]]);
@@ -341,7 +417,6 @@ describe('ImageSource', () => {
341
417
 
342
418
  test('calculates tile ranges for an image exceeds the world bounds - west', () => {
343
419
  const source = createSource({url: '/image.png'});
344
- const map = new StubMap() as any;
345
420
  source.onAdd(map);
346
421
  server.respond();
347
422
  source.setCoordinates([[120, 60], [-270, 60], [-270, -60], [120, -60]]);
@@ -29,18 +29,34 @@ import {isAbortError} from '../util/abort_error.ts';
29
29
  export type Coordinates = [[number, number], [number, number], [number, number], [number, number]];
30
30
 
31
31
  /**
32
- * The options object for the {@link ImageSource.updateImage} method
32
+ * An already-decoded image that can be handed to an {@link ImageSource} directly,
33
+ * without a network request.
34
+ */
35
+ export type ImageSourceImage = HTMLImageElement | HTMLCanvasElement | ImageBitmap | ImageData;
36
+
37
+ /**
38
+ * The options object for the {@link ImageSource.updateImage} method.
39
+ *
40
+ * Provide exactly one of `url` (to load an image over the network) or `image`
41
+ * (an already-decoded image to display directly, without a network request).
33
42
  */
34
43
  export type UpdateImageOptions = {
35
44
  /**
36
- * Required image URL.
45
+ * The image coordinates
46
+ */
47
+ coordinates?: Coordinates;
48
+ } & ({
49
+ /**
50
+ * The image URL to load.
37
51
  */
38
52
  url: string;
53
+ } | {
39
54
  /**
40
- * The image coordinates
55
+ * An already-decoded image (`HTMLImageElement`, `HTMLCanvasElement`, `ImageBitmap` or `ImageData`)
56
+ * to display directly, without a network request.
41
57
  */
42
- coordinates?: Coordinates;
43
- };
58
+ image: ImageSourceImage;
59
+ });
44
60
 
45
61
  export type CanonicalTileRange = {
46
62
  minTileY: number;
@@ -96,6 +112,10 @@ export type CanonicalTileRange = {
96
112
  * ]
97
113
  * })
98
114
  *
115
+ * // update with an already-decoded image (no network request)
116
+ * const bitmap = await createImageBitmap(myCanvas);
117
+ * mySource.updateImage({image: bitmap});
118
+ *
99
119
  * map.removeSource('some id'); // remove
100
120
  * ```
101
121
  */
@@ -118,7 +138,7 @@ export class ImageSource extends Evented<SourceEventType> implements Source {
118
138
  dispatcher: Dispatcher;
119
139
  map: Map;
120
140
  texture: Texture | null;
121
- image: HTMLImageElement | ImageBitmap;
141
+ image: ImageSourceImage;
122
142
  tileID: CanonicalTileID;
123
143
  tileCoords: Point[];
124
144
  flippedWindingOrder: boolean = false;
@@ -150,9 +170,10 @@ export class ImageSource extends Evented<SourceEventType> implements Source {
150
170
 
151
171
  this.url = this.options.url;
152
172
 
173
+ const request = await this.map._requestManager.transformRequest(this.url, ResourceType.Image);
153
174
  this._request = new AbortController();
154
175
  try {
155
- const image = await ImageRequest.getImage(await this.map._requestManager.transformRequest(this.url, ResourceType.Image), this._request);
176
+ const image = await ImageRequest.getImage(request, this._request);
156
177
  this._request = null;
157
178
  this._loaded = true;
158
179
 
@@ -177,21 +198,37 @@ export class ImageSource extends Evented<SourceEventType> implements Source {
177
198
  }
178
199
 
179
200
  /**
180
- * Updates the image URL and, optionally, the coordinates. To avoid having the image flash after changing,
201
+ * Updates the image and, optionally, the coordinates. To avoid having the image flash after changing,
181
202
  * set the `raster-fade-duration` paint property on the raster layer to 0.
182
203
  *
204
+ * Provide exactly one of `url` (to fetch a new image over the network) or `image` (an
205
+ * already-decoded `HTMLImageElement`, `HTMLCanvasElement`, `ImageBitmap` or `ImageData` to
206
+ * display directly, without a network request).
207
+ *
183
208
  * @param options - The options object.
184
209
  */
185
210
  updateImage(options: UpdateImageOptions): this {
186
- if (!options.url) {
187
- return this;
188
- }
189
-
190
211
  if (this._request) {
191
212
  this._request.abort();
192
213
  this._request = null;
193
214
  }
194
215
 
216
+ if ('image' in options) {
217
+ // Use the already-decoded image directly, skipping the network request.
218
+ this._loaded = true;
219
+ this.image = options.image;
220
+ if (options.coordinates) {
221
+ this.coordinates = options.coordinates;
222
+ }
223
+ this.texture = null;
224
+ this._finishLoading();
225
+ return this;
226
+ }
227
+
228
+ if (!options.url) {
229
+ return this;
230
+ }
231
+
195
232
  this.options.url = options.url;
196
233
  this.load(options.coordinates).finally(() => this.texture = null);
197
234
  return this;
@@ -1,4 +1,4 @@
1
- import {describe, beforeEach, afterEach, test, expect, vi, it} from 'vitest';
1
+ import {describe, beforeEach, afterEach, test, expect, vi} from 'vitest';
2
2
  import {fakeServer, type FakeServer} from 'nise';
3
3
  import {RasterDEMTileSource} from './raster_dem_tile_source.ts';
4
4
  import {OverscaledTileID} from '../tile/tile_id.ts';
@@ -198,7 +198,7 @@ describe('RasterDEMTileSource', () => {
198
198
  ]);
199
199
  });
200
200
 
201
- it('serializes options', () => {
201
+ test('serializes options', () => {
202
202
  const source = createSource({
203
203
  tiles: ['http://localhost:2900/raster-dem/{z}/{x}/{y}.png'],
204
204
  minzoom: 2,
@@ -1,4 +1,4 @@
1
- import {describe, beforeEach, afterEach, test, expect, vi, it} from 'vitest';
1
+ import {describe, beforeEach, afterEach, test, expect, vi} from 'vitest';
2
2
  import {RasterTileSource} from './raster_tile_source.ts';
3
3
  import {OverscaledTileID} from '../tile/tile_id.ts';
4
4
  import {RequestManager} from '../util/request_manager.ts';
@@ -216,15 +216,21 @@ describe('RasterTileSource', () => {
216
216
  expect(tile.state).toBe('loaded');
217
217
  });
218
218
 
219
- test('supports updating tiles', () => {
219
+ test('supports updating tiles', async () => {
220
+ server.respondWith('/source.json', JSON.stringify({
221
+ minzoom: 0,
222
+ maxzoom: 22,
223
+ attribution: 'MapLibre',
224
+ tiles: ['http://example.com/{z}/{x}/{y}.png'],
225
+ }));
220
226
  const source = createSource({url: '/source.json'});
227
+
228
+ server.respondImmediately = true;
229
+ const promise = waitForEvent(source, 'data', (e: MapSourceDataEvent) => e.sourceDataType === 'metadata');
221
230
  source.setTiles(['http://example.com/{z}/{x}/{y}.png?updated=true']);
231
+ await promise;
222
232
 
223
- source.on('data', (e) => {
224
- if (e.sourceDataType === 'metadata') {
225
- expect(source.tiles[0]).toBe('http://example.com/{z}/{x}/{y}.png?updated=true');
226
- }
227
- });
233
+ expect(source.tiles[0]).toBe('http://example.com/{z}/{x}/{y}.png?updated=true');
228
234
  });
229
235
 
230
236
  test('cancels TileJSON request if removed', async () => {
@@ -256,7 +262,7 @@ describe('RasterTileSource', () => {
256
262
 
257
263
  await waitForEvent(source, 'data', (e: MapSourceDataEvent) => e.sourceDataType === 'metadata');
258
264
 
259
- expect(server.requests.length).toBe(2);
265
+ expect(server.requests).toHaveLength(2);
260
266
  expect(server.requests[0].aborted).toBe(true);
261
267
  expect(source.serialize()).toEqual({
262
268
  type: 'raster',
@@ -265,7 +271,7 @@ describe('RasterTileSource', () => {
265
271
  expect(errorHandler).not.toHaveBeenCalled();
266
272
  });
267
273
 
268
- it('serializes options', () => {
274
+ test('serializes options', () => {
269
275
  const source = createSource({
270
276
  tiles: ['http://localhost:2900/raster/{z}/{x}/{y}.png'],
271
277
  minzoom: 2,
@@ -439,6 +445,55 @@ describe('RasterTileSource', () => {
439
445
  expect(expiryDataSpy).toHaveBeenCalledTimes(1);
440
446
  });
441
447
 
448
+ test('passes a live AbortController to ImageRequest when the tile is aborted during an async transformRequest', async () => {
449
+ let transformStarted: () => void;
450
+ const transformCalled = new Promise<void>((resolve) => {
451
+ transformStarted = resolve;
452
+ });
453
+ let releaseTransform: (params: {url: string}) => void;
454
+ const source = createSource({
455
+ tiles: ['http://example.com/{z}/{x}/{y}.png']
456
+ }, (url, type) => {
457
+ if (type !== 'Tile') return {url};
458
+ transformStarted();
459
+ return new Promise<{url: string}>((resolve) => {
460
+ releaseTransform = resolve;
461
+ });
462
+ });
463
+ source.tiles = ['http://example.com/{z}/{x}/{y}.png'];
464
+
465
+ const image = {width: 256, height: 256} as ImageBitmap;
466
+ let requestController: AbortController;
467
+ let tileControllerAtRequestTime: AbortController;
468
+ const getImageSpy = vi.spyOn(ImageRequest, 'getImage').mockImplementation(async (_request, abortController) => {
469
+ requestController = abortController;
470
+ tileControllerAtRequestTime = tile.abortController;
471
+ return {data: image};
472
+ });
473
+ source.map.painter = {
474
+ context: {gl: {}},
475
+ getTileTexture: () => ({update: vi.fn()})
476
+ } as any;
477
+
478
+ const tile = {
479
+ tileID: new OverscaledTileID(10, 0, 10, 5, 5),
480
+ state: 'loading',
481
+ setExpiryData() {}
482
+ } as any as Tile;
483
+
484
+ const loadPromise = source.loadTile(tile);
485
+ await transformCalled; // loadTile is suspended in the transform
486
+ await source.abortTile(tile); // the abort lands during that suspension
487
+ releaseTransform({url: 'http://example.com/10/5/5.png'});
488
+ await expect(loadPromise).resolves.toBeUndefined();
489
+
490
+ expect(getImageSpy).toHaveBeenCalledTimes(1);
491
+ // The request must carry the tile's own live controller, so an abort
492
+ // arriving while it is in flight reaches exactly this request.
493
+ expect(requestController).toBeInstanceOf(AbortController);
494
+ expect(requestController).toBe(tileControllerAtRequestTime);
495
+ });
496
+
442
497
  test('does not throw when tile is aborted', async () => {
443
498
  const source = createSource({
444
499
  minzoom: 0,
@@ -206,10 +206,11 @@ export class RasterTileSource extends Evented<SourceEventType> implements Source
206
206
  const url = tile.tileID.canonical.url(this.tiles, this.map.getPixelRatio(), this.scheme);
207
207
  const premultiply = this._premultiplyAlpha;
208
208
  const imageBitmapOptions = premultiply ? undefined : {premultiplyAlpha: 'none'} as const;
209
+ const request = await this.map._requestManager.transformRequest(url, ResourceType.Tile);
209
210
  tile.abortController = new AbortController();
210
211
  try {
211
212
  const response = await ImageRequest.getImage(
212
- await this.map._requestManager.transformRequest(url, ResourceType.Tile),
213
+ request,
213
214
  tile.abortController,
214
215
  this.map._refreshExpiredTiles,
215
216
  imageBitmapOptions