maplibre-gl 3.3.1 → 3.4.1

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 (46) hide show
  1. package/README.md +1 -1
  2. package/dist/maplibre-gl-csp-worker.js +1 -1
  3. package/dist/maplibre-gl-csp-worker.js.map +1 -1
  4. package/dist/maplibre-gl-csp.js +1 -1
  5. package/dist/maplibre-gl-csp.js.map +1 -1
  6. package/dist/maplibre-gl-dev.js +430 -128
  7. package/dist/maplibre-gl-dev.js.map +1 -1
  8. package/dist/maplibre-gl.d.ts +18 -9
  9. package/dist/maplibre-gl.js +4 -4
  10. package/dist/maplibre-gl.js.map +1 -1
  11. package/package.json +41 -41
  12. package/src/data/dem_data.test.ts +120 -165
  13. package/src/data/dem_data.ts +38 -18
  14. package/src/render/glyph_manager.test.ts +10 -9
  15. package/src/render/glyph_manager.ts +17 -10
  16. package/src/source/image_source.test.ts +17 -24
  17. package/src/source/raster_dem_tile_source.ts +36 -11
  18. package/src/source/raster_dem_tile_worker_source.ts +9 -26
  19. package/src/source/raster_tile_source.test.ts +1 -1
  20. package/src/source/raster_tile_source.ts +1 -1
  21. package/src/source/terrain_source_cache.test.ts +1 -1
  22. package/src/source/vector_tile_source.test.ts +1 -1
  23. package/src/source/vector_tile_worker_source.test.ts +45 -1
  24. package/src/source/vector_tile_worker_source.ts +19 -6
  25. package/src/source/worker_source.ts +6 -2
  26. package/src/style/load_glyph_range.test.ts +6 -8
  27. package/src/style/load_sprite.test.ts +48 -71
  28. package/src/style/style.test.ts +19 -49
  29. package/src/style/style.ts +3 -0
  30. package/src/style/style_glyph.ts +4 -3
  31. package/src/style/style_layer/line_style_layer.test.ts +50 -0
  32. package/src/style/style_layer/line_style_layer.ts +8 -4
  33. package/src/symbol/quads.ts +4 -2
  34. package/src/ui/handler/scroll_zoom.ts +6 -0
  35. package/src/ui/handler_manager.ts +2 -1
  36. package/src/ui/map.test.ts +17 -0
  37. package/src/ui/map.ts +1 -0
  38. package/src/ui/marker.test.ts +25 -0
  39. package/src/ui/marker.ts +8 -1
  40. package/src/util/ajax.test.ts +1 -1
  41. package/src/util/image_request.test.ts +1 -1
  42. package/src/util/offscreen_canvas_distorted.test.ts +13 -0
  43. package/src/util/offscreen_canvas_distorted.ts +39 -0
  44. package/src/util/test/util.ts +12 -0
  45. package/src/util/util.test.ts +171 -1
  46. package/src/util/util.ts +150 -0
@@ -2,18 +2,25 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import {RequestManager} from '../util/request_manager';
4
4
  import {loadSprite} from './load_sprite';
5
- import {fakeXhr} from 'nise';
5
+ import {type FakeServer, fakeServer} from 'nise';
6
6
  import * as util from '../util/util';
7
+ import {bufferToArrayBuffer} from '../util/test/util';
7
8
 
