import assert from 'assert'; import {Event, ErrorEvent, Evented} from '../util/evented'; import StyleLayer from './style_layer'; import createStyleLayer from './create_style_layer'; import loadSprite from './load_sprite'; import ImageManager from '../render/image_manager'; import GlyphManager from '../render/glyph_manager'; import Light from './light'; import LineAtlas from '../render/line_atlas'; import {pick, clone, extend, deepEqual, filterObject, mapObject} from '../util/util'; import {getJSON, getReferrer, makeRequest, ResourceType} from '../util/ajax'; import browser from '../util/browser'; import Dispatcher from '../util/dispatcher'; import {validateStyle, emitValidationErrors as _emitValidationErrors} from './validate_style'; import {getSourceType, setSourceType, Source} from '../source/source'; import type {SourceClass} from '../source/source'; import {queryRenderedFeatures, queryRenderedSymbols, querySourceFeatures} from '../source/query_features'; import SourceCache from '../source/source_cache'; import GeoJSONSource from '../source/geojson_source'; import styleSpec from '../style-spec/reference/latest'; import getWorkerPool from '../util/global_worker_pool'; import deref from '../style-spec/deref'; import emptyStyle from '../style-spec/empty'; import diffStyles, {operations as diffOperations} from '../style-spec/diff'; import { registerForPluginStateChange, evented as rtlTextPluginEvented, triggerPluginCompletionEvent } from '../source/rtl_text_plugin'; import PauseablePlacement from './pauseable_placement'; import ZoomHistory from './zoom_history'; import CrossTileSymbolIndex from '../symbol/cross_tile_symbol_index'; import {validateCustomStyleLayer} from './style_layer/custom_style_layer'; import type {MapGeoJSONFeature} from '../util/vectortile_to_geojson'; // We're skipping validation errors with the `source.canvas` identifier in order // to continue to allow canvas sources to be added at runtime/updated in // smart setStyle (see https://github.com/mapbox/mapbox-gl-js/pull/6424): const emitValidationErrors = (evented: Evented, errors?: ReadonlyArray<{ message: string; identifier?: string; }> | null) => _emitValidationErrors(evented, errors && errors.filter(error => error.identifier !== 'source.canvas')); import type Map from '../ui/map'; import type Transform from '../geo/transform'; import type {StyleImage} from './style_image'; import type {StyleGlyph} from './style_glyph'; import type {Callback} from '../types/callback'; import type EvaluationParameters from './evaluation_parameters'; import type {Placement} from '../symbol/placement'; import type {Cancelable} from '../types/cancelable'; import type {RequestParameters, ResponseCallback} from '../util/ajax'; import type { LayerSpecification, FilterSpecification, StyleSpecification, LightSpecification, SourceSpecification } from '../style-spec/types.g'; import type {CustomLayerInterface} from './style_layer/custom_style_layer'; import type {Validator} from './validate_style'; import type {OverscaledTileID} from '../source/tile_id'; const supportedDiffOperations = pick(diffOperations, [ 'addLayer', 'removeLayer', 'setPaintProperty', 'setLayoutProperty', 'setFilter', 'addSource', 'removeSource', 'setLayerZoomRange', 'setLight', 'setTransition', 'setGeoJSONSourceData' // 'setGlyphs', // 'setSprite', ]); const ignoredDiffOperations = pick(diffOperations, [ 'setCenter', 'setZoom', 'setBearing', 'setPitch' ]); const empty = emptyStyle() as StyleSpecification; export type FeatureIdentifier = { id?: string | number | undefined; source: string; sourceLayer?: string | undefined; }; export type StyleOptions = { validate?: boolean; localIdeographFontFamily?: string; }; export type StyleSetterOptions = { validate?: boolean; }; /** * @private */ class Style extends Evented { map: Map; stylesheet: StyleSpecification; dispatcher: Dispatcher; imageManager: ImageManager; glyphManager: GlyphManager; lineAtlas: LineAtlas; light: Light; _request: Cancelable; _spriteRequest: Cancelable; _layers: {[_: string]: StyleLayer}; _serializedLayers: {[_: string]: any}; _order: Array; sourceCaches: {[_: string]: SourceCache}; zoomHistory: ZoomHistory; _loaded: boolean; _rtlTextPluginCallback: (a: any) => any; _changed: boolean; _updatedSources: {[_: string]: 'clear' | 'reload'}; _updatedLayers: {[_: string]: true}; _removedLayers: {[_: string]: StyleLayer}; _changedImages: {[_: string]: true}; _updatedPaintProps: {[layer: string]: true}; _layerOrderChanged: boolean; _availableImages: Array; crossTileSymbolIndex: CrossTileSymbolIndex; pauseablePlacement: PauseablePlacement; placement: Placement; z: number; // exposed to allow stubbing by unit tests static getSourceType: typeof getSourceType; static setSourceType: typeof setSourceType; static registerForPluginStateChange: typeof registerForPluginStateChange; constructor(map: Map, options: StyleOptions = {}) { super(); this.map = map; this.dispatcher = new Dispatcher(getWorkerPool(), this); this.imageManager = new ImageManager(); this.imageManager.setEventedParent(this); this.glyphManager = new GlyphManager(map._requestManager, options.localIdeographFontFamily); this.lineAtlas = new LineAtlas(256, 512); this.crossTileSymbolIndex = new CrossTileSymbolIndex(); this._layers = {}; this._serializedLayers = {}; this._order = []; this.sourceCaches = {}; this.zoomHistory = new ZoomHistory(); this._loaded = false; this._availableImages = []; this._resetUpdates(); this.dispatcher.broadcast('setReferrer', getReferrer()); const self = this; this._rtlTextPluginCallback = Style.registerForPluginStateChange((event) => { const state = { pluginStatus: event.pluginStatus, pluginURL: event.pluginURL }; self.dispatcher.broadcast('syncRTLPluginState', state, (err, results) => { triggerPluginCompletionEvent(err); if (results) { const allComplete = results.every((elem) => elem); if (allComplete) { for (const id in self.sourceCaches) { self.sourceCaches[id].reload(); // Should be a no-op if the plugin loads before any tiles load } } } }); }); this.on('data', (event) => { if (event.dataType !== 'source' || event.sourceDataType !== 'metadata') { return; } const sourceCache = this.sourceCaches[event.sourceId]; if (!sourceCache) { return; } const source = sourceCache.getSource(); if (!source || !source.vectorLayerIds) { return; } for (const layerId in this._layers) { const layer = this._layers[layerId]; if (layer.source === source.id) { this._validateLayer(layer); } } }); } loadURL(url: string, options: { validate?: boolean; } = {}) { this.fire(new Event('dataloading', {dataType: 'style'})); const validate = typeof options.validate === 'boolean' ? options.validate : true; const request = this.map._requestManager.transformRequest(url, ResourceType.Style); this._request = getJSON(request, (error?: Error | null, json?: any | null) => { this._request = null; if (error) { this.fire(new ErrorEvent(error)); } else if (json) { this._load(json, validate); } }); } loadJSON(json: StyleSpecification, options: StyleSetterOptions = {}) { this.fire(new Event('dataloading', {dataType: 'style'})); this._request = browser.frame(() => { this._request = null; this._load(json, options.validate !== false); }); } loadEmpty() { this.fire(new Event('dataloading', {dataType: 'style'})); this._load(empty, false); } _load(json: StyleSpecification, validate: boolean) { if (validate && emitValidationErrors(this, validateStyle(json))) { return; } this._loaded = true; this.stylesheet = json; for (const id in json.sources) { this.addSource(id, json.sources[id], {validate: false}); } if (json.sprite) { this._loadSprite(json.sprite); } else { this.imageManager.setLoaded(true); } this.glyphManager.setURL(json.glyphs); const layers = deref(this.stylesheet.layers); this._order = layers.map((layer) => layer.id); this._layers = {}; this._serializedLayers = {}; for (let layer of layers) { layer = createStyleLayer(layer); layer.setEventedParent(this, {layer: {id: layer.id}}); this._layers[layer.id] = layer; this._serializedLayers[layer.id] = layer.serialize(); } this.dispatcher.broadcast('setLayers', this._serializeLayers(this._order)); this.light = new Light(this.stylesheet.light); this.fire(new Event('data', {dataType: 'style'})); this.fire(new Event('style.load')); } _loadSprite(url: string) { this._spriteRequest = loadSprite(url, this.map._requestManager, this.map.getPixelRatio(), (err, images) => { this._spriteRequest = null; if (err) { this.fire(new ErrorEvent(err)); } else if (images) { for (const id in images) { this.imageManager.addImage(id, images[id]); } } this.imageManager.setLoaded(true); this._availableImages = this.imageManager.listImages(); this.dispatcher.broadcast('setImages', this._availableImages); this.fire(new Event('data', {dataType: 'style'})); }); } _validateLayer(layer: StyleLayer) { const sourceCache = this.sourceCaches[layer.source]; if (!sourceCache) { return; } const sourceLayer = layer.sourceLayer; if (!sourceLayer) { return; } const source = sourceCache.getSource(); if (source.type === 'geojson' || (source.vectorLayerIds && source.vectorLayerIds.indexOf(sourceLayer) === -1)) { this.fire(new ErrorEvent(new Error( `Source layer "${sourceLayer}" ` + `does not exist on source "${source.id}" ` + `as specified by style layer "${layer.id}".` ))); } } loaded() { if (!this._loaded) return false; if (Object.keys(this._updatedSources).length) return false; for (const id in this.sourceCaches) if (!this.sourceCaches[id].loaded()) return false; if (!this.imageManager.isLoaded()) return false; return true; } _serializeLayers(ids: Array): Array { const serializedLayers = []; for (const id of ids) { const layer = this._layers[id]; if (layer.type !== 'custom') { serializedLayers.push(layer.serialize()); } } return serializedLayers; } hasTransitions() { if (this.light && this.light.hasTransition()) { return true; } for (const id in this.sourceCaches) { if (this.sourceCaches[id].hasTransition()) { return true; } } for (const id in this._layers) { if (this._layers[id].hasTransition()) { return true; } } return false; } _checkLoaded() { if (!this._loaded) { throw new Error('Style is not done loading.'); } } /** * Apply queued style updates in a batch and recalculate zoom-dependent paint properties. * @private */ update(parameters: EvaluationParameters) { if (!this._loaded) { return; } const changed = this._changed; if (this._changed) { const updatedIds = Object.keys(this._updatedLayers); const removedIds = Object.keys(this._removedLayers); if (updatedIds.length || removedIds.length) { this._updateWorkerLayers(updatedIds, removedIds); } for (const id in this._updatedSources) { const action = this._updatedSources[id]; assert(action === 'reload' || action === 'clear'); if (action === 'reload') { this._reloadSource(id); } else if (action === 'clear') { this._clearSource(id); } } this._updateTilesForChangedImages(); for (const id in this._updatedPaintProps) { this._layers[id].updateTransitions(parameters); } this.light.updateTransitions(parameters); this._resetUpdates(); } const sourcesUsedBefore = {}; for (const sourceId in this.sourceCaches) { const sourceCache = this.sourceCaches[sourceId]; sourcesUsedBefore[sourceId] = sourceCache.used; sourceCache.used = false; } for (const layerId of this._order) { const layer = this._layers[layerId]; layer.recalculate(parameters, this._availableImages); if (!layer.isHidden(parameters.zoom) && layer.source) { this.sourceCaches[layer.source].used = true; } } for (const sourceId in sourcesUsedBefore) { const sourceCache = this.sourceCaches[sourceId]; if (sourcesUsedBefore[sourceId] !== sourceCache.used) { sourceCache.fire(new Event('data', {sourceDataType: 'visibility', dataType:'source', sourceId})); } } this.light.recalculate(parameters); this.z = parameters.zoom; if (changed) { this.fire(new Event('data', {dataType: 'style'})); } } /* * Apply any queued image changes. */ _updateTilesForChangedImages() { const changedImages = Object.keys(this._changedImages); if (changedImages.length) { for (const name in this.sourceCaches) { this.sourceCaches[name].reloadTilesForDependencies(['icons', 'patterns'], changedImages); } this._changedImages = {}; } } _updateWorkerLayers(updatedIds: Array, removedIds: Array) { this.dispatcher.broadcast('updateLayers', { layers: this._serializeLayers(updatedIds), removedIds }); } _resetUpdates() { this._changed = false; this._updatedLayers = {}; this._removedLayers = {}; this._updatedSources = {}; this._updatedPaintProps = {}; this._changedImages = {}; } /** * Update this style's state to match the given style JSON, performing only * the necessary mutations. * * May throw an Error ('Unimplemented: METHOD') if the mapbox-gl-style-spec * diff algorithm produces an operation that is not supported. * * @returns {boolean} true if any changes were made; false otherwise * @private */ setState(nextState: StyleSpecification) { this._checkLoaded(); if (emitValidationErrors(this, validateStyle(nextState))) return false; nextState = clone(nextState); nextState.layers = deref(nextState.layers); const changes = diffStyles(this.serialize(), nextState) .filter(op => !(op.command in ignoredDiffOperations)); if (changes.length === 0) { return false; } const unimplementedOps = changes.filter(op => !(op.command in supportedDiffOperations)); if (unimplementedOps.length > 0) { throw new Error(`Unimplemented: ${unimplementedOps.map(op => op.command).join(', ')}.`); } changes.forEach((op) => { if (op.command === 'setTransition') { // `transition` is always read directly off of // `this.stylesheet`, which we update below return; } (this as any)[op.command].apply(this, op.args); }); this.stylesheet = nextState; return true; } addImage(id: string, image: StyleImage) { if (this.getImage(id)) { return this.fire(new ErrorEvent(new Error(`An image named "${id}" already exists.`))); } this.imageManager.addImage(id, image); this._afterImageUpdated(id); } updateImage(id: string, image: StyleImage) { this.imageManager.updateImage(id, image); } getImage(id: string): StyleImage { return this.imageManager.getImage(id); } removeImage(id: string) { if (!this.getImage(id)) { return this.fire(new ErrorEvent(new Error(`An image named "${id}" does not exist.`))); } this.imageManager.removeImage(id); this._afterImageUpdated(id); } _afterImageUpdated(id: string) { this._availableImages = this.imageManager.listImages(); this._changedImages[id] = true; this._changed = true; this.dispatcher.broadcast('setImages', this._availableImages); this.fire(new Event('data', {dataType: 'style'})); } listImages() { this._checkLoaded(); return this.imageManager.listImages(); } addSource(id: string, source: SourceSpecification, options: StyleSetterOptions = {}) { this._checkLoaded(); if (this.sourceCaches[id] !== undefined) { throw new Error(`Source "${id}" already exists.`); } if (!source.type) { throw new Error(`The type property must be defined, but only the following properties were given: ${Object.keys(source).join(', ')}.`); } const builtIns = ['vector', 'raster', 'geojson', 'video', 'image']; const shouldValidate = builtIns.indexOf(source.type) >= 0; if (shouldValidate && this._validate(validateStyle.source, `sources.${id}`, source, null, options)) return; if (this.map && this.map._collectResourceTiming) (source as any).collectResourceTiming = true; const sourceCache = this.sourceCaches[id] = new SourceCache(id, source, this.dispatcher); sourceCache.style = this; sourceCache.setEventedParent(this, () => ({ isSourceLoaded: this.loaded(), source: sourceCache.serialize(), sourceId: id })); sourceCache.onAdd(this.map); this._changed = true; } /** * Remove a source from this stylesheet, given its id. * @param {string} id id of the source to remove * @throws {Error} if no source is found with the given ID * @returns {Map} The {@link Map} object. */ removeSource(id: string) { this._checkLoaded(); if (this.sourceCaches[id] === undefined) { throw new Error('There is no source with this ID'); } for (const layerId in this._layers) { if (this._layers[layerId].source === id) { return this.fire(new ErrorEvent(new Error(`Source "${id}" cannot be removed while layer "${layerId}" is using it.`))); } } const sourceCache = this.sourceCaches[id]; delete this.sourceCaches[id]; delete this._updatedSources[id]; sourceCache.fire(new Event('data', {sourceDataType: 'metadata', dataType:'source', sourceId: id})); sourceCache.setEventedParent(null); sourceCache.onRemove(this.map); this._changed = true; } /** * Set the data of a GeoJSON source, given its id. * @param {string} id id of the source * @param {GeoJSON|string} data GeoJSON source */ setGeoJSONSourceData(id: string, data: GeoJSON.GeoJSON | string) { this._checkLoaded(); assert(this.sourceCaches[id] !== undefined, 'There is no source with this ID'); const geojsonSource: GeoJSONSource = (this.sourceCaches[id].getSource() as any); assert(geojsonSource.type === 'geojson'); geojsonSource.setData(data); this._changed = true; } /** * Get a source by id. * @param {string} id id of the desired source * @returns {Source | undefined} source */ getSource(id: string): Source | undefined { return this.sourceCaches[id] && this.sourceCaches[id].getSource(); } /** * Add a layer to the map style. The layer will be inserted before the layer with * ID `before`, or appended if `before` is omitted. * @param {Object | CustomLayerInterface} layerObject The style layer to add. * @param {string} [before] ID of an existing layer to insert before * @param {Object} options Style setter options. * @returns {Map} The {@link Map} object. */ addLayer(layerObject: LayerSpecification | CustomLayerInterface, before?: string, options: StyleSetterOptions = {}) { this._checkLoaded(); const id = layerObject.id; if (this.getLayer(id)) { this.fire(new ErrorEvent(new Error(`Layer "${id}" already exists on this map.`))); return; } let layer; if (layerObject.type === 'custom') { if (emitValidationErrors(this, validateCustomStyleLayer(layerObject))) return; layer = createStyleLayer(layerObject); } else { if (typeof (layerObject as any).source === 'object') { this.addSource(id, (layerObject as any).source); layerObject = clone(layerObject); layerObject = (extend(layerObject, {source: id}) as any); } // this layer is not in the style.layers array, so we pass an impossible array index if (this._validate(validateStyle.layer, `layers.${id}`, layerObject, {arrayIndex: -1}, options)) return; layer = createStyleLayer(layerObject); this._validateLayer(layer); layer.setEventedParent(this, {layer: {id}}); this._serializedLayers[layer.id] = layer.serialize(); } const index = before ? this._order.indexOf(before) : this._order.length; if (before && index === -1) { this.fire(new ErrorEvent(new Error(`Cannot add layer "${id}" before non-existing layer "${before}".`))); return; } this._order.splice(index, 0, id); this._layerOrderChanged = true; this._layers[id] = layer; if (this._removedLayers[id] && layer.source && layer.type !== 'custom') { // If, in the current batch, we have already removed this layer // and we are now re-adding it with a different `type`, then we // need to clear (rather than just reload) the underyling source's // tiles. Otherwise, tiles marked 'reloading' will have buckets / // buffers that are set up for the _previous_ version of this // layer, causing, e.g.: // https://github.com/mapbox/mapbox-gl-js/issues/3633 const removed = this._removedLayers[id]; delete this._removedLayers[id]; if (removed.type !== layer.type) { this._updatedSources[layer.source] = 'clear'; } else { this._updatedSources[layer.source] = 'reload'; this.sourceCaches[layer.source].pause(); } } this._updateLayer(layer); if (layer.onAdd) { layer.onAdd(this.map); } } /** * Moves a layer to a different z-position. The layer will be inserted before the layer with * ID `before`, or appended if `before` is omitted. * @param {string} id ID of the layer to move * @param {string} [before] ID of an existing layer to insert before */ moveLayer(id: string, before?: string) { this._checkLoaded(); this._changed = true; const layer = this._layers[id]; if (!layer) { this.fire(new ErrorEvent(new Error(`The layer '${id}' does not exist in the map's style and cannot be moved.`))); return; } if (id === before) { return; } const index = this._order.indexOf(id); this._order.splice(index, 1); const newIndex = before ? this._order.indexOf(before) : this._order.length; if (before && newIndex === -1) { this.fire(new ErrorEvent(new Error(`Cannot move layer "${id}" before non-existing layer "${before}".`))); return; } this._order.splice(newIndex, 0, id); this._layerOrderChanged = true; } /** * Remove the layer with the given id from the style. * * If no such layer exists, an `error` event is fired. * * @param {string} id id of the layer to remove * @fires error */ removeLayer(id: string) { this._checkLoaded(); const layer = this._layers[id]; if (!layer) { this.fire(new ErrorEvent(new Error(`Cannot remove non-existing layer "${id}".`))); return; } layer.setEventedParent(null); const index = this._order.indexOf(id); this._order.splice(index, 1); this._layerOrderChanged = true; this._changed = true; this._removedLayers[id] = layer; delete this._layers[id]; delete this._serializedLayers[id]; delete this._updatedLayers[id]; delete this._updatedPaintProps[id]; if (layer.onRemove) { layer.onRemove(this.map); } } /** * Return the style layer object with the given `id`. * * @param {string} id - id of the desired layer * @returns {?Object} a layer, if one with the given `id` exists */ getLayer(id: string): StyleLayer { return this._layers[id]; } /** * checks if a specific layer is present within the style. * * @param {string} id - id of the desired layer * @returns {boolean} a boolean specifying if the given layer is present */ hasLayer(id: string): boolean { return id in this._layers; } setLayerZoomRange(layerId: string, minzoom?: number | null, maxzoom?: number | null) { this._checkLoaded(); const layer = this.getLayer(layerId); if (!layer) { this.fire(new ErrorEvent(new Error(`Cannot set the zoom range of non-existing layer "${layerId}".`))); return; } if (layer.minzoom === minzoom && layer.maxzoom === maxzoom) return; if (minzoom != null) { layer.minzoom = minzoom; } if (maxzoom != null) { layer.maxzoom = maxzoom; } this._updateLayer(layer); } setFilter(layerId: string, filter?: FilterSpecification | null, options: StyleSetterOptions = {}) { this._checkLoaded(); const layer = this.getLayer(layerId); if (!layer) { this.fire(new ErrorEvent(new Error(`Cannot filter non-existing layer "${layerId}".`))); return; } if (deepEqual(layer.filter, filter)) { return; } if (filter === null || filter === undefined) { layer.filter = undefined; this._updateLayer(layer); return; } if (this._validate(validateStyle.filter, `layers.${layer.id}.filter`, filter, null, options)) { return; } layer.filter = clone(filter); this._updateLayer(layer); } /** * Get a layer's filter object * @param {string} layer the layer to inspect * @returns {*} the layer's filter, if any */ getFilter(layer: string) { return clone(this.getLayer(layer).filter); } setLayoutProperty(layerId: string, name: string, value: any, options: StyleSetterOptions = {}) { this._checkLoaded(); const layer = this.getLayer(layerId); if (!layer) { this.fire(new ErrorEvent(new Error(`Cannot style non-existing layer "${layerId}".`))); return; } if (deepEqual(layer.getLayoutProperty(name), value)) return; layer.setLayoutProperty(name, value, options); this._updateLayer(layer); } /** * Get a layout property's value from a given layer * @param {string} layerId the layer to inspect * @param {string} name the name of the layout property * @returns {*} the property value */ getLayoutProperty(layerId: string, name: string) { const layer = this.getLayer(layerId); if (!layer) { this.fire(new ErrorEvent(new Error(`Cannot get style of non-existing layer "${layerId}".`))); return; } return layer.getLayoutProperty(name); } setPaintProperty(layerId: string, name: string, value: any, options: StyleSetterOptions = {}) { this._checkLoaded(); const layer = this.getLayer(layerId); if (!layer) { this.fire(new ErrorEvent(new Error(`Cannot style non-existing layer "${layerId}".`))); return; } if (deepEqual(layer.getPaintProperty(name), value)) return; const requiresRelayout = layer.setPaintProperty(name, value, options); if (requiresRelayout) { this._updateLayer(layer); } this._changed = true; this._updatedPaintProps[layerId] = true; } getPaintProperty(layer: string, name: string) { return this.getLayer(layer).getPaintProperty(name); } setFeatureState(target: FeatureIdentifier, state: any) { this._checkLoaded(); const sourceId = target.source; const sourceLayer = target.sourceLayer; const sourceCache = this.sourceCaches[sourceId]; if (sourceCache === undefined) { this.fire(new ErrorEvent(new Error(`The source '${sourceId}' does not exist in the map's style.`))); return; } const sourceType = sourceCache.getSource().type; if (sourceType === 'geojson' && sourceLayer) { this.fire(new ErrorEvent(new Error('GeoJSON sources cannot have a sourceLayer parameter.'))); return; } if (sourceType === 'vector' && !sourceLayer) { this.fire(new ErrorEvent(new Error('The sourceLayer parameter must be provided for vector source types.'))); return; } if (target.id === undefined) { this.fire(new ErrorEvent(new Error('The feature id parameter must be provided.'))); } sourceCache.setFeatureState(sourceLayer, target.id, state); } removeFeatureState(target: FeatureIdentifier, key?: string) { this._checkLoaded(); const sourceId = target.source; const sourceCache = this.sourceCaches[sourceId]; if (sourceCache === undefined) { this.fire(new ErrorEvent(new Error(`The source '${sourceId}' does not exist in the map's style.`))); return; } const sourceType = sourceCache.getSource().type; const sourceLayer = sourceType === 'vector' ? target.sourceLayer : undefined; if (sourceType === 'vector' && !sourceLayer) { this.fire(new ErrorEvent(new Error('The sourceLayer parameter must be provided for vector source types.'))); return; } if (key && (typeof target.id !== 'string' && typeof target.id !== 'number')) { this.fire(new ErrorEvent(new Error('A feature id is required to remove its specific state property.'))); return; } sourceCache.removeFeatureState(sourceLayer, target.id, key); } getFeatureState(target: FeatureIdentifier) { this._checkLoaded(); const sourceId = target.source; const sourceLayer = target.sourceLayer; const sourceCache = this.sourceCaches[sourceId]; if (sourceCache === undefined) { this.fire(new ErrorEvent(new Error(`The source '${sourceId}' does not exist in the map's style.`))); return; } const sourceType = sourceCache.getSource().type; if (sourceType === 'vector' && !sourceLayer) { this.fire(new ErrorEvent(new Error('The sourceLayer parameter must be provided for vector source types.'))); return; } if (target.id === undefined) { this.fire(new ErrorEvent(new Error('The feature id parameter must be provided.'))); } return sourceCache.getFeatureState(sourceLayer, target.id); } getTransition() { return extend({duration: 300, delay: 0}, this.stylesheet && this.stylesheet.transition); } serialize(): StyleSpecification { return filterObject({ version: this.stylesheet.version, name: this.stylesheet.name, metadata: this.stylesheet.metadata, light: this.stylesheet.light, center: this.stylesheet.center, zoom: this.stylesheet.zoom, bearing: this.stylesheet.bearing, pitch: this.stylesheet.pitch, sprite: this.stylesheet.sprite, glyphs: this.stylesheet.glyphs, transition: this.stylesheet.transition, sources: mapObject(this.sourceCaches, (source) => source.serialize()), layers: this._serializeLayers(this._order) }, (value) => { return value !== undefined; }); } _updateLayer(layer: StyleLayer) { this._updatedLayers[layer.id] = true; if (layer.source && !this._updatedSources[layer.source] && //Skip for raster layers (https://github.com/mapbox/mapbox-gl-js/issues/7865) this.sourceCaches[layer.source].getSource().type !== 'raster') { this._updatedSources[layer.source] = 'reload'; this.sourceCaches[layer.source].pause(); } this._changed = true; } _flattenAndSortRenderedFeatures(sourceResults: Array<{ [key: string]: Array<{featureIndex: number; feature: MapGeoJSONFeature}> }>) { // Feature order is complicated. // The order between features in two 2D layers is always determined by layer order. // The order between features in two 3D layers is always determined by depth. // The order between a feature in a 2D layer and a 3D layer is tricky: // Most often layer order determines the feature order in this case. If // a line layer is above a extrusion layer the line feature will be rendered // above the extrusion. If the line layer is below the extrusion layer, // it will be rendered below it. // // There is a weird case though. // You have layers in this order: extrusion_layer_a, line_layer, extrusion_layer_b // Each layer has a feature that overlaps the other features. // The feature in extrusion_layer_a is closer than the feature in extrusion_layer_b so it is rendered above. // The feature in line_layer is rendered above extrusion_layer_a. // This means that that the line_layer feature is above the extrusion_layer_b feature despite // it being in an earlier layer. const isLayer3D = layerId => this._layers[layerId].type === 'fill-extrusion'; const layerIndex = {}; const features3D = []; for (let l = this._order.length - 1; l >= 0; l--) { const layerId = this._order[l]; if (isLayer3D(layerId)) { layerIndex[layerId] = l; for (const sourceResult of sourceResults) { const layerFeatures = sourceResult[layerId]; if (layerFeatures) { for (const featureWrapper of layerFeatures) { features3D.push(featureWrapper); } } } } } features3D.sort((a, b) => { return b.intersectionZ - a.intersectionZ; }); const features = []; for (let l = this._order.length - 1; l >= 0; l--) { const layerId = this._order[l]; if (isLayer3D(layerId)) { // add all 3D features that are in or above the current layer for (let i = features3D.length - 1; i >= 0; i--) { const topmost3D = features3D[i].feature; if (layerIndex[topmost3D.layer.id] < l) break; features.push(topmost3D); features3D.pop(); } } else { for (const sourceResult of sourceResults) { const layerFeatures = sourceResult[layerId]; if (layerFeatures) { for (const featureWrapper of layerFeatures) { features.push(featureWrapper.feature); } } } } } return features; } queryRenderedFeatures(queryGeometry: any, params: any, transform: Transform) { if (params && params.filter) { this._validate(validateStyle.filter, 'queryRenderedFeatures.filter', params.filter, null, params); } const includedSources = {}; if (params && params.layers) { if (!Array.isArray(params.layers)) { this.fire(new ErrorEvent(new Error('parameters.layers must be an Array.'))); return []; } for (const layerId of params.layers) { const layer = this._layers[layerId]; if (!layer) { // this layer is not in the style.layers array this.fire(new ErrorEvent(new Error(`The layer '${layerId}' does not exist in the map's style and cannot be queried for features.`))); return []; } includedSources[layer.source] = true; } } const sourceResults = []; params.availableImages = this._availableImages; for (const id in this.sourceCaches) { if (params.layers && !includedSources[id]) continue; sourceResults.push( queryRenderedFeatures( this.sourceCaches[id], this._layers, this._serializedLayers, queryGeometry, params, transform) ); } if (this.placement) { // If a placement has run, query against its CollisionIndex // for symbol results, and treat it as an extra source to merge sourceResults.push( queryRenderedSymbols( this._layers, this._serializedLayers, this.sourceCaches, queryGeometry, params, this.placement.collisionIndex, this.placement.retainedQueryData) ); } return this._flattenAndSortRenderedFeatures(sourceResults); } querySourceFeatures( sourceID: string, params?: { sourceLayer: string; filter: Array; validate?: boolean; } ) { if (params && params.filter) { this._validate(validateStyle.filter, 'querySourceFeatures.filter', params.filter, null, params); } const sourceCache = this.sourceCaches[sourceID]; return sourceCache ? querySourceFeatures(sourceCache, params) : []; } addSourceType(name: string, SourceType: SourceClass, callback: Callback) { if (Style.getSourceType(name)) { return callback(new Error(`A source type called "${name}" already exists.`)); } Style.setSourceType(name, SourceType); if (!SourceType.workerSourceURL) { return callback(null, null); } this.dispatcher.broadcast('loadWorkerSource', { name, url: SourceType.workerSourceURL }, callback); } getLight() { return this.light.getLight(); } setLight(lightOptions: LightSpecification, options: StyleSetterOptions = {}) { this._checkLoaded(); const light = this.light.getLight(); let _update = false; for (const key in lightOptions) { if (!deepEqual(lightOptions[key], light[key])) { _update = true; break; } } if (!_update) return; const parameters = { now: browser.now(), transition: extend({ duration: 300, delay: 0 }, this.stylesheet.transition) }; this.light.setLight(lightOptions, options); this.light.updateTransitions(parameters); } _validate(validate: Validator, key: string, value: any, props: any, options: { validate?: boolean; } = {}) { if (options && options.validate === false) { return false; } return emitValidationErrors(this, validate.call(validateStyle, extend({ key, style: this.serialize(), value, styleSpec }, props))); } _remove() { if (this._request) { this._request.cancel(); this._request = null; } if (this._spriteRequest) { this._spriteRequest.cancel(); this._spriteRequest = null; } rtlTextPluginEvented.off('pluginStateChange', this._rtlTextPluginCallback); for (const layerId in this._layers) { const layer: StyleLayer = this._layers[layerId]; layer.setEventedParent(null); } for (const id in this.sourceCaches) { const sourceCache = this.sourceCaches[id]; sourceCache.setEventedParent(null); sourceCache.onRemove(this.map); } this.imageManager.setEventedParent(null); this.setEventedParent(null); this.dispatcher.remove(); } _clearSource(id: string) { this.sourceCaches[id].clearTiles(); } _reloadSource(id: string) { this.sourceCaches[id].resume(); this.sourceCaches[id].reload(); } _updateSources(transform: Transform) { for (const id in this.sourceCaches) { this.sourceCaches[id].update(transform); } } _generateCollisionBoxes() { for (const id in this.sourceCaches) { this._reloadSource(id); } } _updatePlacement(transform: Transform, showCollisionBoxes: boolean, fadeDuration: number, crossSourceCollisions: boolean, forceFullPlacement: boolean = false) { let symbolBucketsChanged = false; let placementCommitted = false; const layerTiles = {}; for (const layerID of this._order) { const styleLayer = this._layers[layerID]; if (styleLayer.type !== 'symbol') continue; if (!layerTiles[styleLayer.source]) { const sourceCache = this.sourceCaches[styleLayer.source]; layerTiles[styleLayer.source] = sourceCache.getRenderableIds(true) .map((id) => sourceCache.getTileByID(id)) .sort((a, b) => (b.tileID.overscaledZ - a.tileID.overscaledZ) || (a.tileID.isLessThan(b.tileID) ? -1 : 1)); } const layerBucketsChanged = this.crossTileSymbolIndex.addLayer(styleLayer, layerTiles[styleLayer.source], transform.center.lng); symbolBucketsChanged = symbolBucketsChanged || layerBucketsChanged; } this.crossTileSymbolIndex.pruneUnusedLayers(this._order); // Anything that changes our "in progress" layer and tile indices requires us // to start over. When we start over, we do a full placement instead of incremental // to prevent starvation. // We need to restart placement to keep layer indices in sync. // Also force full placement when fadeDuration === 0 to ensure that newly loaded // tiles will fully display symbols in their first frame forceFullPlacement = forceFullPlacement || this._layerOrderChanged || fadeDuration === 0; if (forceFullPlacement || !this.pauseablePlacement || (this.pauseablePlacement.isDone() && !this.placement.stillRecent(browser.now(), transform.zoom))) { this.pauseablePlacement = new PauseablePlacement(transform, this._order, forceFullPlacement, showCollisionBoxes, fadeDuration, crossSourceCollisions, this.placement); this._layerOrderChanged = false; } if (this.pauseablePlacement.isDone()) { // the last placement finished running, but the next one hasn’t // started yet because of the `stillRecent` check immediately // above, so mark it stale to ensure that we request another // render frame this.placement.setStale(); } else { this.pauseablePlacement.continuePlacement(this._order, this._layers, layerTiles); if (this.pauseablePlacement.isDone()) { this.placement = this.pauseablePlacement.commit(browser.now()); placementCommitted = true; } if (symbolBucketsChanged) { // since the placement gets split over multiple frames it is possible // these buckets were processed before they were changed and so the // placement is already stale while it is in progress this.pauseablePlacement.placement.setStale(); } } if (placementCommitted || symbolBucketsChanged) { for (const layerID of this._order) { const styleLayer = this._layers[layerID]; if (styleLayer.type !== 'symbol') continue; this.placement.updateLayerOpacities(styleLayer, layerTiles[styleLayer.source]); } } // needsRender is false when we have just finished a placement that didn't change the visibility of any symbols const needsRerender = !this.pauseablePlacement.isDone() || this.placement.hasTransitions(browser.now()); return needsRerender; } _releaseSymbolFadeTiles() { for (const id in this.sourceCaches) { this.sourceCaches[id].releaseSymbolFadeTiles(); } } // Callbacks from web workers getImages( mapId: string, params: { icons: Array; source: string; tileID: OverscaledTileID; type: string; }, callback: Callback<{[_: string]: StyleImage}> ) { this.imageManager.getImages(params.icons, callback); // Apply queued image changes before setting the tile's dependencies so that the tile // is not reloaded unecessarily. Without this forced update the reload could happen in cases // like this one: // - icons contains "my-image" // - imageManager.getImages(...) triggers `onstyleimagemissing` // - the user adds "my-image" within the callback // - addImage adds "my-image" to this._changedImages // - the next frame triggers a reload of this tile even though it already has the latest version this._updateTilesForChangedImages(); const sourceCache = this.sourceCaches[params.source]; if (sourceCache) { sourceCache.setDependencies(params.tileID.key, params.type, params.icons); } } getGlyphs( mapId: string, params: {stacks: {[_: string]: Array}}, callback: Callback<{[_: string]: {[_: number]: StyleGlyph}}> ) { this.glyphManager.getGlyphs(params.stacks, callback); } getResource(mapId: string, params: RequestParameters, callback: ResponseCallback): Cancelable { return makeRequest(params, callback); } } Style.getSourceType = getSourceType; Style.setSourceType = setSourceType; Style.registerForPluginStateChange = registerForPluginStateChange; export default Style;