UNPKG

70.7 kBPlain TextView Raw
1import Style from './style';
2import SourceCache from '../source/source_cache';
3import StyleLayer from './style_layer';
4import Transform from '../geo/transform';
5import {extend} from '../util/util';
6import {RequestManager} from '../util/request_manager';
7import {Event, Evented} from '../util/evented';
8import {
9 setRTLTextPlugin,
10 clearRTLTextPlugin,
11 evented as rtlTextPluginEvented
12} from '../source/rtl_text_plugin';
13import browser from '../util/browser';
14import {OverscaledTileID} from '../source/tile_id';
15import {fakeXhr, fakeServer} from 'nise';
16import {WorkerGlobalScopeInterface} from '../util/web_worker';
17import EvaluationParameters from './evaluation_parameters';
18import {LayerSpecification, GeoJSONSourceSpecification, FilterSpecification, SourceSpecification} from '../style-spec/types.g';
19import {SourceClass} from '../source/source';
20import GeoJSONSource from '../source/geojson_source';
21
22function createStyleJSON(properties?) {
23 return extend({
24 'version': 8,
25 'sources': {},
26 'layers': []
27 }, properties);
28}
29
30function createSource() {
31 return {
32 type: 'vector',
33 minzoom: 1,
34 maxzoom: 10,
35 attribution: 'MapLibre',
36 tiles: ['http://example.com/{z}/{x}/{y}.png']
37 } as any as SourceSpecification;
38}
39
40function createGeoJSONSource() {
41 return {
42 'type': 'geojson',
43 'data': {
44 'type': 'FeatureCollection',
45 'features': []
46 }
47 };
48}
49
50class StubMap extends Evented {
51 style: Style;
52 transform: Transform;
53 private _requestManager: RequestManager;
54
55 constructor() {
56 super();
57 this.transform = new Transform();
58 this._requestManager = new RequestManager();
59 }
60
61 _getMapId() {
62 return 1;
63 }
64
65 getPixelRatio() {
66 return 1;
67 }
68}
69
70const getStubMap = () => new StubMap() as any;
71
72function createStyle(map = getStubMap()) {
73 const style = new Style(map);
74 map.style = style;
75 return style;
76}
77
78let sinonFakeXMLServer;
79let sinonFakeServer;
80let _self;
81let mockConsoleError;
82
83beforeEach(() => {
84 global.fetch = null;
85 sinonFakeServer = fakeServer.create();
86 sinonFakeXMLServer = fakeXhr.useFakeXMLHttpRequest();
87
88 _self = {
89 addEventListener() {}
90 } as any as WorkerGlobalScopeInterface & typeof globalThis;
91 global.self = _self;
92
93 mockConsoleError = jest.spyOn(console, 'error').mockImplementation(() => { });
94});
95
96afterEach(() => {
97 sinonFakeXMLServer.restore();
98 sinonFakeServer.restore();
99
100 global.self = undefined;
101
102 mockConsoleError.mockRestore();
103});
104
105describe('Style', () => {
106 test('registers plugin state change listener', () => {
107 clearRTLTextPlugin();
108
109 jest.spyOn(Style, 'registerForPluginStateChange');
110 const style = new Style(getStubMap());
111 const mockStyleDispatcherBroadcast = jest.spyOn(style.dispatcher, 'broadcast');
112 expect(Style.registerForPluginStateChange).toHaveBeenCalledTimes(1);
113
114 setRTLTextPlugin('/plugin.js', undefined);
115 expect(mockStyleDispatcherBroadcast.mock.calls[0][0]).toBe('syncRTLPluginState');
116 expect(mockStyleDispatcherBroadcast.mock.calls[0][1]).toEqual({
117 pluginStatus: 'deferred',
118 pluginURL: 'http://localhost/plugin.js',
119 });
120 });
121
122 test('loads plugin immediately if already registered', done => {
123 clearRTLTextPlugin();
124 sinonFakeServer.respondWith('/plugin.js', 'doesn\'t matter');
125 setRTLTextPlugin('/plugin.js', (error) => {
126 expect(error).toMatch(/Cannot set the state of the rtl-text-plugin when not in the web-worker context/);
127 done();
128 });
129 sinonFakeServer.respond();
130 new Style(createStyleJSON());
131 });
132});
133
134describe('Style#loadURL', () => {
135 test('fires "dataloading"', () => {
136 const style = new Style(getStubMap());
137 const spy = jest.fn();
138
139 style.on('dataloading', spy);
140 style.loadURL('style.json');
141
142 expect(spy).toHaveBeenCalledTimes(1);
143 expect(spy.mock.calls[0][0].target).toBe(style);
144 expect(spy.mock.calls[0][0].dataType).toBe('style');
145 });
146
147 test('transforms style URL before request', () => {
148 const map = getStubMap();
149 const spy = jest.spyOn(map._requestManager, 'transformRequest');
150
151 const style = new Style(map);
152 style.loadURL('style.json');
153
154 expect(spy).toHaveBeenCalledTimes(1);
155 expect(spy.mock.calls[0][0]).toBe('style.json');
156 expect(spy.mock.calls[0][1]).toBe('Style');
157 });
158
159 test('validates the style', done => {
160 const style = new Style(getStubMap());
161
162 style.on('error', ({error}) => {
163 expect(error).toBeTruthy();
164 expect(error.message).toMatch(/version/);
165 done();
166 });
167
168 style.loadURL('style.json');
169 sinonFakeServer.respondWith(JSON.stringify(createStyleJSON({version: 'invalid'})));
170 sinonFakeServer.respond();
171 });
172
173 test('cancels pending requests if removed', () => {
174 const style = new Style(getStubMap());
175 style.loadURL('style.json');
176 style._remove();
177 expect(sinonFakeServer.lastRequest.aborted).toBe(true);
178 });
179});
180
181describe('Style#loadJSON', () => {
182 test('fires "dataloading" (synchronously)', () => {
183 const style = new Style(getStubMap());
184 const spy = jest.fn();
185
186 style.on('dataloading', spy);
187 style.loadJSON(createStyleJSON());
188
189 expect(spy).toHaveBeenCalledTimes(1);
190 expect(spy.mock.calls[0][0].target).toBe(style);
191 expect(spy.mock.calls[0][0].dataType).toBe('style');
192 });
193
194 test('fires "data" (asynchronously)', done => {
195 const style = new Style(getStubMap());
196
197 style.loadJSON(createStyleJSON());
198
199 style.on('data', (e) => {
200 expect(e.target).toBe(style);
201 expect(e.dataType).toBe('style');
202 done();
203 });
204 });
205
206 test('fires "data" when the sprite finishes loading', done => {
207 // Stubbing to bypass Web APIs that supported by jsdom:
208 // * `URL.createObjectURL` in ajax.getImage (https://github.com/tmpvar/jsdom/issues/1721)
209 // * `canvas.getContext('2d')` in browser.getImageData
210 jest.spyOn(browser, 'getImageData');
211 // stub Image so we can invoke 'onload'
212 // https://github.com/jsdom/jsdom/commit/58a7028d0d5b6aacc5b435daee9fd8f9eacbb14c
213
214 // fake the image request (sinon doesn't allow non-string data for
215 // server.respondWith, so we do so manually)
216 const requests = [];
217 sinonFakeXMLServer.onCreate = req => { requests.push(req); };
218 const respond = () => {
219 let req = requests.find(req => req.url === 'http://example.com/sprite.png');
220 req.setStatus(200);
221 req.response = new ArrayBuffer(8);
222 req.onload();
223
224 req = requests.find(req => req.url === 'http://example.com/sprite.json');
225 req.setStatus(200);
226 req.response = '{}';
227 req.onload();
228 };
229
230 const style = new Style(getStubMap());
231
232 style.loadJSON({
233 'version': 8,
234 'sources': {},
235 'layers': [],
236 'sprite': 'http://example.com/sprite'
237 });
238
239 style.once('error', (e) => expect(e).toBeFalsy());
240
241 style.once('data', (e) => {
242 expect(e.target).toBe(style);
243 expect(e.dataType).toBe('style');
244
245 style.once('data', (e) => {
246 expect(e.target).toBe(style);
247 expect(e.dataType).toBe('style');
248 done();
249 });
250
251 respond();
252 });
253 });
254
255 test('validates the style', done => {
256 const style = new Style(getStubMap());
257
258 style.on('error', ({error}) => {
259 expect(error).toBeTruthy();
260 expect(error.message).toMatch(/version/);
261 done();
262 });
263
264 style.loadJSON(createStyleJSON({version: 'invalid'}));
265 });
266
267 test('creates sources', done => {
268 const style = createStyle();
269
270 style.on('style.load', () => {
271 expect(style.sourceCaches['mapLibre'] instanceof SourceCache).toBeTruthy();
272 done();
273 });
274
275 style.loadJSON(extend(createStyleJSON(), {
276 'sources': {
277 'mapLibre': {
278 'type': 'vector',
279 'tiles': []
280 }
281 }
282 }));
283 });
284
285 test('creates layers', done => {
286 const style = createStyle();
287
288 style.on('style.load', () => {
289 expect(style.getLayer('fill') instanceof StyleLayer).toBeTruthy();
290 done();
291 });
292
293 style.loadJSON({
294 'version': 8,
295 'sources': {
296 'foo': {
297 'type': 'vector'
298 }
299 },
300 'layers': [{
301 'id': 'fill',
302 'source': 'foo',
303 'source-layer': 'source-layer',
304 'type': 'fill'
305 }]
306 });
307 });
308
309 test('transforms sprite json and image URLs before request', done => {
310 const map = getStubMap();
311 const transformSpy = jest.spyOn(map._requestManager, 'transformRequest');
312 const style = createStyle(map);
313
314 style.on('style.load', () => {
315 expect(transformSpy).toHaveBeenCalledTimes(2);
316 expect(transformSpy.mock.calls[0][0]).toBe('http://example.com/sprites/bright-v8.json');
317 expect(transformSpy.mock.calls[0][1]).toBe('SpriteJSON');
318 expect(transformSpy.mock.calls[1][0]).toBe('http://example.com/sprites/bright-v8.png');
319 expect(transformSpy.mock.calls[1][1]).toBe('SpriteImage');
320 done();
321 });
322
323 style.loadJSON(extend(createStyleJSON(), {
324 'sprite': 'http://example.com/sprites/bright-v8'
325 }));
326 });
327
328 test('emits an error on non-existant vector source layer', done => {
329 const style = createStyle();
330 style.loadJSON(createStyleJSON({
331 sources: {
332 '-source-id-': {type: 'vector', tiles: []}
333 },
334 layers: []
335 }));
336
337 style.on('style.load', () => {
338 style.removeSource('-source-id-');
339
340 const source = createSource();
341 source['vector_layers'] = [{id: 'green'}];
342 style.addSource('-source-id-', source);
343 style.addLayer({
344 'id': '-layer-id-',
345 'type': 'circle',
346 'source': '-source-id-',
347 'source-layer': '-source-layer-'
348 });
349 style.update({} as EvaluationParameters);
350 });
351
352 style.on('error', (event) => {
353 const err = event.error;
354 expect(err).toBeTruthy();
355 expect(err.toString().indexOf('-source-layer-') !== -1).toBeTruthy();
356 expect(err.toString().indexOf('-source-id-') !== -1).toBeTruthy();
357 expect(err.toString().indexOf('-layer-id-') !== -1).toBeTruthy();
358
359 done();
360 });
361 });
362
363 test('sets up layer event forwarding', done => {
364 const style = new Style(getStubMap());
365 style.loadJSON(createStyleJSON({
366 layers: [{
367 id: 'background',
368 type: 'background'
369 }]
370 }));
371
372 style.on('error', (e) => {
373 expect(e.layer).toEqual({id: 'background'});
374 expect(e.mapLibre).toBeTruthy();
375 done();
376 });
377
378 style.on('style.load', () => {
379 style._layers.background.fire(new Event('error', {mapLibre: true}));
380 });
381 });
382});
383
384describe('Style#_remove', () => {
385 test('removes cache sources and clears their tiles', done => {
386 const style = new Style(getStubMap());
387 style.loadJSON(createStyleJSON({
388 sources: {'source-id': createGeoJSONSource()}
389 }));
390
391 style.on('style.load', () => {
392 const sourceCache = style.sourceCaches['source-id'];
393 jest.spyOn(sourceCache, 'setEventedParent');
394 jest.spyOn(sourceCache, 'onRemove');
395 jest.spyOn(sourceCache, 'clearTiles');
396
397 style._remove();
398
399 expect(sourceCache.setEventedParent).toHaveBeenCalledWith(null);
400 expect(sourceCache.onRemove).toHaveBeenCalledWith(style.map);
401 expect(sourceCache.clearTiles).toHaveBeenCalled();
402
403 done();
404 });
405 });
406
407 test('deregisters plugin listener', done => {
408 const style = new Style(getStubMap());
409 style.loadJSON(createStyleJSON());
410 const mockStyleDispatcherBroadcast = jest.spyOn(style.dispatcher, 'broadcast');
411
412 style.on('style.load', () => {
413 style._remove();
414
415 rtlTextPluginEvented.fire(new Event('pluginStateChange'));
416 expect(mockStyleDispatcherBroadcast).not.toHaveBeenCalledWith('syncRTLPluginState');
417 done();
418 });
419 });
420});
421
422describe('Style#update', () => {
423 test('on error', done => {
424 const style = createStyle();
425 style.loadJSON({
426 'version': 8,
427 'sources': {
428 'source': {
429 'type': 'vector'
430 }
431 },
432 'layers': [{
433 'id': 'second',
434 'source': 'source',
435 'source-layer': 'source-layer',
436 'type': 'fill'
437 }]
438 });
439
440 style.on('error', (error) => { expect(error).toBeFalsy(); });
441
442 style.on('style.load', () => {
443 style.addLayer({id: 'first', source: 'source', type: 'fill', 'source-layer': 'source-layer'}, 'second');
444 style.addLayer({id: 'third', source: 'source', type: 'fill', 'source-layer': 'source-layer'});
445 style.removeLayer('second');
446
447 style.dispatcher.broadcast = function(key, value) {
448 expect(key).toBe('updateLayers');
449 expect(value['layers'].map((layer) => { return layer.id; })).toEqual(['first', 'third']);
450 expect(value['removedIds']).toEqual(['second']);
451 done();
452 };
453
454 style.update({} as EvaluationParameters);
455 });
456 });
457});
458
459describe('Style#setState', () => {
460 test('throw before loaded', () => {
461 const style = new Style(getStubMap());
462 expect(() => style.setState(createStyleJSON())).toThrow(/load/i);
463 });
464
465 test('do nothing if there are no changes', done => {
466 const style = createStyle();
467 style.loadJSON(createStyleJSON());
468 jest.spyOn(style, 'addLayer').mockImplementation(() => done('test failed'));
469 jest.spyOn(style, 'removeLayer').mockImplementation(() => done('test failed'));
470 jest.spyOn(style, 'setPaintProperty').mockImplementation(() => done('test failed'));
471 jest.spyOn(style, 'setLayoutProperty').mockImplementation(() => done('test failed'));
472 jest.spyOn(style, 'setFilter').mockImplementation(() => done('test failed'));
473 jest.spyOn(style, 'addSource').mockImplementation(() => done('test failed'));
474 jest.spyOn(style, 'removeSource').mockImplementation(() => done('test failed'));
475 jest.spyOn(style, 'setGeoJSONSourceData').mockImplementation(() => done('test failed'));
476 jest.spyOn(style, 'setLayerZoomRange').mockImplementation(() => done('test failed'));
477 jest.spyOn(style, 'setLight').mockImplementation(() => done('test failed'));
478 style.on('style.load', () => {
479 const didChange = style.setState(createStyleJSON());
480 expect(didChange).toBeFalsy();
481 done();
482 });
483 });
484
485 test('Issue #3893: compare new source options against originally provided options rather than normalized properties', done => {
486 sinonFakeServer.respondWith('/tilejson.json', JSON.stringify({
487 tiles: ['http://tiles.server']
488 }));
489 const initial = createStyleJSON();
490 initial.sources.mySource = {
491 type: 'raster',
492 url: '/tilejson.json'
493 };
494 const style = new Style(getStubMap());
495 style.loadJSON(initial);
496 style.on('style.load', () => {
497 jest.spyOn(style, 'removeSource').mockImplementation(() => done('test failed: removeSource called'));
498 jest.spyOn(style, 'addSource').mockImplementation(() => done('test failed: addSource called'));
499 style.setState(initial);
500 done();
501 });
502 sinonFakeServer.respond();
503 });
504
505 test('return true if there is a change', done => {
506 const initialState = createStyleJSON();
507 const nextState = createStyleJSON({
508 sources: {
509 foo: {
510 type: 'geojson',
511 data: {type: 'FeatureCollection', features: []}
512 }
513 }
514 });
515
516 const style = new Style(getStubMap());
517 style.loadJSON(initialState);
518 style.on('style.load', () => {
519 const didChange = style.setState(nextState);
520 expect(didChange).toBeTruthy();
521 expect(style.stylesheet).toEqual(nextState);
522 done();
523 });
524 });
525
526 test('sets GeoJSON source data if different', done => {
527 const initialState = createStyleJSON({
528 'sources': {'source-id': createGeoJSONSource()}
529 });
530
531 const geoJSONSourceData = {
532 'type': 'FeatureCollection',
533 'features': [
534 {
535 'type': 'Feature',
536 'geometry': {
537 'type': 'Point',
538 'coordinates': [125.6, 10.1]
539 }
540 }
541 ]
542 };
543
544 const nextState = createStyleJSON({
545 'sources': {
546 'source-id': {
547 'type': 'geojson',
548 'data': geoJSONSourceData
549 }
550 }
551 });
552
553 const style = new Style(getStubMap());
554 style.loadJSON(initialState);
555
556 style.on('style.load', () => {
557 const geoJSONSource = style.sourceCaches['source-id'].getSource() as GeoJSONSource;
558 const mockStyleSetGeoJSONSourceDate = jest.spyOn(style, 'setGeoJSONSourceData');
559 const mockGeoJSONSourceSetData = jest.spyOn(geoJSONSource, 'setData');
560 const didChange = style.setState(nextState);
561
562 expect(mockStyleSetGeoJSONSourceDate).toHaveBeenCalledWith('source-id', geoJSONSourceData);
563 expect(mockGeoJSONSourceSetData).toHaveBeenCalledWith(geoJSONSourceData);
564 expect(didChange).toBeTruthy();
565 expect(style.stylesheet).toEqual(nextState);
566 done();
567 });
568 });
569});
570
571describe('Style#addSource', () => {
572 test('throw before loaded', () => {
573 const style = new Style(getStubMap());
574 expect(() => style.addSource('source-id', createSource())).toThrow(/load/i);
575 });
576
577 test('throw if missing source type', done => {
578 const style = new Style(getStubMap());
579 style.loadJSON(createStyleJSON());
580
581 const source = createSource();
582 delete source.type;
583
584 style.on('style.load', () => {
585 expect(() => style.addSource('source-id', source)).toThrow(/type/i);
586 done();
587 });
588 });
589
590 test('fires "data" event', done => {
591 const style = createStyle();
592 style.loadJSON(createStyleJSON());
593 const source = createSource();
594 style.once('data', () => { done(); });
595 style.on('style.load', () => {
596 style.addSource('source-id', source);
597 style.update({} as EvaluationParameters);
598 });
599 });
600
601 test('throws on duplicates', done => {
602 const style = createStyle();
603 style.loadJSON(createStyleJSON());
604 const source = createSource();
605 style.on('style.load', () => {
606 style.addSource('source-id', source);
607 expect(() => {
608 style.addSource('source-id', source);
609 }).toThrow(/Source "source-id" already exists./);
610 done();
611 });
612 });
613
614 test('sets up source event forwarding', done => {
615 let one = 0;
616 let two = 0;
617 let three = 0;
618 let four = 0;
619 const checkVisited = () => {
620 if (one === 1 && two === 1 && three === 1 && four === 1) {
621 done();
622 }
623 };
624 const style = createStyle();
625 style.loadJSON(createStyleJSON({
626 layers: [{
627 id: 'background',
628 type: 'background'
629 }]
630 }));
631 const source = createSource();
632
633 style.on('style.load', () => {
634 style.on('error', () => {
635 one = 1;
636 checkVisited();
637 });
638 style.on('data', (e) => {
639 if (e.sourceDataType === 'metadata' && e.dataType === 'source') {
640 two = 1;
641 checkVisited();
642 } else if (e.sourceDataType === 'content' && e.dataType === 'source') {
643 three = 1;
644 checkVisited();
645 } else {
646 four = 1;
647 checkVisited();
648 }
649 });
650
651 style.addSource('source-id', source); // fires data twice
652 style.sourceCaches['source-id'].fire(new Event('error'));
653 style.sourceCaches['source-id'].fire(new Event('data'));
654 });
655 });
656});
657
658describe('Style#removeSource', () => {
659 test('throw before loaded', () => {
660 const style = new Style(getStubMap());
661 expect(() => style.removeSource('source-id')).toThrow(/load/i);
662 });
663
664 test('fires "data" event', done => {
665 const style = new Style(getStubMap());
666 style.loadJSON(createStyleJSON());
667 const source = createSource();
668 style.once('data', () => { done(); });
669 style.on('style.load', () => {
670 style.addSource('source-id', source);
671 style.removeSource('source-id');
672 style.update({} as EvaluationParameters);
673 });
674 });
675
676 test('clears tiles', done => {
677 const style = new Style(getStubMap());
678 style.loadJSON(createStyleJSON({
679 sources: {'source-id': createGeoJSONSource()}
680 }));
681
682 style.on('style.load', () => {
683 const sourceCache = style.sourceCaches['source-id'];
684 jest.spyOn(sourceCache, 'clearTiles');
685 style.removeSource('source-id');
686 expect(sourceCache.clearTiles).toHaveBeenCalledTimes(1);
687 done();
688 });
689 });
690
691 test('throws on non-existence', done => {
692 const style = new Style(getStubMap());
693 style.loadJSON(createStyleJSON());
694 style.on('style.load', () => {
695 expect(() => {
696 style.removeSource('source-id');
697 }).toThrow(/There is no source with this ID/);
698 done();
699 });
700 });
701
702 function createStyle(callback) {
703 const style = new Style(getStubMap());
704 style.loadJSON(createStyleJSON({
705 'sources': {
706 'mapLibre-source': createGeoJSONSource()
707 },
708 'layers': [{
709 'id': 'mapLibre-layer',
710 'type': 'circle',
711 'source': 'mapLibre-source',
712 'source-layer': 'whatever'
713 }]
714 }));
715 style.on('style.load', () => {
716 style.update(1 as any as EvaluationParameters);
717 callback(style);
718 });
719 return style;
720 }
721
722 test('throws if source is in use', done => {
723 createStyle((style) => {
724 style.on('error', (event) => {
725 expect(event.error.message.includes('"mapLibre-source"')).toBeTruthy();
726 expect(event.error.message.includes('"mapLibre-layer"')).toBeTruthy();
727 done();
728 });
729 style.removeSource('mapLibre-source');
730 });
731 });
732
733 test('does not throw if source is not in use', done => {
734 createStyle((style) => {
735 style.on('error', () => {
736 done('test failed');
737 });
738 style.removeLayer('mapLibre-layer');
739 style.removeSource('mapLibre-source');
740 done();
741 });
742 });
743
744 test('tears down source event forwarding', done => {
745 const style = new Style(getStubMap());
746 style.loadJSON(createStyleJSON());
747 const source = createSource();
748
749 style.on('style.load', () => {
750 style.addSource('source-id', source);
751 const sourceCache = style.sourceCaches['source-id'];
752
753 style.removeSource('source-id');
754
755 // Suppress error reporting
756 sourceCache.on('error', () => {});
757
758 style.on('data', () => { expect(false).toBeTruthy(); });
759 style.on('error', () => { expect(false).toBeTruthy(); });
760 sourceCache.fire(new Event('data'));
761 sourceCache.fire(new Event('error'));
762
763 done();
764 });
765 });
766});
767
768describe('Style#setGeoJSONSourceData', () => {
769 const geoJSON = {type: 'FeatureCollection', features: []} as GeoJSON.GeoJSON;
770
771 test('throws before loaded', () => {
772 const style = new Style(getStubMap());
773 expect(() => style.setGeoJSONSourceData('source-id', geoJSON)).toThrow(/load/i);
774 });
775
776 test('throws on non-existence', done => {
777 const style = new Style(getStubMap());
778 style.loadJSON(createStyleJSON());
779 style.on('style.load', () => {
780 expect(() => style.setGeoJSONSourceData('source-id', geoJSON)).toThrow(/There is no source with this ID/);
781 done();
782 });
783 });
784});
785
786describe('Style#addLayer', () => {
787 test('throw before loaded', () => {
788 const style = new Style(getStubMap());
789 expect(() => style.addLayer({id: 'background', type: 'background'})).toThrow(/load/i);
790 });
791
792 test('sets up layer event forwarding', done => {
793 const style = new Style(getStubMap());
794 style.loadJSON(createStyleJSON());
795
796 style.on('error', (e) => {
797 expect(e.layer).toEqual({id: 'background'});
798 expect(e.mapLibre).toBeTruthy();
799 done();
800 });
801
802 style.on('style.load', () => {
803 style.addLayer({
804 id: 'background',
805 type: 'background'
806 });
807 style._layers.background.fire(new Event('error', {mapLibre: true}));
808 });
809 });
810
811 test('throws on non-existant vector source layer', done => {
812 const style = createStyle();
813 style.loadJSON(createStyleJSON({
814 sources: {
815 // At least one source must be added to trigger the load event
816 dummy: {type: 'vector', tiles: []}
817 }
818 }));
819
820 style.on('style.load', () => {
821 const source = createSource();
822 source['vector_layers'] = [{id: 'green'}];
823 style.addSource('-source-id-', source);
824 style.addLayer({
825 'id': '-layer-id-',
826 'type': 'circle',
827 'source': '-source-id-',
828 'source-layer': '-source-layer-'
829 });
830 });
831
832 style.on('error', (event) => {
833 const err = event.error;
834
835 expect(err).toBeTruthy();
836 expect(err.toString().indexOf('-source-layer-') !== -1).toBeTruthy();
837 expect(err.toString().indexOf('-source-id-') !== -1).toBeTruthy();
838 expect(err.toString().indexOf('-layer-id-') !== -1).toBeTruthy();
839
840 done();
841 });
842 });
843
844 test('emits error on invalid layer', done => {
845 const style = new Style(getStubMap());
846 style.loadJSON(createStyleJSON());
847 style.on('style.load', () => {
848 style.on('error', () => {
849 expect(style.getLayer('background')).toBeFalsy();
850 done();
851 });
852 style.addLayer({
853 id: 'background',
854 type: 'background',
855 paint: {
856 'background-opacity': 5
857 }
858 });
859 });
860 });
861
862 test('#4040 does not mutate source property when provided inline', done => {
863 const style = new Style(getStubMap());
864 style.loadJSON(createStyleJSON());
865 style.on('style.load', () => {
866 const source = {
867 'type': 'geojson',
868 'data': {
869 'type': 'Point',
870 'coordinates': [ 0, 0]
871 }
872 };
873 const layer = {id: 'inline-source-layer', type: 'circle', source} as any as LayerSpecification;
874 style.addLayer(layer);
875 expect((layer as any).source).toEqual(source);
876 done();
877 });
878 });
879
880 test('reloads source', done => {
881 const style = createStyle();
882 style.loadJSON(extend(createStyleJSON(), {
883 'sources': {
884 'mapLibre': {
885 'type': 'vector',
886 'tiles': []
887 }
888 }
889 }));
890 const layer = {
891 'id': 'symbol',
892 'type': 'symbol',
893 'source': 'mapLibre',
894 'source-layer': 'libremap',
895 'filter': ['==', 'id', 0]
896 } as LayerSpecification;
897
898 style.on('data', (e) => {
899 if (e.dataType === 'source' && e.sourceDataType === 'content') {
900 style.sourceCaches['mapLibre'].reload = function() { done(); };
901 style.addLayer(layer);
902 style.update({} as EvaluationParameters);
903 }
904 });
905 });
906
907 test('#3895 reloads source (instead of clearing) if adding this layer with the same type, immediately after removing it', done => {
908 const style = createStyle();
909 style.loadJSON(extend(createStyleJSON(), {
910 'sources': {
911 'mapLibre': {
912 'type': 'vector',
913 'tiles': []
914 }
915 },
916 layers: [{
917 'id': 'my-layer',
918 'type': 'symbol',
919 'source': 'mapLibre',
920 'source-layer': 'libremap',
921 'filter': ['==', 'id', 0]
922 }]
923 }));
924
925 const layer = {
926 'id': 'my-layer',
927 'type': 'symbol',
928 'source': 'mapLibre',
929 'source-layer': 'libremap'
930 }as LayerSpecification;
931
932 style.on('data', (e) => {
933 if (e.dataType === 'source' && e.sourceDataType === 'content') {
934 style.sourceCaches['mapLibre'].reload = function() { done(); };
935 style.sourceCaches['mapLibre'].clearTiles = function() { done('test failed'); };
936 style.removeLayer('my-layer');
937 style.addLayer(layer);
938 style.update({} as EvaluationParameters);
939 }
940 });
941
942 });
943
944 test('clears source (instead of reloading) if adding this layer with a different type, immediately after removing it', done => {
945 const style = createStyle();
946 style.loadJSON(extend(createStyleJSON(), {
947 'sources': {
948 'mapLibre': {
949 'type': 'vector',
950 'tiles': []
951 }
952 },
953 layers: [{
954 'id': 'my-layer',
955 'type': 'symbol',
956 'source': 'mapLibre',
957 'source-layer': 'libremap',
958 'filter': ['==', 'id', 0]
959 }]
960 }));
961
962 const layer = {
963 'id': 'my-layer',
964 'type': 'circle',
965 'source': 'mapLibre',
966 'source-layer': 'libremap'
967 }as LayerSpecification;
968 style.on('data', (e) => {
969 if (e.dataType === 'source' && e.sourceDataType === 'content') {
970 style.sourceCaches['mapLibre'].reload = function() { done('test failed'); };
971 style.sourceCaches['mapLibre'].clearTiles = function() { done(); };
972 style.removeLayer('my-layer');
973 style.addLayer(layer);
974 style.update({} as EvaluationParameters);
975 }
976 });
977
978 });
979
980 test('fires "data" event', done => {
981 const style = new Style(getStubMap());
982 style.loadJSON(createStyleJSON());
983 const layer = {id: 'background', type: 'background'} as LayerSpecification;
984
985 style.once('data', () => { done(); });
986
987 style.on('style.load', () => {
988 style.addLayer(layer);
989 style.update({} as EvaluationParameters);
990 });
991 });
992
993 test('emits error on duplicates', done => {
994 const style = new Style(getStubMap());
995 style.loadJSON(createStyleJSON());
996 const layer = {id: 'background', type: 'background'} as LayerSpecification;
997
998 style.on('error', (e) => {
999 expect(e.error.message).toMatch(/already exists/);
1000 done();
1001 });
1002
1003 style.on('style.load', () => {
1004 style.addLayer(layer);
1005 style.addLayer(layer);
1006 });
1007 });
1008
1009 test('adds to the end by default', done => {
1010 const style = new Style(getStubMap());
1011 style.loadJSON(createStyleJSON({
1012 layers: [{
1013 id: 'a',
1014 type: 'background'
1015 }, {
1016 id: 'b',
1017 type: 'background'
1018 }]
1019 }));
1020 const layer = {id: 'c', type: 'background'} as LayerSpecification;
1021
1022 style.on('style.load', () => {
1023 style.addLayer(layer);
1024 expect(style._order).toEqual(['a', 'b', 'c']);
1025 done();
1026 });
1027 });
1028
1029 test('adds before the given layer', done => {
1030 const style = new Style(getStubMap());
1031 style.loadJSON(createStyleJSON({
1032 layers: [{
1033 id: 'a',
1034 type: 'background'
1035 }, {
1036 id: 'b',
1037 type: 'background'
1038 }]
1039 }));
1040 const layer = {id: 'c', type: 'background'} as LayerSpecification;
1041
1042 style.on('style.load', () => {
1043 style.addLayer(layer, 'a');
1044 expect(style._order).toEqual(['c', 'a', 'b']);
1045 done();
1046 });
1047 });
1048
1049 test('fire error if before layer does not exist', done => {
1050 const style = new Style(getStubMap());
1051 style.loadJSON(createStyleJSON({
1052 layers: [{
1053 id: 'a',
1054 type: 'background'
1055 }, {
1056 id: 'b',
1057 type: 'background'
1058 }]
1059 }));
1060 const layer = {id: 'c', type: 'background'} as LayerSpecification;
1061
1062 style.on('style.load', () => {
1063 style.on('error', (error) => {
1064 expect(error.error.message).toMatch(/Cannot add layer "c" before non-existing layer "z"./);
1065 done();
1066 });
1067 style.addLayer(layer, 'z');
1068 });
1069 });
1070
1071 test('fires an error on non-existant source layer', done => {
1072 const style = new Style(getStubMap());
1073 style.loadJSON(extend(createStyleJSON(), {
1074 sources: {
1075 dummy: {
1076 type: 'geojson',
1077 data: {type: 'FeatureCollection', features: []}
1078 }
1079 }
1080 }));
1081
1082 const layer = {
1083 id: 'dummy',
1084 type: 'fill',
1085 source: 'dummy',
1086 'source-layer': 'dummy'
1087 }as LayerSpecification;
1088
1089 style.on('style.load', () => {
1090 style.on('error', ({error}) => {
1091 expect(error.message).toMatch(/does not exist on source/);
1092 done();
1093 });
1094 style.addLayer(layer);
1095 });
1096
1097 });
1098});
1099
1100describe('Style#removeLayer', () => {
1101 test('throw before loaded', () => {
1102 const style = new Style(getStubMap());
1103 expect(() => style.removeLayer('background')).toThrow(/load/i);
1104 });
1105
1106 test('fires "data" event', done => {
1107 const style = new Style(getStubMap());
1108 style.loadJSON(createStyleJSON());
1109 const layer = {id: 'background', type: 'background'} as LayerSpecification;
1110
1111 style.once('data', () => { done(); });
1112
1113 style.on('style.load', () => {
1114 style.addLayer(layer);
1115 style.removeLayer('background');
1116 style.update({} as EvaluationParameters);
1117 });
1118 });
1119
1120 test('tears down layer event forwarding', done => {
1121 const style = new Style(getStubMap());
1122 style.loadJSON(createStyleJSON({
1123 layers: [{
1124 id: 'background',
1125 type: 'background'
1126 }]
1127 }));
1128
1129 style.on('error', () => {
1130 done('test failed');
1131 });
1132
1133 style.on('style.load', () => {
1134 const layer = style._layers.background;
1135 style.removeLayer('background');
1136
1137 // Bind a listener to prevent fallback Evented error reporting.
1138 layer.on('error', () => {});
1139
1140 layer.fire(new Event('error', {mapLibre: true}));
1141 done();
1142 });
1143 });
1144
1145 test('fires an error on non-existence', done => {
1146 const style = new Style(getStubMap());
1147 style.loadJSON(createStyleJSON());
1148
1149 style.on('style.load', () => {
1150 style.on('error', ({error}) => {
1151 expect(error.message).toMatch(/Cannot remove non-existing layer "background"./);
1152 done();
1153 });
1154 style.removeLayer('background');
1155 });
1156 });
1157
1158 test('removes from the order', done => {
1159 const style = new Style(getStubMap());
1160 style.loadJSON(createStyleJSON({
1161 layers: [{
1162 id: 'a',
1163 type: 'background'
1164 }, {
1165 id: 'b',
1166 type: 'background'
1167 }]
1168 }));
1169
1170 style.on('style.load', () => {
1171 style.removeLayer('a');
1172 expect(style._order).toEqual(['b']);
1173 done();
1174 });
1175 });
1176
1177 test('does not remove dereffed layers', done => {
1178 const style = new Style(getStubMap());
1179 style.loadJSON(createStyleJSON({
1180 layers: [{
1181 id: 'a',
1182 type: 'background'
1183 }, {
1184 id: 'b',
1185 ref: 'a'
1186 }]
1187 }));
1188
1189 style.on('style.load', () => {
1190 style.removeLayer('a');
1191 expect(style.getLayer('a')).toBeUndefined();
1192 expect(style.getLayer('b')).toBeDefined();
1193 done();
1194 });
1195 });
1196});
1197
1198describe('Style#moveLayer', () => {
1199 test('throw before loaded', () => {
1200 const style = new Style(getStubMap());
1201 expect(() => style.moveLayer('background')).toThrow(/load/i);
1202 });
1203
1204 test('fires "data" event', done => {
1205 const style = new Style(getStubMap());
1206 style.loadJSON(createStyleJSON());
1207 const layer = {id: 'background', type: 'background'} as LayerSpecification;
1208
1209 style.once('data', () => { done(); });
1210
1211 style.on('style.load', () => {
1212 style.addLayer(layer);
1213 style.moveLayer('background');
1214 style.update({} as EvaluationParameters);
1215 });
1216 });
1217
1218 test('fires an error on non-existence', done => {
1219 const style = new Style(getStubMap());
1220 style.loadJSON(createStyleJSON());
1221
1222 style.on('style.load', () => {
1223 style.on('error', ({error}) => {
1224 expect(error.message).toMatch(/does not exist in the map\'s style and cannot be moved/);
1225 done();
1226 });
1227 style.moveLayer('background');
1228 });
1229 });
1230
1231 test('changes the order', done => {
1232 const style = new Style(getStubMap());
1233 style.loadJSON(createStyleJSON({
1234 layers: [
1235 {id: 'a', type: 'background'},
1236 {id: 'b', type: 'background'},
1237 {id: 'c', type: 'background'}
1238 ]
1239 }));
1240
1241 style.on('style.load', () => {
1242 style.moveLayer('a', 'c');
1243 expect(style._order).toEqual(['b', 'a', 'c']);
1244 done();
1245 });
1246 });
1247
1248 test('moves to existing location', done => {
1249 const style = new Style(getStubMap());
1250 style.loadJSON(createStyleJSON({
1251 layers: [
1252 {id: 'a', type: 'background'},
1253 {id: 'b', type: 'background'},
1254 {id: 'c', type: 'background'}
1255 ]
1256 }));
1257
1258 style.on('style.load', () => {
1259 style.moveLayer('b', 'b');
1260 expect(style._order).toEqual(['a', 'b', 'c']);
1261 done();
1262 });
1263 });
1264});
1265
1266describe('Style#setPaintProperty', () => {
1267 test('#4738 postpones source reload until layers have been broadcast to workers', done => {
1268 const style = new Style(getStubMap());
1269 style.loadJSON(extend(createStyleJSON(), {
1270 'sources': {
1271 'geojson': {
1272 'type': 'geojson',
1273 'data': {'type': 'FeatureCollection', 'features': []}
1274 }
1275 },
1276 'layers': [
1277 {
1278 'id': 'circle',
1279 'type': 'circle',
1280 'source': 'geojson'
1281 }
1282 ]
1283 }));
1284
1285 const tr = new Transform();
1286 tr.resize(512, 512);
1287
1288 style.once('style.load', () => {
1289 style.update(tr.zoom as any as EvaluationParameters);
1290 const sourceCache = style.sourceCaches['geojson'];
1291 const source = style.getSource('geojson');
1292
1293 let begun = false;
1294 let styleUpdateCalled = false;
1295
1296 (source as any).on('data', (e) => setTimeout(() => {
1297 if (!begun && sourceCache.loaded()) {
1298 begun = true;
1299 jest.spyOn(sourceCache, 'reload').mockImplementation(() => {
1300 expect(styleUpdateCalled).toBeTruthy();
1301 done();
1302 });
1303
1304 (source as any).setData({'type': 'FeatureCollection', 'features': []});
1305 style.setPaintProperty('circle', 'circle-color', {type: 'identity', property: 'foo'});
1306 }
1307
1308 if (begun && e.sourceDataType === 'content') {
1309 // setData() worker-side work is complete; simulate an
1310 // animation frame a few ms later, so that this test can
1311 // confirm that SourceCache#reload() isn't called until
1312 // after the next Style#update()
1313 setTimeout(() => {
1314 styleUpdateCalled = true;
1315 style.update({} as EvaluationParameters);
1316 }, 50);
1317 }
1318 }));
1319 });
1320 });
1321
1322 test('#5802 clones the input', done => {
1323 const style = new Style(getStubMap());
1324 style.loadJSON({
1325 'version': 8,
1326 'sources': {},
1327 'layers': [
1328 {
1329 'id': 'background',
1330 'type': 'background'
1331 }
1332 ]
1333 });
1334
1335 style.on('style.load', () => {
1336 const value = {stops: [[0, 'red'], [10, 'blue']]};
1337 style.setPaintProperty('background', 'background-color', value);
1338 expect(style.getPaintProperty('background', 'background-color')).not.toBe(value);
1339 expect(style._changed).toBeTruthy();
1340
1341 style.update({} as EvaluationParameters);
1342 expect(style._changed).toBeFalsy();
1343
1344 value.stops[0][0] = 1;
1345 style.setPaintProperty('background', 'background-color', value);
1346 expect(style._changed).toBeTruthy();
1347
1348 done();
1349 });
1350 });
1351
1352 test('respects validate option', done => {
1353 const style = new Style(getStubMap());
1354 style.loadJSON({
1355 'version': 8,
1356 'sources': {},
1357 'layers': [
1358 {
1359 'id': 'background',
1360 'type': 'background'
1361 }
1362 ]
1363 });
1364
1365 style.on('style.load', () => {
1366 const backgroundLayer = style.getLayer('background');
1367 const validate = jest.spyOn(backgroundLayer, '_validate');
1368
1369 style.setPaintProperty('background', 'background-color', 'notacolor', {validate: false});
1370 expect(validate.mock.calls[0][4]).toEqual({validate: false});
1371 expect(mockConsoleError).not.toHaveBeenCalled();
1372
1373 expect(style._changed).toBeTruthy();
1374 style.update({} as EvaluationParameters);
1375
1376 style.setPaintProperty('background', 'background-color', 'alsonotacolor');
1377 expect(mockConsoleError).toHaveBeenCalledTimes(1);
1378 expect(validate.mock.calls[1][4]).toEqual({});
1379
1380 done();
1381 });
1382 });
1383});
1384
1385describe('Style#getPaintProperty', () => {
1386 test('#5802 clones the output', done => {
1387 const style = new Style(getStubMap());
1388 style.loadJSON({
1389 'version': 8,
1390 'sources': {},
1391 'layers': [
1392 {
1393 'id': 'background',
1394 'type': 'background'
1395 }
1396 ]
1397 });
1398
1399 style.on('style.load', () => {
1400 style.setPaintProperty('background', 'background-color', {stops: [[0, 'red'], [10, 'blue']]});
1401 style.update({} as EvaluationParameters);
1402 expect(style._changed).toBeFalsy();
1403
1404 const value = style.getPaintProperty('background', 'background-color');
1405 value['stops'][0][0] = 1;
1406 style.setPaintProperty('background', 'background-color', value);
1407 expect(style._changed).toBeTruthy();
1408
1409 done();
1410 });
1411 });
1412});
1413
1414describe('Style#setLayoutProperty', () => {
1415 test('#5802 clones the input', done => {
1416 const style = new Style(getStubMap());
1417 style.loadJSON({
1418 'version': 8,
1419 'sources': {
1420 'geojson': {
1421 'type': 'geojson',
1422 'data': {
1423 'type': 'FeatureCollection',
1424 'features': []
1425 }
1426 }
1427 },
1428 'layers': [
1429 {
1430 'id': 'line',
1431 'type': 'line',
1432 'source': 'geojson'
1433 }
1434 ]
1435 });
1436
1437 style.on('style.load', () => {
1438 const value = {stops: [[0, 'butt'], [10, 'round']]};
1439 style.setLayoutProperty('line', 'line-cap', value);
1440 expect(style.getLayoutProperty('line', 'line-cap')).not.toBe(value);
1441 expect(style._changed).toBeTruthy();
1442
1443 style.update({} as EvaluationParameters);
1444 expect(style._changed).toBeFalsy();
1445
1446 value.stops[0][0] = 1;
1447 style.setLayoutProperty('line', 'line-cap', value);
1448 expect(style._changed).toBeTruthy();
1449
1450 done();
1451 });
1452 });
1453
1454 test('respects validate option', done => {
1455 const style = new Style(getStubMap());
1456 style.loadJSON({
1457 'version': 8,
1458 'sources': {
1459 'geojson': {
1460 'type': 'geojson',
1461 'data': {
1462 'type': 'FeatureCollection',
1463 'features': []
1464 }
1465 }
1466 },
1467 'layers': [
1468 {
1469 'id': 'line',
1470 'type': 'line',
1471 'source': 'geojson'
1472 }
1473 ]
1474 });
1475
1476 style.on('style.load', () => {
1477 const lineLayer = style.getLayer('line');
1478 const validate = jest.spyOn(lineLayer, '_validate');
1479
1480 style.setLayoutProperty('line', 'line-cap', 'invalidcap', {validate: false});
1481 expect(validate.mock.calls[0][4]).toEqual({validate: false});
1482 expect(mockConsoleError).not.toHaveBeenCalled();
1483 expect(style._changed).toBeTruthy();
1484 style.update({} as EvaluationParameters);
1485
1486 style.setLayoutProperty('line', 'line-cap', 'differentinvalidcap');
1487 expect(mockConsoleError).toHaveBeenCalledTimes(1);
1488 expect(validate.mock.calls[1][4]).toEqual({});
1489
1490 done();
1491 });
1492 });
1493});
1494
1495describe('Style#getLayoutProperty', () => {
1496 test('#5802 clones the output', done => {
1497 const style = new Style(getStubMap());
1498 style.loadJSON({
1499 'version': 8,
1500 'sources': {
1501 'geojson': {
1502 'type': 'geojson',
1503 'data': {
1504 'type': 'FeatureCollection',
1505 'features': []
1506 }
1507 }
1508 },
1509 'layers': [
1510 {
1511 'id': 'line',
1512 'type': 'line',
1513 'source': 'geojson'
1514 }
1515 ]
1516 });
1517
1518 style.on('style.load', () => {
1519 style.setLayoutProperty('line', 'line-cap', {stops: [[0, 'butt'], [10, 'round']]});
1520 style.update({} as EvaluationParameters);
1521 expect(style._changed).toBeFalsy();
1522
1523 const value = style.getLayoutProperty('line', 'line-cap');
1524 value.stops[0][0] = 1;
1525 style.setLayoutProperty('line', 'line-cap', value);
1526 expect(style._changed).toBeTruthy();
1527
1528 done();
1529 });
1530 });
1531});
1532
1533describe('Style#setFilter', () => {
1534 test('throws if style is not loaded', () => {
1535 const style = new Style(getStubMap());
1536 expect(() => style.setFilter('symbol', ['==', 'id', 1])).toThrow(/load/i);
1537 });
1538
1539 function createStyle() {
1540 const style = new Style(getStubMap());
1541 style.loadJSON({
1542 version: 8,
1543 sources: {
1544 geojson: createGeoJSONSource() as GeoJSONSourceSpecification
1545 },
1546 layers: [
1547 {id: 'symbol', type: 'symbol', source: 'geojson', filter: ['==', 'id', 0]}
1548 ]
1549 });
1550 return style;
1551 }
1552
1553 test('sets filter', done => {
1554 const style = createStyle();
1555
1556 style.on('style.load', () => {
1557 style.dispatcher.broadcast = function(key, value) {
1558 expect(key).toBe('updateLayers');
1559 expect(value['layers'][0].id).toBe('symbol');
1560 expect(value['layers'][0].filter).toEqual(['==', 'id', 1]);
1561 done();
1562 };
1563
1564 style.setFilter('symbol', ['==', 'id', 1]);
1565 expect(style.getFilter('symbol')).toEqual(['==', 'id', 1]);
1566 style.update({} as EvaluationParameters); // trigger dispatcher broadcast
1567 });
1568 });
1569
1570 test('gets a clone of the filter', done => {
1571 const style = createStyle();
1572
1573 style.on('style.load', () => {
1574 const filter1 = ['==', 'id', 1] as FilterSpecification;
1575 style.setFilter('symbol', filter1);
1576 const filter2 = style.getFilter('symbol');
1577 const filter3 = style.getLayer('symbol').filter;
1578
1579 expect(filter1).not.toBe(filter2);
1580 expect(filter1).not.toBe(filter3);
1581 expect(filter2).not.toBe(filter3);
1582
1583 done();
1584 });
1585 });
1586
1587 test('sets again mutated filter', done => {
1588 const style = createStyle();
1589
1590 style.on('style.load', () => {
1591 const filter = ['==', 'id', 1] as FilterSpecification;
1592 style.setFilter('symbol', filter);
1593 style.update({} as EvaluationParameters); // flush pending operations
1594
1595 style.dispatcher.broadcast = function(key, value) {
1596 expect(key).toBe('updateLayers');
1597 expect(value['layers'][0].id).toBe('symbol');
1598 expect(value['layers'][0].filter).toEqual(['==', 'id', 2]);
1599 done();
1600 };
1601 filter[2] = 2;
1602 style.setFilter('symbol', filter);
1603 style.update({} as EvaluationParameters); // trigger dispatcher broadcast
1604 });
1605 });
1606
1607 test('unsets filter', done => {
1608 const style = createStyle();
1609 style.on('style.load', () => {
1610 style.setFilter('symbol', null);
1611 expect(style.getLayer('symbol').serialize()['filter']).toBeUndefined();
1612 done();
1613 });
1614 });
1615
1616 test('emits if invalid', done => {
1617 const style = createStyle();
1618 style.on('style.load', () => {
1619 style.on('error', () => {
1620 expect(style.getLayer('symbol').serialize()['filter']).toEqual(['==', 'id', 0]);
1621 done();
1622 });
1623 style.setFilter('symbol', ['==', '$type', 1]);
1624 });
1625 });
1626
1627 test('fires an error if layer not found', done => {
1628 const style = createStyle();
1629
1630 style.on('style.load', () => {
1631 style.on('error', ({error}) => {
1632 expect(error.message).toMatch(/Cannot filter non-existing layer "non-existant"./);
1633 done();
1634 });
1635 style.setFilter('non-existant', ['==', 'id', 1]);
1636 });
1637 });
1638
1639 test('validates filter by default', done => {
1640 const style = createStyle();
1641 style.on('style.load', () => {
1642 style.setFilter('symbol', 'notafilter' as any as FilterSpecification);
1643 expect(style.getFilter('symbol')).toEqual(['==', 'id', 0]);
1644 expect(mockConsoleError).toHaveBeenCalledTimes(1);
1645 style.update({} as EvaluationParameters); // trigger dispatcher broadcast
1646 done();
1647 });
1648 });
1649
1650 test('respects validate option', done => {
1651 const style = createStyle();
1652
1653 style.on('style.load', () => {
1654 style.dispatcher.broadcast = function(key, value) {
1655 expect(key).toBe('updateLayers');
1656 expect(value['layers'][0].id).toBe('symbol');
1657 expect(value['layers'][0].filter).toBe('notafilter');
1658 done();
1659 };
1660
1661 style.setFilter('symbol', 'notafilter' as any as FilterSpecification, {validate: false});
1662 expect(style.getFilter('symbol')).toBe('notafilter');
1663 style.update({} as EvaluationParameters); // trigger dispatcher broadcast
1664 });
1665 });
1666});
1667
1668describe('Style#setLayerZoomRange', () => {
1669 test('throw before loaded', () => {
1670 const style = new Style(getStubMap());
1671 expect(() => style.setLayerZoomRange('symbol', 5, 12)).toThrow(/load/i);
1672 });
1673
1674 function createStyle() {
1675 const style = new Style(getStubMap());
1676 style.loadJSON({
1677 'version': 8,
1678 'sources': {
1679 'geojson': createGeoJSONSource() as GeoJSONSourceSpecification
1680 },
1681 'layers': [{
1682 'id': 'symbol',
1683 'type': 'symbol',
1684 'source': 'geojson'
1685 }]
1686 });
1687 return style;
1688 }
1689
1690 test('sets zoom range', done => {
1691 const style = createStyle();
1692
1693 style.on('style.load', () => {
1694 style.dispatcher.broadcast = function(key, value) {
1695 expect(key).toBe('updateLayers');
1696 expect(value['layers'].map((layer) => { return layer.id; })).toEqual(['symbol']);
1697 done();
1698 };
1699 style.setLayerZoomRange('symbol', 5, 12);
1700 expect(style.getLayer('symbol').minzoom).toBe(5);
1701 expect(style.getLayer('symbol').maxzoom).toBe(12);
1702 style.update({} as EvaluationParameters); // trigger dispatcher broadcast
1703 });
1704 });
1705
1706 test('fires an error if layer not found', done => {
1707 const style = createStyle();
1708 style.on('style.load', () => {
1709 style.on('error', ({error}) => {
1710 expect(error.message).toMatch(/Cannot set the zoom range of non-existing layer "non-existant"./);
1711 done();
1712 });
1713 style.setLayerZoomRange('non-existant', 5, 12);
1714 });
1715 });
1716
1717 test('does not reload raster source', done => {
1718 const style = new Style(getStubMap());
1719 style.loadJSON({
1720 'version': 8,
1721 'sources': {
1722 'raster': {
1723 type: 'raster',
1724 tiles: ['http://tiles.server']
1725 }
1726 },
1727 'layers': [{
1728 'id': 'raster',
1729 'type': 'raster',
1730 'source': 'raster'
1731 }]
1732 });
1733
1734 style.on('style.load', () => {
1735 jest.spyOn(style, '_reloadSource');
1736
1737 style.setLayerZoomRange('raster', 5, 12);
1738 style.update(0 as any as EvaluationParameters);
1739 expect(style._reloadSource).not.toHaveBeenCalled();
1740 done();
1741 });
1742 });
1743});
1744
1745describe('Style#queryRenderedFeatures', () => {
1746
1747 let style;
1748 let transform;
1749
1750 beforeEach((callback) => {
1751 style = new Style(getStubMap());
1752 transform = new Transform();
1753 transform.resize(512, 512);
1754 function queryMapLibreFeatures(layers, serializedLayers, getFeatureState, queryGeom, cameraQueryGeom, scale, params) {
1755 const features = {
1756 'land': [{
1757 type: 'Feature',
1758 layer: style._layers.land.serialize(),
1759 geometry: {
1760 type: 'Polygon'
1761 }
1762 }, {
1763 type: 'Feature',
1764 layer: style._layers.land.serialize(),
1765 geometry: {
1766 type: 'Point'
1767 }
1768 }],
1769 'landref': [{
1770 type: 'Feature',
1771 layer: style._layers.landref.serialize(),
1772 geometry: {
1773 type: 'Line'
1774 }
1775 }]
1776 };
1777
1778 // format result to shape of tile.queryRenderedFeatures result
1779 for (const layer in features) {
1780 features[layer] = features[layer].map((feature, featureIndex) =>
1781 ({feature, featureIndex}));
1782 }
1783
1784 if (params.layers) {
1785 for (const l in features) {
1786 if (params.layers.indexOf(l) < 0) {
1787 delete features[l];
1788 }
1789 }
1790 }
1791
1792 return features;
1793 }
1794
1795 style.loadJSON({
1796 'version': 8,
1797 'sources': {
1798 'mapLibre': {
1799 'type': 'geojson',
1800 'data': {type: 'FeatureCollection', features: []}
1801 },
1802 'other': {
1803 'type': 'geojson',
1804 'data': {type: 'FeatureCollection', features: []}
1805 }
1806 },
1807 'layers': [{
1808 'id': 'land',
1809 'type': 'line',
1810 'source': 'mapLibre',
1811 'source-layer': 'water',
1812 'layout': {
1813 'line-cap': 'round'
1814 },
1815 'paint': {
1816 'line-color': 'red'
1817 },
1818 'metadata': {
1819 'something': 'else'
1820 }
1821 }, {
1822 'id': 'landref',
1823 'ref': 'land',
1824 'paint': {
1825 'line-color': 'blue'
1826 }
1827 } as any as LayerSpecification, {
1828 'id': 'land--other',
1829 'type': 'line',
1830 'source': 'other',
1831 'source-layer': 'water',
1832 'layout': {
1833 'line-cap': 'round'
1834 },
1835 'paint': {
1836 'line-color': 'red'
1837 },
1838 'metadata': {
1839 'something': 'else'
1840 }
1841 }]
1842 });
1843
1844 style.on('style.load', () => {
1845 style.sourceCaches.mapLibre.tilesIn = () => {
1846 return [{
1847 tile: {queryRenderedFeatures: queryMapLibreFeatures},
1848 tileID: new OverscaledTileID(0, 0, 0, 0, 0),
1849 queryGeometry: [],
1850 scale: 1
1851 }];
1852 };
1853 style.sourceCaches.other.tilesIn = () => {
1854 return [];
1855 };
1856
1857 style.sourceCaches.mapLibre.transform = transform;
1858 style.sourceCaches.other.transform = transform;
1859
1860 style.update(0 as any as EvaluationParameters);
1861 style._updateSources(transform);
1862 callback();
1863 });
1864 });
1865
1866 afterEach(() => {
1867 style = undefined;
1868 transform = undefined;
1869 });
1870
1871 test('returns feature type', () => {
1872 const results = style.queryRenderedFeatures([{x: 0, y: 0}], {}, transform);
1873 expect(results[0].geometry.type).toBe('Line');
1874 });
1875
1876 test('filters by `layers` option', () => {
1877 const results = style.queryRenderedFeatures([{x: 0, y: 0}], {layers: ['land']}, transform);
1878 expect(results).toHaveLength(2);
1879 });
1880
1881 test('checks type of `layers` option', () => {
1882 let errors = 0;
1883 jest.spyOn(style, 'fire').mockImplementation((event) => {
1884 if (event['error'] && event['error'].message.includes('parameters.layers must be an Array.')) {
1885 errors++;
1886 }
1887 });
1888 style.queryRenderedFeatures([{x: 0, y: 0}], {layers:'string'}, transform);
1889 expect(errors).toBe(1);
1890 });
1891
1892 test('includes layout properties', () => {
1893 const results = style.queryRenderedFeatures([{x: 0, y: 0}], {}, transform);
1894 const layout = results[0].layer.layout;
1895 expect(layout['line-cap']).toBe('round');
1896 });
1897
1898 test('includes paint properties', () => {
1899 const results = style.queryRenderedFeatures([{x: 0, y: 0}], {}, transform);
1900 expect(results[2].layer.paint['line-color']).toBe('red');
1901 });
1902
1903 test('includes metadata', () => {
1904 const results = style.queryRenderedFeatures([{x: 0, y: 0}], {}, transform);
1905
1906 const layer = results[1].layer;
1907 expect(layer.metadata.something).toBe('else');
1908
1909 });
1910
1911 test('include multiple layers', () => {
1912 const results = style.queryRenderedFeatures([{x: 0, y: 0}], {layers: ['land', 'landref']}, transform);
1913 expect(results).toHaveLength(3);
1914 });
1915
1916 test('does not query sources not implicated by `layers` parameter', () => {
1917 style.sourceCaches.mapLibre.queryRenderedFeatures = function() { expect(true).toBe(false); };
1918 style.queryRenderedFeatures([{x: 0, y: 0}], {layers: ['land--other']}, transform);
1919 });
1920
1921 test('fires an error if layer included in params does not exist on the style', () => {
1922 let errors = 0;
1923 jest.spyOn(style, 'fire').mockImplementation((event) => {
1924 if (event['error'] && event['error'].message.includes('does not exist in the map\'s style and cannot be queried for features.')) errors++;
1925 });
1926 const results = style.queryRenderedFeatures([{x: 0, y: 0}], {layers:['merp']}, transform);
1927 expect(errors).toBe(1);
1928 expect(results).toHaveLength(0);
1929 });
1930});
1931
1932describe('Style defers ...', () => {
1933 test('... expensive methods', done => {
1934 const style = new Style(getStubMap());
1935 style.loadJSON(createStyleJSON({
1936 'sources': {
1937 'streets': createGeoJSONSource(),
1938 'terrain': createGeoJSONSource()
1939 }
1940 }));
1941
1942 style.on('style.load', () => {
1943 style.update({} as EvaluationParameters);
1944
1945 // spies to track defered methods
1946 const mockStyleFire = jest.spyOn(style, 'fire');
1947 jest.spyOn(style, '_reloadSource');
1948 jest.spyOn(style, '_updateWorkerLayers');
1949
1950 style.addLayer({id: 'first', type: 'symbol', source: 'streets'});
1951 style.addLayer({id: 'second', type: 'symbol', source: 'streets'});
1952 style.addLayer({id: 'third', type: 'symbol', source: 'terrain'});
1953
1954 style.setPaintProperty('first', 'text-color', 'black');
1955 style.setPaintProperty('first', 'text-halo-color', 'white');
1956
1957 expect(style.fire).not.toHaveBeenCalled();
1958 expect(style._reloadSource).not.toHaveBeenCalled();
1959 expect(style._updateWorkerLayers).not.toHaveBeenCalled();
1960
1961 style.update({} as EvaluationParameters);
1962
1963 expect(mockStyleFire.mock.calls[0][0]['type']).toBe('data');
1964
1965 // called per source
1966 expect(style._reloadSource).toHaveBeenCalledTimes(2);
1967 expect(style._reloadSource).toHaveBeenCalledWith('streets');
1968 expect(style._reloadSource).toHaveBeenCalledWith('terrain');
1969
1970 // called once
1971 expect(style._updateWorkerLayers).toHaveBeenCalledTimes(1);
1972
1973 done();
1974 });
1975 });
1976});
1977
1978describe('Style#query*Features', () => {
1979
1980 // These tests only cover filter validation. Most tests for these methods
1981 // live in the integration tests.
1982
1983 let style;
1984 let onError;
1985 let transform;
1986
1987 beforeEach((callback) => {
1988 transform = new Transform();
1989 transform.resize(100, 100);
1990 style = new Style(getStubMap());
1991 style.loadJSON({
1992 'version': 8,
1993 'sources': {
1994 'geojson': createGeoJSONSource()
1995 },
1996 'layers': [{
1997 'id': 'symbol',
1998 'type': 'symbol',
1999 'source': 'geojson'
2000 }]
2001 });
2002
2003 onError = jest.fn();
2004
2005 style.on('error', onError)
2006 .on('style.load', () => {
2007 callback();
2008 });
2009 });
2010
2011 test('querySourceFeatures emits an error on incorrect filter', () => {
2012 expect(style.querySourceFeatures([10, 100], {filter: 7}, transform)).toEqual([]);
2013 expect(onError.mock.calls[0][0].error.message).toMatch(/querySourceFeatures\.filter/);
2014 });
2015
2016 test('queryRenderedFeatures emits an error on incorrect filter', () => {
2017 expect(style.queryRenderedFeatures([{x: 0, y: 0}], {filter: 7}, transform)).toEqual([]);
2018 expect(onError.mock.calls[0][0].error.message).toMatch(/queryRenderedFeatures\.filter/);
2019 });
2020
2021 test('querySourceFeatures not raise validation errors if validation was disabled', () => {
2022 let errors = 0;
2023 jest.spyOn(style, 'fire').mockImplementation((event) => {
2024 if (event['error']) {
2025 errors++;
2026 }
2027 });
2028 style.queryRenderedFeatures([{x: 0, y: 0}], {filter: 'invalidFilter', validate: false}, transform);
2029 expect(errors).toBe(0);
2030 });
2031
2032 test('querySourceFeatures not raise validation errors if validation was disabled', () => {
2033 let errors = 0;
2034 jest.spyOn(style, 'fire').mockImplementation((event) => {
2035 if (event['error']) errors++;
2036 });
2037 style.querySourceFeatures([{x: 0, y: 0}], {filter: 'invalidFilter', validate: false}, transform);
2038 expect(errors).toBe(0);
2039 });
2040});
2041
2042describe('Style#addSourceType', () => {
2043 const _types = {'existing' () {}};
2044
2045 jest.spyOn(Style, 'getSourceType').mockImplementation(name => _types[name]);
2046 jest.spyOn(Style, 'setSourceType').mockImplementation((name, create) => {
2047 _types[name] = create;
2048 });
2049
2050 test('adds factory function', done => {
2051 const style = new Style(getStubMap());
2052 const sourceType = function () {} as any as SourceClass;
2053
2054 // expect no call to load worker source
2055 style.dispatcher.broadcast = function (type) {
2056 if (type === 'loadWorkerSource') {
2057 done('test failed');
2058 }
2059 };
2060
2061 style.addSourceType('foo', sourceType, () => {
2062 expect(_types['foo']).toBe(sourceType);
2063 done();
2064 });
2065 });
2066
2067 test('triggers workers to load worker source code', done => {
2068 const style = new Style(getStubMap());
2069 const sourceType = function () {} as any as SourceClass;
2070 sourceType.workerSourceURL = 'worker-source.js' as any as URL;
2071
2072 style.dispatcher.broadcast = function (type, params) {
2073 if (type === 'loadWorkerSource') {
2074 expect(_types['bar']).toBe(sourceType);
2075 expect(params['name']).toBe('bar');
2076 expect(params['url']).toBe('worker-source.js');
2077 done();
2078 }
2079 };
2080
2081 style.addSourceType('bar', sourceType, (err) => { expect(err).toBeFalsy(); });
2082 });
2083
2084 test('refuses to add new type over existing name', done => {
2085 const style = new Style(getStubMap());
2086 const sourceType = function () {} as any as SourceClass;
2087 style.addSourceType('existing', sourceType, (err) => {
2088 expect(err).toBeTruthy();
2089 done();
2090 });
2091 });
2092});
2093
2094describe('Style#hasTransitions', () => {
2095 test('returns false when the style is loading', () => {
2096 const style = new Style(getStubMap());
2097 expect(style.hasTransitions()).toBe(false);
2098 });
2099
2100 test('returns true when a property is transitioning', done => {
2101 const style = new Style(getStubMap());
2102 style.loadJSON({
2103 'version': 8,
2104 'sources': {},
2105 'layers': [{
2106 'id': 'background',
2107 'type': 'background'
2108 }]
2109 });
2110
2111 style.on('style.load', () => {
2112 style.setPaintProperty('background', 'background-color', 'blue');
2113 style.update({transition: {duration: 300, delay: 0}} as EvaluationParameters);
2114 expect(style.hasTransitions()).toBe(true);
2115 done();
2116 });
2117 });
2118
2119 test('returns false when a property is not transitioning', done => {
2120 const style = new Style(getStubMap());
2121 style.loadJSON({
2122 'version': 8,
2123 'sources': {},
2124 'layers': [{
2125 'id': 'background',
2126 'type': 'background'
2127 }]
2128 });
2129
2130 style.on('style.load', () => {
2131 style.setPaintProperty('background', 'background-color', 'blue');
2132 style.update({transition: {duration: 0, delay: 0}} as EvaluationParameters);
2133 expect(style.hasTransitions()).toBe(false);
2134 done();
2135 });
2136 });
2137});