8
9
  describe('loadSprite', () => {
9
- jest.spyOn(util, 'arrayBufferToImageBitmap').mockImplementation((data: ArrayBuffer, callback: (err?: Error | null, image?: ImageBitmap | null) => void) => {
10
- createImageBitmap(new ImageData(1024, 824)).then((imgBitmap) => {
11
- callback(null, imgBitmap);
12
- }).catch((e) => {
13
- callback(new Error(`Could not load image because of ${e.message}. Please make sure to use a supported image type such as PNG or JPEG. Note that SVGs are not supported.`));
10
+
11
+ let server: FakeServer;
12
+
13
+ beforeEach(() => {
14
+ jest.spyOn(util, 'arrayBufferToImageBitmap').mockImplementation((data: ArrayBuffer, callback: (err?: Error | null, image?: ImageBitmap | null) => void) => {
15
+ createImageBitmap(new ImageData(1024, 824)).then((imgBitmap) => {
16
+ callback(null, imgBitmap);
17
+ }).catch((e) => {
18
+ callback(new Error(`Could not load image because of ${e.message}. Please make sure to use a supported image type such as PNG or JPEG. Note that SVGs are not supported.`));
19
+ });
14
20
  });
21
+ global.fetch = null;
22
+ server = fakeServer.create();
15
23
  });
16
- global.fetch = null;
17
24
 
18
25
  test('backwards compatibility: single string is treated as a URL for the default sprite', done => {
19
26
  const transform = jest.fn().mockImplementation((url, type) => {
@@ -22,8 +29,8 @@ describe('loadSprite', () => {
22
29
 
23
30
  const manager = new RequestManager(transform);
24
31
 
25
- const requests = [];
26
- fakeXhr.useFakeXMLHttpRequest().onCreate = (req) => { requests.push(req); };
32
+ server.respondWith('GET', 'http://localhost:9966/test/unit/assets/sprite1.json', fs.readFileSync(path.join(__dirname, '../../test/unit/assets/sprite1.json')).toString());
33
+ server.respondWith('GET', 'http://localhost:9966/test/unit/assets/sprite1.png', bufferToArrayBuffer(fs.readFileSync(path.join(__dirname, '../../test/unit/assets/sprite1.png'))));
27
34
 
28
35
  loadSprite('http://localhost:9966/test/unit/assets/sprite1', manager, 1, (err, result) => {
29
36
  expect(err).toBeFalsy();
@@ -43,15 +50,10 @@ describe('loadSprite', () => {
43
50
  done();
44
51
  });
45
52
 
46
- expect(requests[0].url).toBe('http://localhost:9966/test/unit/assets/sprite1.json');
47
- requests[0].setStatus(200);
48
- requests[0].response = fs.readFileSync(path.join(__dirname, '../../test/unit/assets/sprite1.json'));
49
- requests[0].onload();
53
+ server.respond();
50
54
 
51
- expect(requests[1].url).toBe('http://localhost:9966/test/unit/assets/sprite1.png');
52
- requests[1].setStatus(200);
53
- requests[1].response = fs.readFileSync(path.join(__dirname, '../../test/unit/assets/sprite1.png')).buffer;
54
- requests[1].onload();
55
+ expect(server.requests[0].url).toBe('http://localhost:9966/test/unit/assets/sprite1.json');
56
+ expect(server.requests[1].url).toBe('http://localhost:9966/test/unit/assets/sprite1.png');
55
57
  });
56
58
 
57
59
  test('array of objects support', done => {
@@ -61,8 +63,10 @@ describe('loadSprite', () => {
61
63
 
62
64
  const manager = new RequestManager(transform);
63
65
 
64
- const requests = [];
65
- fakeXhr.useFakeXMLHttpRequest().onCreate = (req) => { requests.push(req); };
66
+ server.respondWith('GET', 'http://localhost:9966/test/unit/assets/sprite1.json', fs.readFileSync(path.join(__dirname, '../../test/unit/assets/sprite1.json')).toString());
67
+ server.respondWith('GET', 'http://localhost:9966/test/unit/assets/sprite1.png', bufferToArrayBuffer(fs.readFileSync(path.join(__dirname, '../../test/unit/assets/sprite1.png'))));
68
+ server.respondWith('GET', 'http://localhost:9966/test/unit/assets/sprite2.json', fs.readFileSync(path.join(__dirname, '../../test/unit/assets/sprite2.json')).toString());
69
+ server.respondWith('GET', 'http://localhost:9966/test/unit/assets/sprite2.png', bufferToArrayBuffer(fs.readFileSync(path.join(__dirname, '../../test/unit/assets/sprite2.png'))));
66
70
 
67
71
  loadSprite([{id: 'sprite1', url: 'http://localhost:9966/test/unit/assets/sprite1'}, {id: 'sprite2', url: 'http://localhost:9966/test/unit/assets/sprite2'}], manager, 1, (err, result) => {
68
72
  expect(err).toBeFalsy();
@@ -90,25 +94,11 @@ describe('loadSprite', () => {
90
94
  done();
91
95
  });
92
96
 
93
- expect(requests[0].url).toBe('http://localhost:9966/test/unit/assets/sprite1.json');
94
- requests[0].setStatus(200);
95
- requests[0].response = fs.readFileSync(path.join(__dirname, '../../test/unit/assets/sprite1.json'));
96
- requests[0].onload();
97
-
98
- expect(requests[1].url).toBe('http://localhost:9966/test/unit/assets/sprite1.png');
99
- requests[1].setStatus(200);
100
- requests[1].response = fs.readFileSync(path.join(__dirname, '../../test/unit/assets/sprite1.png')).buffer;
101
- requests[1].onload();
102
-
103
- expect(requests[2].url).toBe('http://localhost:9966/test/unit/assets/sprite2.json');
104
- requests[2].setStatus(200);
105
- requests[2].response = fs.readFileSync(path.join(__dirname, '../../test/unit/assets/sprite2.json'));
106
- requests[2].onload();
107
-
108
- expect(requests[3].url).toBe('http://localhost:9966/test/unit/assets/sprite2.png');
109
- requests[3].setStatus(200);
110
- requests[3].response = fs.readFileSync(path.join(__dirname, '../../test/unit/assets/sprite2.png')).buffer;
111
- requests[3].onload();
97
+ server.respond();
98
+ expect(server.requests[0].url).toBe('http://localhost:9966/test/unit/assets/sprite1.json');
99
+ expect(server.requests[1].url).toBe('http://localhost:9966/test/unit/assets/sprite1.png');
100
+ expect(server.requests[2].url).toBe('http://localhost:9966/test/unit/assets/sprite2.json');
101
+ expect(server.requests[3].url).toBe('http://localhost:9966/test/unit/assets/sprite2.png');
112
102
  });
113
103
 
114
104
  test('error in callback', done => {
@@ -118,20 +108,19 @@ describe('loadSprite', () => {
118
108
 
119
109
  const manager = new RequestManager(transform);
120
110
 
121
- const requests = [];
122
- fakeXhr.useFakeXMLHttpRequest().onCreate = (req) => { requests.push(req); };
123
-
111
+ server.respondWith((xhr) => xhr.respond(500));
112
+ let last = false;
124
113
  loadSprite([{id: 'sprite1', url: 'http://localhost:9966/test/unit/assets/sprite1'}], manager, 1, (err, result) => {
125
114
  expect(err).toBeTruthy();
126
115
  expect(result).toBeUndefined();
127
-
128
- done();
116
+ if (!last) {
117
+ done();
118
+ last = true;
119
+ }
129
120
  });
130
121
 
131
- expect(requests[0].url).toBe('http://localhost:9966/test/unit/assets/sprite1.json');
132
- requests[0].setStatus(500);
133
- requests[0].response = undefined;
134
- requests[0].onload();
122
+ server.respond();
123
+ expect(server.requests[0].url).toBe('http://localhost:9966/test/unit/assets/sprite1.json');
135
124
  });
136
125
 
137
126
  test('request canceling', done => {
@@ -141,30 +130,24 @@ describe('loadSprite', () => {
141
130
 
142
131
  const manager = new RequestManager(transform);
143
132
 
144
- const requests = [];
145
- fakeXhr.useFakeXMLHttpRequest().onCreate = (req) => { requests.push(req); };
133
+ server.respondWith('GET', 'http://localhost:9966/test/unit/assets/sprite1.json', fs.readFileSync(path.join(__dirname, '../../test/unit/assets/sprite1.json')).toString());
134
+ server.respondWith('GET', 'http://localhost:9966/test/unit/assets/sprite1.png', bufferToArrayBuffer(fs.readFileSync(path.join(__dirname, '../../test/unit/assets/sprite1.png'))));
146
135
 
147
136
  const cancelable = loadSprite([{id: 'sprite1', url: 'http://localhost:9966/test/unit/assets/sprite1'}], manager, 1, () => {});
148
137
 
149
138
  setTimeout(() => {
150
139
  cancelable.cancel();
151
140
 
152
- expect(requests[0].aborted).toBeTruthy();
153
- expect(requests[1].aborted).toBeTruthy();
141
+ expect((server.requests[0] as any).aborted).toBeTruthy();
142
+ expect((server.requests[1] as any).aborted).toBeTruthy();
154
143
 
155
144
  done();
156
145
  });
157
146
 
158
147
  setTimeout(() => {
159
- expect(requests[0].url).toBe('http://localhost:9966/test/unit/assets/sprite1.json');
160
- requests[0].setStatus(200);
161
- requests[0].response = fs.readFileSync(path.join(__dirname, '../../test/unit/assets/sprite1.json'));
162
- requests[0].onload();
163
-
164
- expect(requests[1].url).toBe('http://localhost:9966/test/unit/assets/sprite1.png');
165
- requests[1].setStatus(200);
166
- requests[1].response = fs.readFileSync(path.join(__dirname, '../../test/unit/assets/sprite1.png')).buffer;
167
- requests[1].onload();
148
+ server.respond();
149
+ expect(server.requests[0].url).toBe('http://localhost:9966/test/unit/assets/sprite1.json');
150
+ expect(server.requests[1].url).toBe('http://localhost:9966/test/unit/assets/sprite1.png');
168
151
  }, 10);
169
152
  });
170
153
 
@@ -175,8 +158,8 @@ describe('loadSprite', () => {
175
158
 
176
159
  const manager = new RequestManager(transform);
177
160
 
178
- const requests = [];
179
- fakeXhr.useFakeXMLHttpRequest().onCreate = (req) => { requests.push(req); };
161
+ server.respondWith('GET', 'http://localhost:9966/test/unit/assets/sprite1@2x.json', fs.readFileSync(path.join(__dirname, '../../test/unit/assets/sprite1.json')).toString());
162
+ server.respondWith('GET', 'http://localhost:9966/test/unit/assets/sprite1@2x.png', bufferToArrayBuffer(fs.readFileSync(path.join(__dirname, '../../test/unit/assets/sprite1.png'))));
180
163
 
181
164
  loadSprite('http://localhost:9966/test/unit/assets/sprite1', manager, 2, (err, result) => {
182
165
  expect(err).toBeFalsy();
@@ -196,14 +179,8 @@ describe('loadSprite', () => {
196
179
  done();
197
180
  });
198
181
 
199
- expect(requests[0].url).toBe('http://localhost:9966/test/unit/assets/sprite1@2x.json');
200
- requests[0].setStatus(200);
201
- requests[0].response = fs.readFileSync(path.join(__dirname, '../../test/unit/assets/sprite1.json'));
202
- requests[0].onload();
203
-
204
- expect(requests[1].url).toBe('http://localhost:9966/test/unit/assets/sprite1@2x.png');
205
- requests[1].setStatus(200);
206
- requests[1].response = fs.readFileSync(path.join(__dirname, '../../test/unit/assets/sprite1.png')).buffer;
207
- requests[1].onload();
182
+ server.respond();
183
+ expect(server.requests[0].url).toBe('http://localhost:9966/test/unit/assets/sprite1@2x.json');
184
+ expect(server.requests[1].url).toBe('http://localhost:9966/test/unit/assets/sprite1@2x.png');
208
185
  });
209
186
  });
@@ -13,7 +13,7 @@ import {
13
13
  } from '../source/rtl_text_plugin';
14
14
  import {browser} from '../util/browser';
15
15
  import {OverscaledTileID} from '../source/tile_id';
16
- import {fakeXhr, fakeServer} from 'nise';
16
+ import {fakeServer, type FakeServer} from 'nise';
17
17
 
18
18
  import {EvaluationParameters} from './evaluation_parameters';
19
19
  import {LayerSpecification, GeoJSONSourceSpecification, FilterSpecification, SourceSpecification} from '@maplibre/maplibre-gl-style-spec';
@@ -78,21 +78,17 @@ function createStyle(map = getStubMap()) {
78
78
  return style;
79
79
  }
80
80
 
81
- let sinonFakeXMLServer;
82
- let sinonFakeServer;
81
+ let server: FakeServer;
83
82
  let mockConsoleError;
84
83
 
85
84
  beforeEach(() => {
86
85
  global.fetch = null;
87
- sinonFakeServer = fakeServer.create();
88
- sinonFakeXMLServer = fakeXhr.useFakeXMLHttpRequest();
89
-
86
+ server = fakeServer.create();
90
87
  mockConsoleError = jest.spyOn(console, 'error').mockImplementation(() => { });
91
88
  });
92
89
 
93
90
  afterEach(() => {
94
- sinonFakeXMLServer.restore();
95
- sinonFakeServer.restore();
91
+ server.restore();
96
92
  mockConsoleError.mockRestore();
97
93
  });
98
94
 
@@ -115,12 +111,12 @@ describe('Style', () => {
115
111
 
116
112
  test('loads plugin immediately if already registered', done => {
117
113
  clearRTLTextPlugin();
118
- sinonFakeServer.respondWith('/plugin.js', 'doesn\'t matter');
114
+ server.respondWith('/plugin.js', 'doesn\'t matter');
119
115
  setRTLTextPlugin('/plugin.js', (error) => {
120
116
  expect(error).toMatch(/Cannot set the state of the rtl-text-plugin when not in the web-worker context/);
121
117
  done();
122
118
  });
123
- sinonFakeServer.respond();
119
+ server.respond();
124
120
  new Style(getStubMap());
125
121
  });
126
122
 
@@ -152,7 +148,7 @@ describe('Style', () => {
152
148
  jest.spyOn(style.sourceCaches['vector'], 'reload');
153
149
 
154
150
  clearRTLTextPlugin();
155
- sinonFakeServer.respondWith('/plugin.js', 'doesn\'t matter');
151
+ server.respondWith('/plugin.js', 'doesn\'t matter');
156
152
  const _broadcast = style.dispatcher.broadcast;
157
153
  style.dispatcher.broadcast = function (type, state, callback) {
158
154
  if (type === 'syncRTLPluginState') {
@@ -171,7 +167,7 @@ describe('Style', () => {
171
167
  done();
172
168
  }, 0);
173
169
  });
174
- sinonFakeServer.respond();
170
+ server.respond();
175
171
  });
176
172
  });
177
173
  });
@@ -211,15 +207,15 @@ describe('Style#loadURL', () => {
211
207
  });
212
208
 
213
209
  style.loadURL('style.json');
214
- sinonFakeServer.respondWith(JSON.stringify(createStyleJSON({version: 'invalid'})));
215
- sinonFakeServer.respond();
210
+ server.respondWith(JSON.stringify(createStyleJSON({version: 'invalid'})));
211
+ server.respond();
216
212
  });
217
213
 
218
214
  test('cancels pending requests if removed', () => {
219
215
  const style = new Style(getStubMap());
220
216
  style.loadURL('style.json');
221
217
  style._remove();
222
- expect(sinonFakeServer.lastRequest.aborted).toBe(true);
218
+ expect((server.lastRequest as any).aborted).toBe(true);
223
219
  });
224
220
  });
225
221
 
@@ -266,21 +262,8 @@ describe('Style#loadJSON', () => {
266
262
  // stub Image so we can invoke 'onload'
267
263
  // https://github.com/jsdom/jsdom/commit/58a7028d0d5b6aacc5b435daee9fd8f9eacbb14c
268
264
 
269
- // fake the image request (sinon doesn't allow non-string data for
270
- // server.respondWith, so we do so manually)
271
- const requests = [];
272
- sinonFakeXMLServer.onCreate = req => { requests.push(req); };
273
- const respond = () => {
274
- let req = requests.find(req => req.url === 'http://example.com/sprite.png');
275
- req.setStatus(200);
276
- req.response = new ArrayBuffer(8);
277
- req.onload();
278
-
279
- req = requests.find(req => req.url === 'http://example.com/sprite.json');
280
- req.setStatus(200);
281
- req.response = '{}';
282
- req.onload();
283
- };
265
+ server.respondWith('GET', 'http://example.com/sprite.png', new ArrayBuffer(8));
266
+ server.respondWith('GET', 'http://example.com/sprite.json', '{}');
284
267
 
285
268
  const style = new Style(getStubMap());
286
269
 
@@ -303,7 +286,7 @@ describe('Style#loadJSON', () => {
303
286
  done();
304
287
  });
305
288
 
306
- respond();
289
+ server.respond();
307
290
  });
308
291
  });
309
292
 
@@ -315,21 +298,8 @@ describe('Style#loadJSON', () => {
315
298
  // stub Image so we can invoke 'onload'
316
299
  // https://github.com/jsdom/jsdom/commit/58a7028d0d5b6aacc5b435daee9fd8f9eacbb14c
317
300
 
318
- // fake the image request (sinon doesn't allow non-string data for
319
- // server.respondWith, so we do so manually)
320
- const requests = [];
321
- sinonFakeXMLServer.onCreate = req => { requests.push(req); };
322
- const respond = () => {
323
- let req = requests.find(req => req.url === 'http://example.com/sprite.png');
324
- req.setStatus(200);
325
- req.response = new ArrayBuffer(8);
326
- req.onload();
327
-
328
- req = requests.find(req => req.url === 'http://example.com/sprite.json');
329
- req.setStatus(200);
330
- req.response = '{"image1": {"width": 1, "height": 1, "x": 0, "y": 0, "pixelRatio": 1.0}}';
331
- req.onload();
332
- };
301
+ server.respondWith('GET', 'http://example.com/sprite.png', new ArrayBuffer(8));
302
+ server.respondWith('GET', 'http://example.com/sprite.json', '{"image1": {"width": 1, "height": 1, "x": 0, "y": 0, "pixelRatio": 1.0}}');
333
303
 
334
304
  const style = new Style(getStubMap());
335
305
 
@@ -357,7 +327,7 @@ describe('Style#loadJSON', () => {
357
327
  });
358
328
  });
359
329
 
360
- respond();
330
+ server.respond();
361
331
  });
362
332
  });
363
333
 
@@ -751,7 +721,7 @@ describe('Style#setState', () => {
751
721
  });
752
722
 
753
723
  test('Issue #3893: compare new source options against originally provided options rather than normalized properties', done => {
754
- sinonFakeServer.respondWith('/tilejson.json', JSON.stringify({
724
+ server.respondWith('/tilejson.json', JSON.stringify({
755
725
  tiles: ['http://tiles.server']
756
726
  }));
757
727
  const initial = createStyleJSON();
@@ -767,7 +737,7 @@ describe('Style#setState', () => {
767
737
  style.setState(initial);
768
738
  done();
769
739
  });
770
- sinonFakeServer.respond();
740
+ server.respond();
771
741
  });
772
742
 
773
743
  test('return true if there is a change', done => {
@@ -725,6 +725,9 @@ export class Style extends Evented {
725
725
 
726
726
  this.stylesheet = nextState;
727
727
 
728
+ // reset serialization field, to be populated only when needed
729
+ this._serializedLayers = null;
730
+
728
731
  return true;
729
732
  }
730
733
 
@@ -1,14 +1,15 @@
1
1
  import type {AlphaImage} from '../util/image';
2
2
 
3
- /**
4
- * The glyph's metrices
5
- */
6
3
  export type GlyphMetrics = {
7
4
  width: number;
8
5
  height: number;
9
6
  left: number;
10
7
  top: number;
11
8
  advance: number;
9
+ /**
10
+ * isDoubleResolution = true for 48px textures
11
+ */
12
+ isDoubleResolution?: boolean;
12
13
  };
13
14
 
14
15
  /**
@@ -0,0 +1,50 @@
1
+ import {createStyleLayer} from '../create_style_layer';
2
+ import {extend} from '../../util/util';
3
+ import {LineStyleLayer} from './line_style_layer';
4
+
5
+ describe('LineStyleLayer', () => {
6
+ function createLineLayer(layer?) {
7
+ return extend({
8
+ type: 'line',
9
+ source: 'line',
10
+ id: 'line',
11
+ paint: {
12
+ 'line-color': 'red',
13
+ 'line-width': 14,
14
+ 'line-gradient': [
15
+ 'interpolate',
16
+ ['linear'],
17
+ ['line-progress'],
18
+ 0,
19
+ 'blue',
20
+ 1,
21
+ 'red'
22
+ ]
23
+ }
24
+ }, layer);
25
+ }
26
+
27
+ test('updating with valid line-gradient updates this.gradientVersion', () => {
28
+ const lineLayer = createStyleLayer(createLineLayer()) as LineStyleLayer;
29
+ const gradientVersion = lineLayer.gradientVersion;
30
+
31
+ lineLayer.setPaintProperty('line-gradient', [
32
+ 'interpolate',
33
+ ['linear'],
34
+ ['line-progress'],
35
+ 0,
36
+ 'red',
37
+ 1,
38
+ 'blue'
39
+ ]);
40
+ expect(lineLayer.gradientVersion).toBeGreaterThan(gradientVersion);
41
+ });
42
+
43
+ test('updating with invalid line-gradient updates this.gradientVersion', () => {
44
+ const lineLayer = createStyleLayer(createLineLayer()) as LineStyleLayer;
45
+ const gradientVersion = lineLayer.gradientVersion;
46
+
47
+ lineLayer.setPaintProperty('line-gradient', null);
48
+ expect(lineLayer.gradientVersion).toBeGreaterThan(gradientVersion);
49
+ });
50
+ });
@@ -9,8 +9,8 @@ import {extend} from '../../util/util';
9
9
  import {EvaluationParameters} from '../evaluation_parameters';
10
10
  import {Transitionable, Transitioning, Layout, PossiblyEvaluated, DataDrivenProperty} from '../properties';
11
11
 
12
- import {Step} from '@maplibre/maplibre-gl-style-spec';
13
- import type {FeatureState, ZoomConstantExpression, LayerSpecification} from '@maplibre/maplibre-gl-style-spec';
12
+ import {isZoomExpression, Step} from '@maplibre/maplibre-gl-style-spec';
13
+ import type {FeatureState, LayerSpecification} from '@maplibre/maplibre-gl-style-spec';
14
14
  import type {Bucket, BucketParameters} from '../../data/bucket';
15
15
  import type {LineLayoutProps, LinePaintProps} from './line_style_layer_properties.g';
16
16
  import type {Transform} from '../../geo/transform';
@@ -60,8 +60,12 @@ export class LineStyleLayer extends StyleLayer {
60
60
 
61
61
  _handleSpecialPaintPropertyUpdate(name: string) {
62
62
  if (name === 'line-gradient') {
63
- const expression: ZoomConstantExpression<'source'> = (this._transitionablePaint._values['line-gradient'].value.expression as any);
64
- this.stepInterpolant = expression._styleExpression.expression instanceof Step;
63
+ const expression = this.gradientExpression();
64
+ if (isZoomExpression(expression)) {
65
+ this.stepInterpolant = expression._styleExpression.expression instanceof Step;
66
+ } else {
67
+ this.stepInterpolant = false;
68
+ }
65
69
  this.gradientVersion = (this.gradientVersion + 1) % Number.MAX_SAFE_INTEGER;
66
70
  }
67
71
  }
@@ -274,10 +274,12 @@ export function getGlyphQuads(
274
274
  builtInOffset = [0, 0];
275
275
  }
276
276
 
277
+ const textureScale = positionedGlyph.metrics.isDoubleResolution ? 2 : 1;
278
+
277
279
  const x1 = (positionedGlyph.metrics.left - rectBuffer) * positionedGlyph.scale - halfAdvance + builtInOffset[0];
278
280
  const y1 = (-positionedGlyph.metrics.top - rectBuffer) * positionedGlyph.scale + builtInOffset[1];
279
- const x2 = x1 + textureRect.w * positionedGlyph.scale / pixelRatio;
280
- const y2 = y1 + textureRect.h * positionedGlyph.scale / pixelRatio;
281
+ const x2 = x1 + textureRect.w / textureScale * positionedGlyph.scale / pixelRatio;
282
+ const y2 = y1 + textureRect.h / textureScale * positionedGlyph.scale / pixelRatio;
281
283
 
282
284
  const tl = new Point(x1, y1);
283
285
  const tr = new Point(x2, y1);
@@ -351,5 +351,11 @@ export class ScrollZoomHandler implements Handler {
351
351
 
352
352
  reset() {
353
353
  this._active = false;
354
+ this._zooming = false;
355
+ delete this._targetZoom;
356
+ if (this._finishTimeout) {
357
+ clearTimeout(this._finishTimeout);
358
+ delete this._finishTimeout;
359
+ }
354
360
  }
355
361
  }
@@ -17,6 +17,7 @@ import {DragPanHandler} from './handler/shim/drag_pan';
17
17
  import {DragRotateHandler} from './handler/shim/drag_rotate';
18
18
  import {TwoFingersTouchZoomRotateHandler} from './handler/shim/two_fingers_touch';
19
19
  import {extend} from '../util/util';
20
+ import {browser} from '../util/browser';
20
21
  import Point from '@mapbox/point-geometry';
21
22
 
22
23
  export type InputEvent = MouseEvent | TouchEvent | KeyboardEvent | WheelEvent;
@@ -583,7 +584,7 @@ export class HandlerManager {
583
584
 
584
585
  const shouldSnapToNorth = bearing => bearing !== 0 && -this._bearingSnap < bearing && bearing < this._bearingSnap;
585
586
 
586
- if (inertialEase) {
587
+ if (inertialEase && (inertialEase.essential || !browser.prefersReducedMotion)) {
587
588
  if (shouldSnapToNorth(inertialEase.bearing || this._map.getBearing())) {
588
589
  inertialEase.bearing = 0;
589
590
  }
@@ -288,6 +288,23 @@ describe('Map', () => {
288
288
 
289
289
  });
290
290
 
291
+ test('setStyle back to the first style should work', done => {
292
+ const redStyle = {version: 8 as const, sources: {}, layers: [
293
+ {id: 'background', type: 'background' as const, paint: {'background-color': 'red'}},
294
+ ]};
295
+ const blueStyle = {version: 8 as const, sources: {}, layers: [
296
+ {id: 'background', type: 'background' as const, paint: {'background-color': 'blue'}},
297
+ ]};
298
+ const map = createMap({style: redStyle});
299
+ map.setStyle(blueStyle);
300
+ map.once('style.load', () => {
301
+ map.setStyle(redStyle);
302
+ const serializedStyle = map.style.serialize();
303
+ expect(serializedStyle.layers[0].paint['background-color']).toBe('red');
304
+ done();
305
+ });
306
+ });
307
+
291
308
  test('style transform overrides unmodified map transform', done => {
292
309
  const map = new Map({container: window.document.createElement('div')} as any as MapOptions);
293
310
  map.transform.lngRange = [-120, 140];
package/src/ui/map.ts CHANGED
@@ -2562,6 +2562,7 @@ export class Map extends Camera {
2562
2562
  * @param name - The name of the paint property to set.
2563
2563
  * @param value - The value of the paint property to set.
2564
2564
  * Must be of a type appropriate for the property, as defined in the [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/).
2565
+ * Pass `null` to unset the existing value.
2565
2566
  * @param options - Options object.
2566
2567
  * @returns `this`
2567
2568
  * @example
@@ -811,4 +811,29 @@ describe('marker', () => {
811
811
 
812
812
  map.remove();
813
813
  });
814
+
815
+ test('Marker after the terrain event must listen to the render event till is fully loaded', async () => {
816
+ const map = createMap();
817
+
818
+ new Marker()
819
+ .setLngLat([1, 1])
820
+ .addTo(map);
821
+
822
+ expect(map._oneTimeListeners.render).toBeUndefined();
823
+
824
+ map.fire('terrain');
825
+ expect(map._oneTimeListeners.render).toHaveLength(1);
826
+
827
+ map.fire('render');
828
+ expect(map._oneTimeListeners.render).toHaveLength(1);
829
+
830
+ map.fire('render');
831
+ expect(map._oneTimeListeners.render).toHaveLength(1);
832
+
833
+ // await idle to be fully loaded
834
+ await map.once('idle');
835
+ map.fire('render');
836
+ expect(map._oneTimeListeners.render).toHaveLength(0);
837
+ map.remove();
838
+ });
814
839
  });
package/src/ui/marker.ts CHANGED
@@ -297,6 +297,8 @@ export class Marker extends Evented {
297
297
  map.getCanvasContainer().appendChild(this._element);
298
298
  map.on('move', this._update);
299
299
  map.on('moveend', this._update);
300
+ map.on('terrain', this._update);
301
+
300
302
  this.setDraggable(this._draggable);
301
303
  this._update();
302
304
 
@@ -504,9 +506,14 @@ export class Marker extends Evented {
504
506
  return this;
505
507
  }
506
508
 
507
- _update = (e?: { type: 'move' | 'moveend' }) => {
509
+ _update = (e?: { type: 'move' | 'moveend' | 'terrain' | 'render' }) => {
508
510
  if (!this._map) return;
509
511
 
512
+ const isFullyLoaded = this._map.loaded() && !this._map.isMoving();
513
+ if (e?.type === 'terrain' || (e?.type === 'render' && !isFullyLoaded)) {
514
+ this._map.once('render', this._update);
515
+ }
516
+
510
517
  if (this._map.transform.renderWorldCopies) {
511
518
  this._lngLat = smartWrap(this._lngLat, this._pos, this._map.transform);
512
519
  }
@@ -6,7 +6,7 @@ import {
6
6
  sameOrigin
7
7
  } from './ajax';
8
8
 
9
- import {fakeServer, FakeServer} from 'nise';
9
+ import {fakeServer, type FakeServer} from 'nise';
10
10
  import {destroyFetchMock, FetchMock, RequestMock, setupFetchMock} from './test/mock_fetch';
11
11
 
12
12
  function readAsText(blob) {
@@ -1,7 +1,7 @@
1
1
  import {config} from './config';
2
2
  import {webpSupported} from './webp_supported';
3
3
  import {stubAjaxGetImage} from './test/util';
4
- import {fakeServer, FakeServer} from 'nise';
4
+ import {fakeServer, type FakeServer} from 'nise';
5
5
  import {ImageRequest, ImageRequestQueueItem} from './image_request';
6
6
  import * as ajax from './ajax';
7
7
 
@@ -0,0 +1,13 @@
1
+ import {isOffscreenCanvasDistorted} from './offscreen_canvas_distorted';
2
+ import {Canvas} from 'canvas';
3
+ import {offscreenCanvasSupported} from './offscreen_canvas_supported';
4
+
5
+ test('normal operation does not mangle canvas', () => {
6
+ const OffscreenCanvas = (window as any).OffscreenCanvas = jest.fn((width:number, height: number) => {
7
+ return new Canvas(width, height);
8
+ });
9
+ expect(offscreenCanvasSupported()).toBeTruthy();
10
+ OffscreenCanvas.mockClear();
11
+ expect(isOffscreenCanvasDistorted()).toBeFalsy();
12
+ expect(OffscreenCanvas).toHaveBeenCalledTimes(1);
13
+ });