UNPKG

49.3 kBPlain TextView Raw
1import assert from 'assert';
2
3import {Event, ErrorEvent, Evented} from '../util/evented';
4import StyleLayer from './style_layer';
5import createStyleLayer from './create_style_layer';
6import loadSprite from './load_sprite';
7import ImageManager from '../render/image_manager';
8import GlyphManager from '../render/glyph_manager';
9import Light from './light';
10import LineAtlas from '../render/line_atlas';
11import {pick, clone, extend, deepEqual, filterObject, mapObject} from '../util/util';
12import {getJSON, getReferrer, makeRequest, ResourceType} from '../util/ajax';
13import browser from '../util/browser';
14import Dispatcher from '../util/dispatcher';
15import {validateStyle, emitValidationErrors as _emitValidationErrors} from './validate_style';
16import {getSourceType, setSourceType, Source} from '../source/source';
17import type {SourceClass} from '../source/source';
18import {queryRenderedFeatures, queryRenderedSymbols, querySourceFeatures} from '../source/query_features';
19import SourceCache from '../source/source_cache';
20import GeoJSONSource from '../source/geojson_source';
21import styleSpec from '../style-spec/reference/latest';
22import getWorkerPool from '../util/global_worker_pool';
23import deref from '../style-spec/deref';
24import emptyStyle from '../style-spec/empty';
25import diffStyles, {operations as diffOperations} from '../style-spec/diff';
26import {
27 registerForPluginStateChange,
28 evented as rtlTextPluginEvented,
29 triggerPluginCompletionEvent
30} from '../source/rtl_text_plugin';
31import PauseablePlacement from './pauseable_placement';
32import ZoomHistory from './zoom_history';
33import CrossTileSymbolIndex from '../symbol/cross_tile_symbol_index';
34import {validateCustomStyleLayer} from './style_layer/custom_style_layer';
35import type {MapGeoJSONFeature} from '../util/vectortile_to_geojson';
36
37// We're skipping validation errors with the `source.canvas` identifier in order
38// to continue to allow canvas sources to be added at runtime/updated in
39// smart setStyle (see https://github.com/mapbox/mapbox-gl-js/pull/6424):
40const emitValidationErrors = (evented: Evented, errors?: ReadonlyArray<{
41 message: string;
42 identifier?: string;
43}> | null) =>
44 _emitValidationErrors(evented, errors && errors.filter(error => error.identifier !== 'source.canvas'));
45
46import type Map from '../ui/map';
47import type Transform from '../geo/transform';
48import type {StyleImage} from './style_image';
49import type {StyleGlyph} from './style_glyph';
50import type {Callback} from '../types/callback';
51import type EvaluationParameters from './evaluation_parameters';
52import type {Placement} from '../symbol/placement';
53import type {Cancelable} from '../types/cancelable';
54import type {RequestParameters, ResponseCallback} from '../util/ajax';
55import type {
56 LayerSpecification,
57 FilterSpecification,
58 StyleSpecification,
59 LightSpecification,
60 SourceSpecification
61} from '../style-spec/types.g';
62import type {CustomLayerInterface} from './style_layer/custom_style_layer';
63import type {Validator} from './validate_style';
64import type {OverscaledTileID} from '../source/tile_id';
65
66const supportedDiffOperations = pick(diffOperations, [
67 'addLayer',
68 'removeLayer',
69 'setPaintProperty',
70 'setLayoutProperty',
71 'setFilter',
72 'addSource',
73 'removeSource',
74 'setLayerZoomRange',
75 'setLight',
76 'setTransition',
77 'setGeoJSONSourceData'
78 // 'setGlyphs',
79 // 'setSprite',
80]);
81
82const ignoredDiffOperations = pick(diffOperations, [
83 'setCenter',
84 'setZoom',
85 'setBearing',
86 'setPitch'
87]);
88
89const empty = emptyStyle() as StyleSpecification;
90
91export type FeatureIdentifier = {
92 id?: string | number | undefined;
93 source: string;
94 sourceLayer?: string | undefined;
95};
96
97export type StyleOptions = {
98 validate?: boolean;
99 localIdeographFontFamily?: string;
100};
101
102export type StyleSetterOptions = {
103 validate?: boolean;
104};
105/**
106 * @private
107 */
108class Style extends Evented {
109 map: Map;
110 stylesheet: StyleSpecification;
111 dispatcher: Dispatcher;
112 imageManager: ImageManager;
113 glyphManager: GlyphManager;
114 lineAtlas: LineAtlas;
115 light: Light;
116
117 _request: Cancelable;
118 _spriteRequest: Cancelable;
119 _layers: {[_: string]: StyleLayer};
120 _serializedLayers: {[_: string]: any};
121 _order: Array<string>;
122 sourceCaches: {[_: string]: SourceCache};
123 zoomHistory: ZoomHistory;
124 _loaded: boolean;
125 _rtlTextPluginCallback: (a: any) => any;
126 _changed: boolean;
127 _updatedSources: {[_: string]: 'clear' | 'reload'};
128 _updatedLayers: {[_: string]: true};
129 _removedLayers: {[_: string]: StyleLayer};
130 _changedImages: {[_: string]: true};
131 _updatedPaintProps: {[layer: string]: true};
132 _layerOrderChanged: boolean;
133 _availableImages: Array<string>;
134
135 crossTileSymbolIndex: CrossTileSymbolIndex;
136 pauseablePlacement: PauseablePlacement;
137 placement: Placement;
138 z: number;
139
140 // exposed to allow stubbing by unit tests
141 static getSourceType: typeof getSourceType;
142 static setSourceType: typeof setSourceType;
143 static registerForPluginStateChange: typeof registerForPluginStateChange;
144
145 constructor(map: Map, options: StyleOptions = {}) {
146 super();
147
148 this.map = map;
149 this.dispatcher = new Dispatcher(getWorkerPool(), this);
150 this.imageManager = new ImageManager();
151 this.imageManager.setEventedParent(this);
152 this.glyphManager = new GlyphManager(map._requestManager, options.localIdeographFontFamily);
153 this.lineAtlas = new LineAtlas(256, 512);
154 this.crossTileSymbolIndex = new CrossTileSymbolIndex();
155
156 this._layers = {};
157 this._serializedLayers = {};
158 this._order = [];
159 this.sourceCaches = {};
160 this.zoomHistory = new ZoomHistory();
161 this._loaded = false;
162 this._availableImages = [];
163
164 this._resetUpdates();
165
166 this.dispatcher.broadcast('setReferrer', getReferrer());
167
168 const self = this;
169 this._rtlTextPluginCallback = Style.registerForPluginStateChange((event) => {
170 const state = {
171 pluginStatus: event.pluginStatus,
172 pluginURL: event.pluginURL
173 };
174 self.dispatcher.broadcast('syncRTLPluginState', state, (err, results) => {
175 triggerPluginCompletionEvent(err);
176 if (results) {
177 const allComplete = results.every((elem) => elem);
178 if (allComplete) {
179 for (const id in self.sourceCaches) {
180 self.sourceCaches[id].reload(); // Should be a no-op if the plugin loads before any tiles load
181 }
182 }
183 }
184
185 });
186 });
187
188 this.on('data', (event) => {
189 if (event.dataType !== 'source' || event.sourceDataType !== 'metadata') {
190 return;
191 }
192
193 const sourceCache = this.sourceCaches[event.sourceId];
194 if (!sourceCache) {
195 return;
196 }
197
198 const source = sourceCache.getSource();
199 if (!source || !source.vectorLayerIds) {
200 return;
201 }
202
203 for (const layerId in this._layers) {
204 const layer = this._layers[layerId];
205 if (layer.source === source.id) {
206 this._validateLayer(layer);
207 }
208 }
209 });
210 }
211
212 loadURL(url: string, options: {
213 validate?: boolean;
214 } = {}) {
215 this.fire(new Event('dataloading', {dataType: 'style'}));
216
217 const validate = typeof options.validate === 'boolean' ?
218 options.validate : true;
219
220 const request = this.map._requestManager.transformRequest(url, ResourceType.Style);
221 this._request = getJSON(request, (error?: Error | null, json?: any | null) => {
222 this._request = null;
223 if (error) {
224 this.fire(new ErrorEvent(error));
225 } else if (json) {
226 this._load(json, validate);
227 }
228 });
229 }
230
231 loadJSON(json: StyleSpecification, options: StyleSetterOptions = {}) {
232 this.fire(new Event('dataloading', {dataType: 'style'}));
233
234 this._request = browser.frame(() => {
235 this._request = null;
236 this._load(json, options.validate !== false);
237 });
238 }
239
240 loadEmpty() {
241 this.fire(new Event('dataloading', {dataType: 'style'}));
242 this._load(empty, false);
243 }
244
245 _load(json: StyleSpecification, validate: boolean) {
246 if (validate && emitValidationErrors(this, validateStyle(json))) {
247 return;
248 }
249
250 this._loaded = true;
251 this.stylesheet = json;
252
253 for (const id in json.sources) {
254 this.addSource(id, json.sources[id], {validate: false});
255 }
256
257 if (json.sprite) {
258 this._loadSprite(json.sprite);
259 } else {
260 this.imageManager.setLoaded(true);
261 }
262
263 this.glyphManager.setURL(json.glyphs);
264
265 const layers = deref(this.stylesheet.layers);
266
267 this._order = layers.map((layer) => layer.id);
268
269 this._layers = {};
270 this._serializedLayers = {};
271 for (let layer of layers) {
272 layer = createStyleLayer(layer);
273 layer.setEventedParent(this, {layer: {id: layer.id}});
274 this._layers[layer.id] = layer;
275 this._serializedLayers[layer.id] = layer.serialize();
276 }
277 this.dispatcher.broadcast('setLayers', this._serializeLayers(this._order));
278
279 this.light = new Light(this.stylesheet.light);
280
281 this.fire(new Event('data', {dataType: 'style'}));
282 this.fire(new Event('style.load'));
283 }
284
285 _loadSprite(url: string) {
286 this._spriteRequest = loadSprite(url, this.map._requestManager, this.map.getPixelRatio(), (err, images) => {
287 this._spriteRequest = null;
288 if (err) {
289 this.fire(new ErrorEvent(err));
290 } else if (images) {
291 for (const id in images) {
292 this.imageManager.addImage(id, images[id]);
293 }
294 }
295
296 this.imageManager.setLoaded(true);
297 this._availableImages = this.imageManager.listImages();
298 this.dispatcher.broadcast('setImages', this._availableImages);
299 this.fire(new Event('data', {dataType: 'style'}));
300 });
301 }
302
303 _validateLayer(layer: StyleLayer) {
304 const sourceCache = this.sourceCaches[layer.source];
305 if (!sourceCache) {
306 return;
307 }
308
309 const sourceLayer = layer.sourceLayer;
310 if (!sourceLayer) {
311 return;
312 }
313
314 const source = sourceCache.getSource();
315 if (source.type === 'geojson' || (source.vectorLayerIds && source.vectorLayerIds.indexOf(sourceLayer) === -1)) {
316 this.fire(new ErrorEvent(new Error(
317 `Source layer "${sourceLayer}" ` +
318 `does not exist on source "${source.id}" ` +
319 `as specified by style layer "${layer.id}".`
320 )));
321 }
322 }
323
324 loaded() {
325 if (!this._loaded)
326 return false;
327
328 if (Object.keys(this._updatedSources).length)
329 return false;
330
331 for (const id in this.sourceCaches)
332 if (!this.sourceCaches[id].loaded())
333 return false;
334
335 if (!this.imageManager.isLoaded())
336 return false;
337
338 return true;
339 }
340
341 _serializeLayers(ids: Array<string>): Array<LayerSpecification> {
342 const serializedLayers = [];
343 for (const id of ids) {
344 const layer = this._layers[id];
345 if (layer.type !== 'custom') {
346 serializedLayers.push(layer.serialize());
347 }
348 }
349 return serializedLayers;
350 }
351
352 hasTransitions() {
353 if (this.light && this.light.hasTransition()) {
354 return true;
355 }
356
357 for (const id in this.sourceCaches) {
358 if (this.sourceCaches[id].hasTransition()) {
359 return true;
360 }
361 }
362
363 for (const id in this._layers) {
364 if (this._layers[id].hasTransition()) {
365 return true;
366 }
367 }
368
369 return false;
370 }
371
372 _checkLoaded() {
373 if (!this._loaded) {
374 throw new Error('Style is not done loading.');
375 }
376 }
377
378 /**
379 * Apply queued style updates in a batch and recalculate zoom-dependent paint properties.
380 * @private
381 */
382 update(parameters: EvaluationParameters) {
383 if (!this._loaded) {
384 return;
385 }
386
387 const changed = this._changed;
388 if (this._changed) {
389 const updatedIds = Object.keys(this._updatedLayers);
390 const removedIds = Object.keys(this._removedLayers);
391
392 if (updatedIds.length || removedIds.length) {
393 this._updateWorkerLayers(updatedIds, removedIds);
394 }
395 for (const id in this._updatedSources) {
396 const action = this._updatedSources[id];
397 assert(action === 'reload' || action === 'clear');
398 if (action === 'reload') {
399 this._reloadSource(id);
400 } else if (action === 'clear') {
401 this._clearSource(id);
402 }
403 }
404
405 this._updateTilesForChangedImages();
406
407 for (const id in this._updatedPaintProps) {
408 this._layers[id].updateTransitions(parameters);
409 }
410
411 this.light.updateTransitions(parameters);
412
413 this._resetUpdates();
414 }
415
416 const sourcesUsedBefore = {};
417
418 for (const sourceId in this.sourceCaches) {
419 const sourceCache = this.sourceCaches[sourceId];
420 sourcesUsedBefore[sourceId] = sourceCache.used;
421 sourceCache.used = false;
422 }
423
424 for (const layerId of this._order) {
425 const layer = this._layers[layerId];
426
427 layer.recalculate(parameters, this._availableImages);
428 if (!layer.isHidden(parameters.zoom) && layer.source) {
429 this.sourceCaches[layer.source].used = true;
430 }
431 }
432
433 for (const sourceId in sourcesUsedBefore) {
434 const sourceCache = this.sourceCaches[sourceId];
435 if (sourcesUsedBefore[sourceId] !== sourceCache.used) {
436 sourceCache.fire(new Event('data', {sourceDataType: 'visibility', dataType:'source', sourceId}));
437 }
438 }
439
440 this.light.recalculate(parameters);
441 this.z = parameters.zoom;
442
443 if (changed) {
444 this.fire(new Event('data', {dataType: 'style'}));
445 }
446
447 }
448
449 /*
450 * Apply any queued image changes.
451 */
452 _updateTilesForChangedImages() {
453 const changedImages = Object.keys(this._changedImages);
454 if (changedImages.length) {
455 for (const name in this.sourceCaches) {
456 this.sourceCaches[name].reloadTilesForDependencies(['icons', 'patterns'], changedImages);
457 }
458 this._changedImages = {};
459 }
460 }
461
462 _updateWorkerLayers(updatedIds: Array<string>, removedIds: Array<string>) {
463 this.dispatcher.broadcast('updateLayers', {
464 layers: this._serializeLayers(updatedIds),
465 removedIds
466 });
467 }
468
469 _resetUpdates() {
470 this._changed = false;
471
472 this._updatedLayers = {};
473 this._removedLayers = {};
474
475 this._updatedSources = {};
476 this._updatedPaintProps = {};
477
478 this._changedImages = {};
479 }
480
481 /**
482 * Update this style's state to match the given style JSON, performing only
483 * the necessary mutations.
484 *
485 * May throw an Error ('Unimplemented: METHOD') if the mapbox-gl-style-spec
486 * diff algorithm produces an operation that is not supported.
487 *
488 * @returns {boolean} true if any changes were made; false otherwise
489 * @private
490 */
491 setState(nextState: StyleSpecification) {
492 this._checkLoaded();
493
494 if (emitValidationErrors(this, validateStyle(nextState))) return false;
495
496 nextState = clone(nextState);
497 nextState.layers = deref(nextState.layers);
498
499 const changes = diffStyles(this.serialize(), nextState)
500 .filter(op => !(op.command in ignoredDiffOperations));
501
502 if (changes.length === 0) {
503 return false;
504 }
505
506 const unimplementedOps = changes.filter(op => !(op.command in supportedDiffOperations));
507 if (unimplementedOps.length > 0) {
508 throw new Error(`Unimplemented: ${unimplementedOps.map(op => op.command).join(', ')}.`);
509 }
510
511 changes.forEach((op) => {
512 if (op.command === 'setTransition') {
513 // `transition` is always read directly off of
514 // `this.stylesheet`, which we update below
515 return;
516 }
517 (this as any)[op.command].apply(this, op.args);
518 });
519
520 this.stylesheet = nextState;
521
522 return true;
523 }
524
525 addImage(id: string, image: StyleImage) {
526 if (this.getImage(id)) {
527 return this.fire(new ErrorEvent(new Error(`An image named "${id}" already exists.`)));
528 }
529 this.imageManager.addImage(id, image);
530 this._afterImageUpdated(id);
531 }
532
533 updateImage(id: string, image: StyleImage) {
534 this.imageManager.updateImage(id, image);
535 }
536
537 getImage(id: string): StyleImage {
538 return this.imageManager.getImage(id);
539 }
540
541 removeImage(id: string) {
542 if (!this.getImage(id)) {
543 return this.fire(new ErrorEvent(new Error(`An image named "${id}" does not exist.`)));
544 }
545 this.imageManager.removeImage(id);
546 this._afterImageUpdated(id);
547 }
548
549 _afterImageUpdated(id: string) {
550 this._availableImages = this.imageManager.listImages();
551 this._changedImages[id] = true;
552 this._changed = true;
553 this.dispatcher.broadcast('setImages', this._availableImages);
554 this.fire(new Event('data', {dataType: 'style'}));
555 }
556
557 listImages() {
558 this._checkLoaded();
559
560 return this.imageManager.listImages();
561 }
562
563 addSource(id: string, source: SourceSpecification, options: StyleSetterOptions = {}) {
564 this._checkLoaded();
565
566 if (this.sourceCaches[id] !== undefined) {
567 throw new Error(`Source "${id}" already exists.`);
568 }
569
570 if (!source.type) {
571 throw new Error(`The type property must be defined, but only the following properties were given: ${Object.keys(source).join(', ')}.`);
572 }
573
574 const builtIns = ['vector', 'raster', 'geojson', 'video', 'image'];
575 const shouldValidate = builtIns.indexOf(source.type) >= 0;
576 if (shouldValidate && this._validate(validateStyle.source, `sources.${id}`, source, null, options)) return;
577
578 if (this.map && this.map._collectResourceTiming) (source as any).collectResourceTiming = true;
579 const sourceCache = this.sourceCaches[id] = new SourceCache(id, source, this.dispatcher);
580 sourceCache.style = this;
581 sourceCache.setEventedParent(this, () => ({
582 isSourceLoaded: this.loaded(),
583 source: sourceCache.serialize(),
584 sourceId: id
585 }));
586
587 sourceCache.onAdd(this.map);
588 this._changed = true;
589 }
590
591 /**
592 * Remove a source from this stylesheet, given its id.
593 * @param {string} id id of the source to remove
594 * @throws {Error} if no source is found with the given ID
595 * @returns {Map} The {@link Map} object.
596 */
597 removeSource(id: string) {
598 this._checkLoaded();
599
600 if (this.sourceCaches[id] === undefined) {
601 throw new Error('There is no source with this ID');
602 }
603 for (const layerId in this._layers) {
604 if (this._layers[layerId].source === id) {
605 return this.fire(new ErrorEvent(new Error(`Source "${id}" cannot be removed while layer "${layerId}" is using it.`)));
606 }
607 }
608
609 const sourceCache = this.sourceCaches[id];
610 delete this.sourceCaches[id];
611 delete this._updatedSources[id];
612 sourceCache.fire(new Event('data', {sourceDataType: 'metadata', dataType:'source', sourceId: id}));
613 sourceCache.setEventedParent(null);
614 sourceCache.onRemove(this.map);
615 this._changed = true;
616 }
617
618 /**
619 * Set the data of a GeoJSON source, given its id.
620 * @param {string} id id of the source
621 * @param {GeoJSON|string} data GeoJSON source
622 */
623 setGeoJSONSourceData(id: string, data: GeoJSON.GeoJSON | string) {
624 this._checkLoaded();
625
626 assert(this.sourceCaches[id] !== undefined, 'There is no source with this ID');
627 const geojsonSource: GeoJSONSource = (this.sourceCaches[id].getSource() as any);
628 assert(geojsonSource.type === 'geojson');
629
630 geojsonSource.setData(data);
631 this._changed = true;
632 }
633
634 /**
635 * Get a source by id.
636 * @param {string} id id of the desired source
637 * @returns {Source | undefined} source
638 */
639 getSource(id: string): Source | undefined {
640 return this.sourceCaches[id] && this.sourceCaches[id].getSource();
641 }
642
643 /**
644 * Add a layer to the map style. The layer will be inserted before the layer with
645 * ID `before`, or appended if `before` is omitted.
646 * @param {Object | CustomLayerInterface} layerObject The style layer to add.
647 * @param {string} [before] ID of an existing layer to insert before
648 * @param {Object} options Style setter options.
649 * @returns {Map} The {@link Map} object.
650 */
651 addLayer(layerObject: LayerSpecification | CustomLayerInterface, before?: string, options: StyleSetterOptions = {}) {
652 this._checkLoaded();
653
654 const id = layerObject.id;
655
656 if (this.getLayer(id)) {
657 this.fire(new ErrorEvent(new Error(`Layer "${id}" already exists on this map.`)));
658 return;
659 }
660
661 let layer;
662 if (layerObject.type === 'custom') {
663
664 if (emitValidationErrors(this, validateCustomStyleLayer(layerObject))) return;
665
666 layer = createStyleLayer(layerObject);
667
668 } else {
669 if (typeof (layerObject as any).source === 'object') {
670 this.addSource(id, (layerObject as any).source);
671 layerObject = clone(layerObject);
672 layerObject = (extend(layerObject, {source: id}) as any);
673 }
674
675 // this layer is not in the style.layers array, so we pass an impossible array index
676 if (this._validate(validateStyle.layer,
677 `layers.${id}`, layerObject, {arrayIndex: -1}, options)) return;
678
679 layer = createStyleLayer(layerObject);
680 this._validateLayer(layer);
681
682 layer.setEventedParent(this, {layer: {id}});
683 this._serializedLayers[layer.id] = layer.serialize();
684 }
685
686 const index = before ? this._order.indexOf(before) : this._order.length;
687 if (before && index === -1) {
688 this.fire(new ErrorEvent(new Error(`Cannot add layer "${id}" before non-existing layer "${before}".`)));
689 return;
690 }
691
692 this._order.splice(index, 0, id);
693 this._layerOrderChanged = true;
694
695 this._layers[id] = layer;
696
697 if (this._removedLayers[id] && layer.source && layer.type !== 'custom') {
698 // If, in the current batch, we have already removed this layer
699 // and we are now re-adding it with a different `type`, then we
700 // need to clear (rather than just reload) the underyling source's
701 // tiles. Otherwise, tiles marked 'reloading' will have buckets /
702 // buffers that are set up for the _previous_ version of this
703 // layer, causing, e.g.:
704 // https://github.com/mapbox/mapbox-gl-js/issues/3633
705 const removed = this._removedLayers[id];
706 delete this._removedLayers[id];
707 if (removed.type !== layer.type) {
708 this._updatedSources[layer.source] = 'clear';
709 } else {
710 this._updatedSources[layer.source] = 'reload';
711 this.sourceCaches[layer.source].pause();
712 }
713 }
714 this._updateLayer(layer);
715
716 if (layer.onAdd) {
717 layer.onAdd(this.map);
718 }
719 }
720
721 /**
722 * Moves a layer to a different z-position. The layer will be inserted before the layer with
723 * ID `before`, or appended if `before` is omitted.
724 * @param {string} id ID of the layer to move
725 * @param {string} [before] ID of an existing layer to insert before
726 */
727 moveLayer(id: string, before?: string) {
728 this._checkLoaded();
729 this._changed = true;
730
731 const layer = this._layers[id];
732 if (!layer) {
733 this.fire(new ErrorEvent(new Error(`The layer '${id}' does not exist in the map's style and cannot be moved.`)));
734 return;
735 }
736
737 if (id === before) {
738 return;
739 }
740
741 const index = this._order.indexOf(id);
742 this._order.splice(index, 1);
743
744 const newIndex = before ? this._order.indexOf(before) : this._order.length;
745 if (before && newIndex === -1) {
746 this.fire(new ErrorEvent(new Error(`Cannot move layer "${id}" before non-existing layer "${before}".`)));
747 return;
748 }
749 this._order.splice(newIndex, 0, id);
750
751 this._layerOrderChanged = true;
752 }
753
754 /**
755 * Remove the layer with the given id from the style.
756 *
757 * If no such layer exists, an `error` event is fired.
758 *
759 * @param {string} id id of the layer to remove
760 * @fires error
761 */
762 removeLayer(id: string) {
763 this._checkLoaded();
764
765 const layer = this._layers[id];
766 if (!layer) {
767 this.fire(new ErrorEvent(new Error(`Cannot remove non-existing layer "${id}".`)));
768 return;
769 }
770
771 layer.setEventedParent(null);
772
773 const index = this._order.indexOf(id);
774 this._order.splice(index, 1);
775
776 this._layerOrderChanged = true;
777 this._changed = true;
778 this._removedLayers[id] = layer;
779 delete this._layers[id];
780 delete this._serializedLayers[id];
781 delete this._updatedLayers[id];
782 delete this._updatedPaintProps[id];
783
784 if (layer.onRemove) {
785 layer.onRemove(this.map);
786 }
787 }
788
789 /**
790 * Return the style layer object with the given `id`.
791 *
792 * @param {string} id - id of the desired layer
793 * @returns {?Object} a layer, if one with the given `id` exists
794 */
795 getLayer(id: string): StyleLayer {
796 return this._layers[id];
797 }
798
799 /**
800 * checks if a specific layer is present within the style.
801 *
802 * @param {string} id - id of the desired layer
803 * @returns {boolean} a boolean specifying if the given layer is present
804 */
805 hasLayer(id: string): boolean {
806 return id in this._layers;
807 }
808
809 setLayerZoomRange(layerId: string, minzoom?: number | null, maxzoom?: number | null) {
810 this._checkLoaded();
811
812 const layer = this.getLayer(layerId);
813 if (!layer) {
814 this.fire(new ErrorEvent(new Error(`Cannot set the zoom range of non-existing layer "${layerId}".`)));
815 return;
816 }
817
818 if (layer.minzoom === minzoom && layer.maxzoom === maxzoom) return;
819
820 if (minzoom != null) {
821 layer.minzoom = minzoom;
822 }
823 if (maxzoom != null) {
824 layer.maxzoom = maxzoom;
825 }
826 this._updateLayer(layer);
827 }
828
829 setFilter(layerId: string, filter?: FilterSpecification | null, options: StyleSetterOptions = {}) {
830 this._checkLoaded();
831
832 const layer = this.getLayer(layerId);
833 if (!layer) {
834 this.fire(new ErrorEvent(new Error(`Cannot filter non-existing layer "${layerId}".`)));
835 return;
836 }
837
838 if (deepEqual(layer.filter, filter)) {
839 return;
840 }
841
842 if (filter === null || filter === undefined) {
843 layer.filter = undefined;
844 this._updateLayer(layer);
845 return;
846 }
847
848 if (this._validate(validateStyle.filter, `layers.${layer.id}.filter`, filter, null, options)) {
849 return;
850 }
851
852 layer.filter = clone(filter);
853 this._updateLayer(layer);
854 }
855
856 /**
857 * Get a layer's filter object
858 * @param {string} layer the layer to inspect
859 * @returns {*} the layer's filter, if any
860 */
861 getFilter(layer: string) {
862 return clone(this.getLayer(layer).filter);
863 }
864
865 setLayoutProperty(layerId: string, name: string, value: any, options: StyleSetterOptions = {}) {
866 this._checkLoaded();
867
868 const layer = this.getLayer(layerId);
869 if (!layer) {
870 this.fire(new ErrorEvent(new Error(`Cannot style non-existing layer "${layerId}".`)));
871 return;
872 }
873
874 if (deepEqual(layer.getLayoutProperty(name), value)) return;
875
876 layer.setLayoutProperty(name, value, options);
877 this._updateLayer(layer);
878 }
879
880 /**
881 * Get a layout property's value from a given layer
882 * @param {string} layerId the layer to inspect
883 * @param {string} name the name of the layout property
884 * @returns {*} the property value
885 */
886 getLayoutProperty(layerId: string, name: string) {
887 const layer = this.getLayer(layerId);
888 if (!layer) {
889 this.fire(new ErrorEvent(new Error(`Cannot get style of non-existing layer "${layerId}".`)));
890 return;
891 }
892
893 return layer.getLayoutProperty(name);
894 }
895
896 setPaintProperty(layerId: string, name: string, value: any, options: StyleSetterOptions = {}) {
897 this._checkLoaded();
898
899 const layer = this.getLayer(layerId);
900 if (!layer) {
901 this.fire(new ErrorEvent(new Error(`Cannot style non-existing layer "${layerId}".`)));
902 return;
903 }
904
905 if (deepEqual(layer.getPaintProperty(name), value)) return;
906
907 const requiresRelayout = layer.setPaintProperty(name, value, options);
908 if (requiresRelayout) {
909 this._updateLayer(layer);
910 }
911
912 this._changed = true;
913 this._updatedPaintProps[layerId] = true;
914 }
915
916 getPaintProperty(layer: string, name: string) {
917 return this.getLayer(layer).getPaintProperty(name);
918 }
919
920 setFeatureState(target: FeatureIdentifier, state: any) {
921 this._checkLoaded();
922 const sourceId = target.source;
923 const sourceLayer = target.sourceLayer;
924 const sourceCache = this.sourceCaches[sourceId];
925
926 if (sourceCache === undefined) {
927 this.fire(new ErrorEvent(new Error(`The source '${sourceId}' does not exist in the map's style.`)));
928 return;
929 }
930 const sourceType = sourceCache.getSource().type;
931 if (sourceType === 'geojson' && sourceLayer) {
932 this.fire(new ErrorEvent(new Error('GeoJSON sources cannot have a sourceLayer parameter.')));
933 return;
934 }
935 if (sourceType === 'vector' && !sourceLayer) {
936 this.fire(new ErrorEvent(new Error('The sourceLayer parameter must be provided for vector source types.')));
937 return;
938 }
939 if (target.id === undefined) {
940 this.fire(new ErrorEvent(new Error('The feature id parameter must be provided.')));
941 }
942
943 sourceCache.setFeatureState(sourceLayer, target.id, state);
944 }
945
946 removeFeatureState(target: FeatureIdentifier, key?: string) {
947 this._checkLoaded();
948 const sourceId = target.source;
949 const sourceCache = this.sourceCaches[sourceId];
950
951 if (sourceCache === undefined) {
952 this.fire(new ErrorEvent(new Error(`The source '${sourceId}' does not exist in the map's style.`)));
953 return;
954 }
955
956 const sourceType = sourceCache.getSource().type;
957 const sourceLayer = sourceType === 'vector' ? target.sourceLayer : undefined;
958
959 if (sourceType === 'vector' && !sourceLayer) {
960 this.fire(new ErrorEvent(new Error('The sourceLayer parameter must be provided for vector source types.')));
961 return;
962 }
963
964 if (key && (typeof target.id !== 'string' && typeof target.id !== 'number')) {
965 this.fire(new ErrorEvent(new Error('A feature id is required to remove its specific state property.')));
966 return;
967 }
968
969 sourceCache.removeFeatureState(sourceLayer, target.id, key);
970 }
971
972 getFeatureState(target: FeatureIdentifier) {
973 this._checkLoaded();
974 const sourceId = target.source;
975 const sourceLayer = target.sourceLayer;
976 const sourceCache = this.sourceCaches[sourceId];
977
978 if (sourceCache === undefined) {
979 this.fire(new ErrorEvent(new Error(`The source '${sourceId}' does not exist in the map's style.`)));
980 return;
981 }
982 const sourceType = sourceCache.getSource().type;
983 if (sourceType === 'vector' && !sourceLayer) {
984 this.fire(new ErrorEvent(new Error('The sourceLayer parameter must be provided for vector source types.')));
985 return;
986 }
987 if (target.id === undefined) {
988 this.fire(new ErrorEvent(new Error('The feature id parameter must be provided.')));
989 }
990
991 return sourceCache.getFeatureState(sourceLayer, target.id);
992 }
993
994 getTransition() {
995 return extend({duration: 300, delay: 0}, this.stylesheet && this.stylesheet.transition);
996 }
997
998 serialize(): StyleSpecification {
999 return filterObject({
1000 version: this.stylesheet.version,
1001 name: this.stylesheet.name,
1002 metadata: this.stylesheet.metadata,
1003 light: this.stylesheet.light,
1004 center: this.stylesheet.center,
1005 zoom: this.stylesheet.zoom,
1006 bearing: this.stylesheet.bearing,
1007 pitch: this.stylesheet.pitch,
1008 sprite: this.stylesheet.sprite,
1009 glyphs: this.stylesheet.glyphs,
1010 transition: this.stylesheet.transition,
1011 sources: mapObject(this.sourceCaches, (source) => source.serialize()),
1012 layers: this._serializeLayers(this._order)
1013 }, (value) => { return value !== undefined; });
1014 }
1015
1016 _updateLayer(layer: StyleLayer) {
1017 this._updatedLayers[layer.id] = true;
1018 if (layer.source && !this._updatedSources[layer.source] &&
1019 //Skip for raster layers (https://github.com/mapbox/mapbox-gl-js/issues/7865)
1020 this.sourceCaches[layer.source].getSource().type !== 'raster') {
1021 this._updatedSources[layer.source] = 'reload';
1022 this.sourceCaches[layer.source].pause();
1023 }
1024 this._changed = true;
1025 }
1026
1027 _flattenAndSortRenderedFeatures(sourceResults: Array<{ [key: string]: Array<{featureIndex: number; feature: MapGeoJSONFeature}> }>) {
1028 // Feature order is complicated.
1029 // The order between features in two 2D layers is always determined by layer order.
1030 // The order between features in two 3D layers is always determined by depth.
1031 // The order between a feature in a 2D layer and a 3D layer is tricky:
1032 // Most often layer order determines the feature order in this case. If
1033 // a line layer is above a extrusion layer the line feature will be rendered
1034 // above the extrusion. If the line layer is below the extrusion layer,
1035 // it will be rendered below it.
1036 //
1037 // There is a weird case though.
1038 // You have layers in this order: extrusion_layer_a, line_layer, extrusion_layer_b
1039 // Each layer has a feature that overlaps the other features.
1040 // The feature in extrusion_layer_a is closer than the feature in extrusion_layer_b so it is rendered above.
1041 // The feature in line_layer is rendered above extrusion_layer_a.
1042 // This means that that the line_layer feature is above the extrusion_layer_b feature despite
1043 // it being in an earlier layer.
1044
1045 const isLayer3D = layerId => this._layers[layerId].type === 'fill-extrusion';
1046
1047 const layerIndex = {};
1048 const features3D = [];
1049 for (let l = this._order.length - 1; l >= 0; l--) {
1050 const layerId = this._order[l];
1051 if (isLayer3D(layerId)) {
1052 layerIndex[layerId] = l;
1053 for (const sourceResult of sourceResults) {
1054 const layerFeatures = sourceResult[layerId];
1055 if (layerFeatures) {
1056 for (const featureWrapper of layerFeatures) {
1057 features3D.push(featureWrapper);
1058 }
1059 }
1060 }
1061 }
1062 }
1063
1064 features3D.sort((a, b) => {
1065 return b.intersectionZ - a.intersectionZ;
1066 });
1067
1068 const features = [];
1069 for (let l = this._order.length - 1; l >= 0; l--) {
1070 const layerId = this._order[l];
1071
1072 if (isLayer3D(layerId)) {
1073 // add all 3D features that are in or above the current layer
1074 for (let i = features3D.length - 1; i >= 0; i--) {
1075 const topmost3D = features3D[i].feature;
1076 if (layerIndex[topmost3D.layer.id] < l) break;
1077 features.push(topmost3D);
1078 features3D.pop();
1079 }
1080 } else {
1081 for (const sourceResult of sourceResults) {
1082 const layerFeatures = sourceResult[layerId];
1083 if (layerFeatures) {
1084 for (const featureWrapper of layerFeatures) {
1085 features.push(featureWrapper.feature);
1086 }
1087 }
1088 }
1089 }
1090 }
1091
1092 return features;
1093 }
1094
1095 queryRenderedFeatures(queryGeometry: any, params: any, transform: Transform) {
1096 if (params && params.filter) {
1097 this._validate(validateStyle.filter, 'queryRenderedFeatures.filter', params.filter, null, params);
1098 }
1099
1100 const includedSources = {};
1101 if (params && params.layers) {
1102 if (!Array.isArray(params.layers)) {
1103 this.fire(new ErrorEvent(new Error('parameters.layers must be an Array.')));
1104 return [];
1105 }
1106 for (const layerId of params.layers) {
1107 const layer = this._layers[layerId];
1108 if (!layer) {
1109 // this layer is not in the style.layers array
1110 this.fire(new ErrorEvent(new Error(`The layer '${layerId}' does not exist in the map's style and cannot be queried for features.`)));
1111 return [];
1112 }
1113 includedSources[layer.source] = true;
1114 }
1115 }
1116
1117 const sourceResults = [];
1118
1119 params.availableImages = this._availableImages;
1120
1121 for (const id in this.sourceCaches) {
1122 if (params.layers && !includedSources[id]) continue;
1123 sourceResults.push(
1124 queryRenderedFeatures(
1125 this.sourceCaches[id],
1126 this._layers,
1127 this._serializedLayers,
1128 queryGeometry,
1129 params,
1130 transform)
1131 );
1132 }
1133
1134 if (this.placement) {
1135 // If a placement has run, query against its CollisionIndex
1136 // for symbol results, and treat it as an extra source to merge
1137 sourceResults.push(
1138 queryRenderedSymbols(
1139 this._layers,
1140 this._serializedLayers,
1141 this.sourceCaches,
1142 queryGeometry,
1143 params,
1144 this.placement.collisionIndex,
1145 this.placement.retainedQueryData)
1146 );
1147 }
1148
1149 return this._flattenAndSortRenderedFeatures(sourceResults);
1150 }
1151
1152 querySourceFeatures(
1153 sourceID: string,
1154 params?: {
1155 sourceLayer: string;
1156 filter: Array<any>;
1157 validate?: boolean;
1158 }
1159 ) {
1160 if (params && params.filter) {
1161 this._validate(validateStyle.filter, 'querySourceFeatures.filter', params.filter, null, params);
1162 }
1163 const sourceCache = this.sourceCaches[sourceID];
1164 return sourceCache ? querySourceFeatures(sourceCache, params) : [];
1165 }
1166
1167 addSourceType(name: string, SourceType: SourceClass, callback: Callback<void>) {
1168 if (Style.getSourceType(name)) {
1169 return callback(new Error(`A source type called "${name}" already exists.`));
1170 }
1171
1172 Style.setSourceType(name, SourceType);
1173
1174 if (!SourceType.workerSourceURL) {
1175 return callback(null, null);
1176 }
1177
1178 this.dispatcher.broadcast('loadWorkerSource', {
1179 name,
1180 url: SourceType.workerSourceURL
1181 }, callback);
1182 }
1183
1184 getLight() {
1185 return this.light.getLight();
1186 }
1187
1188 setLight(lightOptions: LightSpecification, options: StyleSetterOptions = {}) {
1189 this._checkLoaded();
1190
1191 const light = this.light.getLight();
1192 let _update = false;
1193 for (const key in lightOptions) {
1194 if (!deepEqual(lightOptions[key], light[key])) {
1195 _update = true;
1196 break;
1197 }
1198 }
1199 if (!_update) return;
1200
1201 const parameters = {
1202 now: browser.now(),
1203 transition: extend({
1204 duration: 300,
1205 delay: 0
1206 }, this.stylesheet.transition)
1207 };
1208
1209 this.light.setLight(lightOptions, options);
1210 this.light.updateTransitions(parameters);
1211 }
1212
1213 _validate(validate: Validator, key: string, value: any, props: any, options: {
1214 validate?: boolean;
1215 } = {}) {
1216 if (options && options.validate === false) {
1217 return false;
1218 }
1219 return emitValidationErrors(this, validate.call(validateStyle, extend({
1220 key,
1221 style: this.serialize(),
1222 value,
1223 styleSpec
1224 }, props)));
1225 }
1226
1227 _remove() {
1228 if (this._request) {
1229 this._request.cancel();
1230 this._request = null;
1231 }
1232 if (this._spriteRequest) {
1233 this._spriteRequest.cancel();
1234 this._spriteRequest = null;
1235 }
1236 rtlTextPluginEvented.off('pluginStateChange', this._rtlTextPluginCallback);
1237 for (const layerId in this._layers) {
1238 const layer: StyleLayer = this._layers[layerId];
1239 layer.setEventedParent(null);
1240 }
1241 for (const id in this.sourceCaches) {
1242 const sourceCache = this.sourceCaches[id];
1243 sourceCache.setEventedParent(null);
1244 sourceCache.onRemove(this.map);
1245 }
1246 this.imageManager.setEventedParent(null);
1247 this.setEventedParent(null);
1248 this.dispatcher.remove();
1249 }
1250
1251 _clearSource(id: string) {
1252 this.sourceCaches[id].clearTiles();
1253 }
1254
1255 _reloadSource(id: string) {
1256 this.sourceCaches[id].resume();
1257 this.sourceCaches[id].reload();
1258 }
1259
1260 _updateSources(transform: Transform) {
1261 for (const id in this.sourceCaches) {
1262 this.sourceCaches[id].update(transform);
1263 }
1264 }
1265
1266 _generateCollisionBoxes() {
1267 for (const id in this.sourceCaches) {
1268 this._reloadSource(id);
1269 }
1270 }
1271
1272 _updatePlacement(transform: Transform, showCollisionBoxes: boolean, fadeDuration: number, crossSourceCollisions: boolean, forceFullPlacement: boolean = false) {
1273 let symbolBucketsChanged = false;
1274 let placementCommitted = false;
1275
1276 const layerTiles = {};
1277
1278 for (const layerID of this._order) {
1279 const styleLayer = this._layers[layerID];
1280 if (styleLayer.type !== 'symbol') continue;
1281
1282 if (!layerTiles[styleLayer.source]) {
1283 const sourceCache = this.sourceCaches[styleLayer.source];
1284 layerTiles[styleLayer.source] = sourceCache.getRenderableIds(true)
1285 .map((id) => sourceCache.getTileByID(id))
1286 .sort((a, b) => (b.tileID.overscaledZ - a.tileID.overscaledZ) || (a.tileID.isLessThan(b.tileID) ? -1 : 1));
1287 }
1288
1289 const layerBucketsChanged = this.crossTileSymbolIndex.addLayer(styleLayer, layerTiles[styleLayer.source], transform.center.lng);
1290 symbolBucketsChanged = symbolBucketsChanged || layerBucketsChanged;
1291 }
1292 this.crossTileSymbolIndex.pruneUnusedLayers(this._order);
1293
1294 // Anything that changes our "in progress" layer and tile indices requires us
1295 // to start over. When we start over, we do a full placement instead of incremental
1296 // to prevent starvation.
1297 // We need to restart placement to keep layer indices in sync.
1298 // Also force full placement when fadeDuration === 0 to ensure that newly loaded
1299 // tiles will fully display symbols in their first frame
1300 forceFullPlacement = forceFullPlacement || this._layerOrderChanged || fadeDuration === 0;
1301
1302 if (forceFullPlacement || !this.pauseablePlacement || (this.pauseablePlacement.isDone() && !this.placement.stillRecent(browser.now(), transform.zoom))) {
1303 this.pauseablePlacement = new PauseablePlacement(transform, this._order, forceFullPlacement, showCollisionBoxes, fadeDuration, crossSourceCollisions, this.placement);
1304 this._layerOrderChanged = false;
1305 }
1306
1307 if (this.pauseablePlacement.isDone()) {
1308 // the last placement finished running, but the next one hasn’t
1309 // started yet because of the `stillRecent` check immediately
1310 // above, so mark it stale to ensure that we request another
1311 // render frame
1312 this.placement.setStale();
1313 } else {
1314 this.pauseablePlacement.continuePlacement(this._order, this._layers, layerTiles);
1315
1316 if (this.pauseablePlacement.isDone()) {
1317 this.placement = this.pauseablePlacement.commit(browser.now());
1318 placementCommitted = true;
1319 }
1320
1321 if (symbolBucketsChanged) {
1322 // since the placement gets split over multiple frames it is possible
1323 // these buckets were processed before they were changed and so the
1324 // placement is already stale while it is in progress
1325 this.pauseablePlacement.placement.setStale();
1326 }
1327 }
1328
1329 if (placementCommitted || symbolBucketsChanged) {
1330 for (const layerID of this._order) {
1331 const styleLayer = this._layers[layerID];
1332 if (styleLayer.type !== 'symbol') continue;
1333 this.placement.updateLayerOpacities(styleLayer, layerTiles[styleLayer.source]);
1334 }
1335 }
1336
1337 // needsRender is false when we have just finished a placement that didn't change the visibility of any symbols
1338 const needsRerender = !this.pauseablePlacement.isDone() || this.placement.hasTransitions(browser.now());
1339 return needsRerender;
1340 }
1341
1342 _releaseSymbolFadeTiles() {
1343 for (const id in this.sourceCaches) {
1344 this.sourceCaches[id].releaseSymbolFadeTiles();
1345 }
1346 }
1347
1348 // Callbacks from web workers
1349
1350 getImages(
1351 mapId: string,
1352 params: {
1353 icons: Array<string>;
1354 source: string;
1355 tileID: OverscaledTileID;
1356 type: string;
1357 },
1358 callback: Callback<{[_: string]: StyleImage}>
1359 ) {
1360 this.imageManager.getImages(params.icons, callback);
1361
1362 // Apply queued image changes before setting the tile's dependencies so that the tile
1363 // is not reloaded unecessarily. Without this forced update the reload could happen in cases
1364 // like this one:
1365 // - icons contains "my-image"
1366 // - imageManager.getImages(...) triggers `onstyleimagemissing`
1367 // - the user adds "my-image" within the callback
1368 // - addImage adds "my-image" to this._changedImages
1369 // - the next frame triggers a reload of this tile even though it already has the latest version
1370 this._updateTilesForChangedImages();
1371
1372 const sourceCache = this.sourceCaches[params.source];
1373 if (sourceCache) {
1374 sourceCache.setDependencies(params.tileID.key, params.type, params.icons);
1375 }
1376 }
1377
1378 getGlyphs(
1379 mapId: string,
1380 params: {stacks: {[_: string]: Array<number>}},
1381 callback: Callback<{[_: string]: {[_: number]: StyleGlyph}}>
1382 ) {
1383 this.glyphManager.getGlyphs(params.stacks, callback);
1384 }
1385
1386 getResource(mapId: string, params: RequestParameters, callback: ResponseCallback<any>): Cancelable {
1387 return makeRequest(params, callback);
1388 }
1389}
1390
1391Style.getSourceType = getSourceType;
1392Style.setSourceType = setSourceType;
1393Style.registerForPluginStateChange = registerForPluginStateChange;
1394
1395export default Style;