UNPKG

mapbox-gl

Version:
1,426 lines (1,190 loc) 109 kB
// @flow import assert from 'assert'; import {Event, ErrorEvent, Evented} from '../util/evented.js'; import StyleLayer from './style_layer.js'; import StyleChanges from './style_changes.js'; import createStyleLayer from './create_style_layer.js'; import loadSprite from './load_sprite.js'; import ImageManager from '../render/image_manager.js'; import GlyphManager, {LocalGlyphMode} from '../render/glyph_manager.js'; import Light from './light.js'; import Terrain, {DrapeRenderMode} from './terrain.js'; import Fog from './fog.js'; import {pick, clone, extend, deepEqual, filterObject, cartesianPositionToSpherical, warnOnce} from '../util/util.js'; import {getJSON, getReferrer, makeRequest, ResourceType} from '../util/ajax.js'; import {isMapboxURL} from '../util/mapbox.js'; import browser from '../util/browser.js'; import Dispatcher from '../util/dispatcher.js'; import Lights from '../../3d-style/style/lights.js'; import {properties as ambientProps} from '../../3d-style/style/ambient_light_properties.js'; import {properties as directionalProps} from '../../3d-style/style/directional_light_properties.js'; import {createExpression} from '../style-spec/expression/index.js'; import { validateStyle, validateSource, validateLayer, validateFilter, validateTerrain, validateLights, validateModel, emitValidationErrors as _emitValidationErrors } from './validate_style.js'; import {QueryGeometry} from '../style/query_geometry.js'; import { create as createSource, getType as getSourceType, setType as setSourceType, } from '../source/source.js'; import {queryRenderedFeatures, queryRenderedSymbols, querySourceFeatures} from '../source/query_features.js'; import SourceCache from '../source/source_cache.js'; import BuildingIndex from '../source/building_index.js'; import GeoJSONSource from '../source/geojson_source.js'; import styleSpec from '../style-spec/reference/latest.js'; import getWorkerPool from '../util/global_worker_pool.js'; import deref from '../style-spec/deref.js'; import emptyStyle from '../style-spec/empty.js'; import diffStyles, {operations as diffOperations} from '../style-spec/diff.js'; import { registerForPluginStateChange, evented as rtlTextPluginEvented, triggerPluginCompletionEvent } from '../source/rtl_text_plugin.js'; import PauseablePlacement from './pauseable_placement.js'; import CrossTileSymbolIndex from '../symbol/cross_tile_symbol_index.js'; import {validateCustomStyleLayer} from './style_layer/custom_style_layer.js'; import {isFQID, makeFQID, getNameFromFQID, getScopeFromFQID} from '../util/fqid.js'; import {shadowDirectionFromProperties} from '../../3d-style/render/shadow_renderer.js'; import ModelManager from '../../3d-style/render/model_manager.js'; import {DEFAULT_MAX_ZOOM, DEFAULT_MIN_ZOOM} from '../geo/transform.js'; // 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: ?ValidationErrors) => _emitValidationErrors(evented, errors && errors.filter(error => error.identifier !== 'source.canvas')); import type {LightProps as Ambient} from '../../3d-style/style/ambient_light_properties.js'; import type {LightProps as Directional} from '../../3d-style/style/directional_light_properties.js'; import type {Vec3} from 'gl-matrix'; import type {default as MapboxMap} from '../ui/map.js'; import type Transform from '../geo/transform.js'; import type {StyleImage} from './style_image.js'; import type {StyleGlyph} from './style_glyph.js'; import type {Callback} from '../types/callback.js'; import EvaluationParameters from './evaluation_parameters.js'; import type {Placement} from '../symbol/placement.js'; import type {Cancelable} from '../types/cancelable.js'; import type {RequestParameters, ResponseCallback} from '../util/ajax.js'; import type {GeoJSON} from '@mapbox/geojson-types'; import type { LayerSpecification, FilterSpecification, StyleSpecification, ImportSpecification, LightSpecification, SourceSpecification, TerrainSpecification, LightsSpecification, FlatLightSpecification, FogSpecification, ProjectionSpecification, TransitionSpecification, PropertyValueSpecification, ConfigSpecification, SchemaSpecification, CameraSpecification } from '../style-spec/types.js'; import type {CustomLayerInterface} from './style_layer/custom_style_layer.js'; import type {Validator, ValidationErrors} from './validate_style.js'; import type {OverscaledTileID} from '../source/tile_id.js'; import type {QueryResult} from '../data/feature_index.js'; import type {QueryFeature} from '../util/vectortile_to_geojson.js'; import type {FeatureStates} from '../source/source_state.js'; import type {PointLike} from '@mapbox/point-geometry'; import type {Source, SourceClass} from '../source/source.js'; import type {TransitionParameters, ConfigOptions} from './properties.js'; import type {QueryRenderedFeaturesParams} from '../source/query_features.js'; const supportedDiffOperations = pick(diffOperations, [ 'addLayer', 'removeLayer', 'setLights', 'setPaintProperty', 'setLayoutProperty', 'setSlot', 'setFilter', 'addSource', 'removeSource', 'setLayerZoomRange', 'setLight', 'setTransition', 'setGeoJSONSourceData', 'setTerrain', 'setFog', 'setProjection', 'setCamera', 'addImport', 'removeImport', 'setImportUrl', 'setImportData', 'setImportConfig', // 'setGlyphs', // 'setSprite', ]); const ignoredDiffOperations = pick(diffOperations, [ 'setCenter', 'setZoom', 'setBearing', 'setPitch' ]); const empty = emptyStyle(); export type StyleOptions = { validate?: boolean, localFontFamily?: ?string, localIdeographFontFamily?: string, dispatcher?: Dispatcher, imageManager?: ImageManager, glyphManager?: GlyphManager, modelManager?: ModelManager, styleChanges?: StyleChanges, configOptions?: ConfigOptions, scope?: string, importDepth?: number, importsCache?: Map<string, StyleSpecification>, resolvedImports?: Set<string>, config?: ?ConfigSpecification, configDependentLayers?: Set<string>; }; export type StyleSetterOptions = { validate?: boolean; isInitialLoad?: boolean; }; export type Fragment = {| id: string, style: Style, config?: ?ConfigSpecification |}; const MAX_IMPORT_DEPTH = 5; const defaultTransition = {duration: 300, delay: 0}; // Symbols are draped only on native and for certain cases only const drapedLayers = new Set(['fill', 'line', 'background', 'hillshade', 'raster']); /** * @private */ class Style extends Evented { map: MapboxMap; stylesheet: StyleSpecification; dispatcher: Dispatcher; imageManager: ImageManager; glyphManager: GlyphManager; modelManager: ModelManager; ambientLight: ?Lights<Ambient>; directionalLight: ?Lights<Directional>; light: Light; terrain: ?Terrain; disableElevatedTerrain: ?boolean; fog: ?Fog; camera: CameraSpecification; transition: TransitionSpecification; projection: ProjectionSpecification; scope: string; fragments: Array<Fragment>; importDepth: number; // Shared cache of imported stylesheets importsCache: Map<string, StyleSpecification>; // Keeps track of ancestors' Style URLs. resolvedImports: Set<string>; options: ConfigOptions; // Merged layers and sources _mergedOrder: Array<string>; _mergedLayers: {[_: string]: StyleLayer}; _mergedSourceCaches: {[_: string]: SourceCache}; _mergedOtherSourceCaches: {[_: string]: SourceCache}; _mergedSymbolSourceCaches: {[_: string]: SourceCache}; _request: ?Cancelable; _spriteRequest: ?Cancelable; _layers: {[_: string]: StyleLayer}; _serializedLayers: {[_: string]: Object}; _order: Array<string>; _drapedFirstOrder: Array<string>; _sourceCaches: {[_: string]: SourceCache}; _otherSourceCaches: {[_: string]: SourceCache}; _symbolSourceCaches: {[_: string]: SourceCache}; _loaded: boolean; _shouldPrecompile: boolean; _precompileDone: boolean; _rtlTextPluginCallback: Function; _changes: StyleChanges; _optionsChanged: boolean; _layerOrderChanged: boolean; _availableImages: Array<string>; _markersNeedUpdate: boolean; _brightness: ?number; _configDependentLayers: Set<string>; _config: ?ConfigSpecification; _buildingIndex: BuildingIndex; _transition: TransitionSpecification; crossTileSymbolIndex: CrossTileSymbolIndex; pauseablePlacement: PauseablePlacement; placement: Placement; z: number; _has3DLayers: boolean; _hasCircleLayers: boolean; _hasSymbolLayers: boolean; // exposed to allow stubbing by unit tests static getSourceType: typeof getSourceType; static setSourceType: typeof setSourceType; static registerForPluginStateChange: typeof registerForPluginStateChange; constructor(map: MapboxMap, options: StyleOptions = {}) { super(); this.map = map; // Empty string indicates the root Style scope. this.scope = options.scope || ''; this.fragments = []; this.importDepth = options.importDepth || 0; this.importsCache = options.importsCache || new Map(); this.resolvedImports = options.resolvedImports || new Set(); this.transition = extend({}, defaultTransition); this._buildingIndex = new BuildingIndex(this); this.crossTileSymbolIndex = new CrossTileSymbolIndex(); this._mergedOrder = []; this._drapedFirstOrder = []; this._mergedLayers = {}; this._mergedSourceCaches = {}; this._mergedOtherSourceCaches = {}; this._mergedSymbolSourceCaches = {}; this._has3DLayers = false; this._hasCircleLayers = false; this._hasSymbolLayers = false; this._changes = options.styleChanges || new StyleChanges(); if (options.dispatcher) { this.dispatcher = options.dispatcher; } else { this.dispatcher = new Dispatcher(getWorkerPool(), this); } if (options.imageManager) { this.imageManager = options.imageManager; } else { this.imageManager = new ImageManager(); this.imageManager.setEventedParent(this); } this.imageManager.createScope(this.scope); if (options.glyphManager) { this.glyphManager = options.glyphManager; } else { this.glyphManager = new GlyphManager(map._requestManager, options.localFontFamily ? LocalGlyphMode.all : (options.localIdeographFontFamily ? LocalGlyphMode.ideographs : LocalGlyphMode.none), options.localFontFamily || options.localIdeographFontFamily); } if (options.modelManager) { this.modelManager = options.modelManager; } else { this.modelManager = new ModelManager(map._requestManager); this.modelManager.setEventedParent(this); } this._layers = {}; this._serializedLayers = {}; this._sourceCaches = {}; this._otherSourceCaches = {}; this._symbolSourceCaches = {}; this._loaded = false; this._precompileDone = false; this._shouldPrecompile = false; this._availableImages = []; this._order = []; this._markersNeedUpdate = false; this.options = options.configOptions ? options.configOptions : new Map(); this._configDependentLayers = options.configDependentLayers ? options.configDependentLayers : new Set(); this._config = options.config; 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) { const sourceCache = self._sourceCaches[id]; const sourceCacheType = sourceCache.getSource().type; if (sourceCacheType === 'vector' || sourceCacheType === 'geojson') { sourceCache.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 source = this.getOwnSource(event.sourceId); 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, accessToken?: string } = {}): void { this.fire(new Event('dataloading', {dataType: 'style'})); const validate = typeof options.validate === 'boolean' ? options.validate : !isMapboxURL(url); url = this.map._requestManager.normalizeStyleURL(url, options.accessToken); this.resolvedImports.add(url); const cachedImport = this.importsCache.get(url); if (cachedImport) return this._load(cachedImport, validate); const request = this.map._requestManager.transformRequest(url, ResourceType.Style); this._request = getJSON(request, (error: ?Error, json: ?Object) => { this._request = null; if (error) { this.fire(new ErrorEvent(error)); } else if (json) { this.importsCache.set(url, json); return this._load(json, validate); } }); } loadJSON(json: StyleSpecification, options: StyleSetterOptions = {}): void { 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); } _loadImports(imports: Array<ImportSpecification>, validate: boolean): Promise<any> { // We take the root style into account when calculating the import depth. if (this.importDepth >= MAX_IMPORT_DEPTH - 1) { warnOnce(`Style doesn't support nesting deeper than ${MAX_IMPORT_DEPTH}`); return Promise.resolve(); } const waitForStyles = []; for (const importSpec of imports) { const style = this._createFragmentStyle(importSpec); // Merge everything and update layers after the import style is settled. const waitForStyle = new Promise((resolve) => { style.once('style.import.load', resolve); style.once('error', resolve); }) .then(() => this.mergeAll()); waitForStyles.push(waitForStyle); // Load empty style if one of the ancestors was already // instantiated from this URL to avoid recursion. if (this.resolvedImports.has(importSpec.url)) { style.loadEmpty(); continue; } // Use previously cached style JSON if the import data is not set. const json = importSpec.data || this.importsCache.get(importSpec.url); if (json) { style.loadJSON(json, {validate}); } else if (importSpec.url) { style.loadURL(importSpec.url, {validate}); } else { style.loadEmpty(); } const fragment = { style, id: importSpec.id, config: importSpec.config }; this.fragments.push(fragment); } // $FlowFixMe[method-unbinding] return Promise.allSettled(waitForStyles); } _createFragmentStyle(importSpec: ImportSpecification): Style { const scope = this.scope ? makeFQID(importSpec.id, this.scope) : importSpec.id; const style = new Style(this.map, { scope, styleChanges: this._changes, importDepth: this.importDepth + 1, importsCache: this.importsCache, // Clone resolvedImports so it's not being shared between siblings resolvedImports: new Set(this.resolvedImports), // Use shared Dispatcher and assets Managers between Styles dispatcher: this.dispatcher, imageManager: this.imageManager, glyphManager: this.glyphManager, modelManager: this.modelManager, config: importSpec.config, configOptions: this.options, configDependentLayers: this._configDependentLayers }); // Bubble all events fired by the style to the map. style.setEventedParent(this.map, {style}); return style; } _reloadImports() { this.mergeAll(); this._updateMapProjection(); this.updateConfigDependencies(); this.map._triggerCameraUpdate(this.camera); this.dispatcher.broadcast('setLayers', { layers: this._serializeLayers(this._order), scope: this.scope, options: this.options }); const isRootStyle = this.isRootStyle(); this._shouldPrecompile = isRootStyle; this.fire(new Event(isRootStyle ? 'style.load' : 'style.import.load')); } _load(json: StyleSpecification, validate: boolean) { const schema = json.schema; // This style was loaded as a root style, but it is marked as a fragment and/or has a schema. We instead load // it as an import with the well-known ID "basemap" to make sure that we don't expose the internals. if (this.isRootStyle() && (json.fragment || (schema && json.fragment !== false))) { const basemap = {id: 'basemap', data: json, url: ''}; const style = extend({}, empty, {imports: [basemap]}); this._load(style, validate); return; } this.setConfig(this._config, schema); if (validate && emitValidationErrors(this, validateStyle(json))) { return; } this._loaded = true; this.stylesheet = clone(json); for (const id in json.sources) { this.addSource(id, json.sources[id], {validate: false, isInitialLoad: true}); } if (json.sprite) { this._loadSprite(json.sprite); } else { this.imageManager.setLoaded(true, this.scope); this.dispatcher.broadcast('spriteLoaded', {scope: this.scope, isLoaded: true}); } this.glyphManager.setURL(json.glyphs, this.scope); const layers: Array<LayerSpecification> = deref(this.stylesheet.layers); this._order = layers.map((layer) => layer.id); if (this.stylesheet.light) { warnOnce('The `light` root property is deprecated, prefer using `lights` with `flat` light type instead.'); } if (this.stylesheet.lights) { if (this.stylesheet.lights.length === 1 && this.stylesheet.lights[0].type === "flat") { const flatLight: FlatLightSpecification = this.stylesheet.lights[0]; this.light = new Light(flatLight.properties, flatLight.id); } else { this.setLights(this.stylesheet.lights); } } if (!this.light) { this.light = new Light(this.stylesheet.light); } this._layers = {}; this._serializedLayers = {}; for (const layer of layers) { const styleLayer = createStyleLayer(layer, this.scope, this.options); if (styleLayer.isConfigDependent) this._configDependentLayers.add(styleLayer.fqid); styleLayer.setEventedParent(this, {layer: {id: styleLayer.id}}); this._layers[styleLayer.id] = styleLayer; this._serializedLayers[styleLayer.id] = styleLayer.serialize(); const sourceCache = this.getOwnLayerSourceCache(styleLayer); const shadowsEnabled = !!this.directionalLight && this.directionalLight.shadowsEnabled(); if (sourceCache && styleLayer.canCastShadows() && shadowsEnabled) { sourceCache.castsShadows = true; } } if (this.stylesheet.models) { this.modelManager.addModels(this.stylesheet.models, this.scope); } const terrain = this.stylesheet.terrain; if (terrain) { // This workaround disables terrain and hillshade // if there is noise in the Canvas2D operations used for image decoding. if (this.disableElevatedTerrain === undefined) this.disableElevatedTerrain = browser.hasCanvasFingerprintNoise(); if (this.disableElevatedTerrain) { warnOnce('Terrain and hillshade are disabled because of Canvas2D limitations when fingerprinting protection is enabled (e.g. in private browsing mode).'); } else if (!this.terrainSetForDrapingOnly()) { this._createTerrain(terrain, DrapeRenderMode.elevated); } } if (this.stylesheet.fog) { this._createFog(this.stylesheet.fog); } if (this.stylesheet.transition) { this.setTransition(this.stylesheet.transition); } this.fire(new Event('data', {dataType: 'style'})); if (json.imports) { this._loadImports(json.imports, validate).then(() => this._reloadImports()); } else { this._reloadImports(); } } isRootStyle(): boolean { return this.importDepth === 0; } mergeAll() { let light; let ambientLight; let directionalLight; let terrain; let fog; let projection; let transition; let camera; // Reset terrain that might have been set by a previous merge if (this.terrain && this.terrain.scope !== this.scope) { delete this.terrain; } this.forEachFragmentStyle((style: Style) => { if (!style.stylesheet) return; if (style.light != null) light = style.light; if (style.stylesheet.lights) { for (const light of style.stylesheet.lights) { if (light.type === 'ambient' && style.ambientLight != null) ambientLight = style.ambientLight; if (light.type === 'directional' && style.directionalLight != null) directionalLight = style.directionalLight; } } terrain = this._prioritizeTerrain( terrain, style.terrain, style.stylesheet.terrain, ); if (style.stylesheet.fog && style.fog != null) fog = style.fog; if (style.stylesheet.camera != null) camera = style.stylesheet.camera; if (style.stylesheet.projection != null) projection = style.stylesheet.projection; if (style.stylesheet.transition != null) transition = style.stylesheet.transition; }); // $FlowFixMe[incompatible-type] this.light = light; this.ambientLight = ambientLight; this.directionalLight = directionalLight; this.fog = fog; if (terrain === null) { delete this.terrain; } else { this.terrain = terrain; } // Use perspective camera as a fallback if no camera is specified this.camera = camera || {'camera-projection': 'perspective'}; this.projection = projection || {name: 'mercator'}; this.transition = extend({}, defaultTransition, transition); this.mergeSources(); this.mergeLayers(); } forEachFragmentStyle(fn: (style: Style) => void) { const traverse = (style: Style) => { for (const fragment of style.fragments) { traverse(fragment.style); } fn(style); }; traverse(this); } _prioritizeTerrain(prevTerrain: ?Terrain, nextTerrain: ?Terrain, nextTerrainSpec: ?TerrainSpecification): ?Terrain { // Given the previous and next terrain during imports merging, in order of priority, we select: // 1. null, if the next terrain is explicitly disabled and we are not using the globe // 2. next terrain if it is not null // 3. previous terrain const prevIsDeffered = prevTerrain && prevTerrain.drapeRenderMode === DrapeRenderMode.deferred; const nextIsDeffered = nextTerrain && nextTerrain.drapeRenderMode === DrapeRenderMode.deferred; // Disable terrain if it was explicitly set to null and we are not using globe if (nextTerrainSpec === null) { // First, check if the terrain is deferred // If so, we are using the globe and should keep the terrain if (nextIsDeffered) return nextTerrain; if (prevIsDeffered) return prevTerrain; return null; } // Use next terrain if there is no previous terrain or if it is deferred if (nextTerrain != null) { const nextIsElevated = nextTerrain && nextTerrain.drapeRenderMode === DrapeRenderMode.elevated; if (!prevTerrain || prevIsDeffered || nextIsElevated) return nextTerrain; } return prevTerrain; } mergeTerrain() { let terrain; // Reset terrain that might have been set by a previous merge if (this.terrain && this.terrain.scope !== this.scope) { delete this.terrain; } this.forEachFragmentStyle((style: Style) => { terrain = this._prioritizeTerrain( terrain, style.terrain, style.stylesheet.terrain, ); }); if (terrain === null) { delete this.terrain; } else { this.terrain = terrain; } } mergeProjection() { let projection; this.forEachFragmentStyle((style: Style) => { if (style.stylesheet.projection != null) projection = style.stylesheet.projection; }); this.projection = projection || {name: 'mercator'}; } mergeSources() { const mergedSourceCaches = {}; const mergedOtherSourceCaches = {}; const mergedSymbolSourceCaches = {}; this.forEachFragmentStyle((style: Style) => { for (const id in style._sourceCaches) { const fqid = makeFQID(id, style.scope); mergedSourceCaches[fqid] = style._sourceCaches[id]; } for (const id in style._otherSourceCaches) { const fqid = makeFQID(id, style.scope); mergedOtherSourceCaches[fqid] = style._otherSourceCaches[id]; } for (const id in style._symbolSourceCaches) { const fqid = makeFQID(id, style.scope); mergedSymbolSourceCaches[fqid] = style._symbolSourceCaches[id]; } }); this._mergedSourceCaches = mergedSourceCaches; this._mergedOtherSourceCaches = mergedOtherSourceCaches; this._mergedSymbolSourceCaches = mergedSymbolSourceCaches; } mergeLayers() { const slots: {[string]: StyleLayer[]} = {}; const mergedOrder: StyleLayer[] = []; const mergedLayers: {[string]: StyleLayer} = {}; this._has3DLayers = false; this._hasCircleLayers = false; this._hasSymbolLayers = false; this.forEachFragmentStyle((style: Style) => { for (const layerId of style._order) { const layer = style._layers[layerId]; if (layer.type === 'slot') { const slotName = getNameFromFQID(layerId); if (slots[slotName]) continue; else slots[slotName] = []; } if (layer.slot && slots[layer.slot]) { slots[layer.slot].push(layer); continue; } mergedOrder.push(layer); } }); this._mergedOrder = []; const sort = (layers: StyleLayer[] = []) => { for (const layer of layers) { if (layer.type === 'slot') { const slotName = getNameFromFQID(layer.id); if (slots[slotName]) sort(slots[slotName]); } else { const fqid = makeFQID(layer.id, layer.scope); this._mergedOrder.push(fqid); mergedLayers[fqid] = layer; // Typed layer bookkeeping if (layer.is3D()) this._has3DLayers = true; if (layer.type === 'circle') this._hasCircleLayers = true; if (layer.type === 'symbol') this._hasSymbolLayers = true; } } }; sort(mergedOrder); this._mergedLayers = mergedLayers; this.updateDrapeFirstLayers(); this._buildingIndex.processLayersChanged(); } terrainSetForDrapingOnly(): boolean { return !!this.terrain && this.terrain.drapeRenderMode === DrapeRenderMode.deferred; } getCamera(): ?CameraSpecification { return this.stylesheet.camera; } setCamera(camera: CameraSpecification): Style { this.stylesheet.camera = extend({}, this.stylesheet.camera, camera); this.camera = this.stylesheet.camera; return this; } setProjection(projection?: ?ProjectionSpecification) { if (projection) { this.stylesheet.projection = projection; } else { delete this.stylesheet.projection; } this.mergeProjection(); this._updateMapProjection(); } applyProjectionUpdate() { if (!this._loaded) return; this.dispatcher.broadcast('setProjection', this.map.transform.projectionOptions); if (this.map.transform.projection.requiresDraping) { const hasTerrain = this.getTerrain() || this.stylesheet.terrain; if (!hasTerrain) { this.setTerrainForDraping(); } } else if (this.terrainSetForDrapingOnly()) { this.setTerrain(null); } } _updateMapProjection() { // Skip projection updates from the children fragments if (!this.isRootStyle()) return; if (!this.map._useExplicitProjection) { // Update the visible projection if map's is null this.map._prioritizeAndUpdateProjection(null, this.projection); } else { // Ensure that style is consistent with current projection on style load this.applyProjectionUpdate(); } } _loadSprite(url: string) { this._spriteRequest = loadSprite(url, this.map._requestManager, (err, images) => { this._spriteRequest = null; if (err) { this.fire(new ErrorEvent(err)); } else if (images) { for (const id in images) { this.imageManager.addImage(id, this.scope, images[id]); } } this.imageManager.setLoaded(true, this.scope); this._availableImages = this.imageManager.listImages(this.scope); this.dispatcher.broadcast('setImages', { scope: this.scope, images: this._availableImages }); this.dispatcher.broadcast('spriteLoaded', {scope: this.scope, isLoaded: true}); this.fire(new Event('data', {dataType: 'style'})); }); } _validateLayer(layer: StyleLayer) { const source = this.getOwnSource(layer.source); if (!source) { return; } const sourceLayer = layer.sourceLayer; if (!sourceLayer) { return; } 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(): boolean { if (!this._loaded) return false; if (Object.keys(this._changes.getUpdatedSourceCaches()).length) return false; for (const id in this._sourceCaches) if (!this._sourceCaches[id].loaded()) return false; if (!this.imageManager.isLoaded()) return false; if (!this.modelManager.isLoaded()) return false; for (const {style} of this.fragments) { if (!style.loaded()) return false; } return true; } _serializeImports(): Array<ImportSpecification> | void { if (!this.stylesheet.imports) return undefined; return this.stylesheet.imports.map((importSpec, index) => { const fragment = this.fragments[index]; if (fragment && fragment.style) { importSpec.data = fragment.style.serialize(); } return importSpec; }); } _serializeSources(): {[sourceId: string]: SourceSpecification} { const sources = {}; for (const cacheId in this._sourceCaches) { const source = this._sourceCaches[cacheId].getSource(); if (!sources[source.id]) { sources[source.id] = source.serialize(); } } return sources; } _serializeLayers(ids: Array<string>): Array<LayerSpecification> { const serializedLayers = []; for (const id of ids) { const layer = this._layers[id]; if (layer && layer.type !== 'custom') { serializedLayers.push(layer.serialize()); } } return serializedLayers; } hasLightTransitions(): boolean { if (this.light && this.light.hasTransition()) { return true; } if (this.ambientLight && this.ambientLight.hasTransition()) { return true; } if (this.directionalLight && this.directionalLight.hasTransition()) { return true; } return false; } hasFogTransition(): boolean { if (!this.fog) return false; return this.fog.hasTransition(); } hasTransitions(): boolean { if (this.hasLightTransitions()) { return true; } if (this.hasFogTransition()) { return true; } for (const id in this._sourceCaches) { if (this._sourceCaches[id].hasTransition()) { return true; } } for (const layerId in this._layers) { const layer = this._layers[layerId]; if (layer.hasTransition()) { return true; } } return false; } get order(): Array<string> { if (this.terrain) { assert(this._drapedFirstOrder.length === this._mergedOrder.length, 'drapedFirstOrder doesn\'t match order'); return this._drapedFirstOrder; } return this._mergedOrder; } isLayerDraped(layer: StyleLayer): boolean { if (!this.terrain) return false; if (typeof layer.isLayerDraped === 'function') return layer.isLayerDraped(this.getLayerSourceCache(layer)); return drapedLayers.has(layer.type); } _checkLoaded(): void { if (!this._loaded) { throw new Error('Style is not done loading'); } } _checkLayer(layerId: string): ?StyleLayer { const layer = this.getOwnLayer(layerId); if (!layer) { this.fire(new ErrorEvent(new Error(`The layer '${layerId}' does not exist in the map's style.`))); return; } return layer; } _checkSource(sourceId: string): ?Source { const source = this.getOwnSource(sourceId); if (!source) { this.fire(new ErrorEvent(new Error(`The source '${sourceId}' does not exist in the map's style.`))); return; } return source; } /** * Apply queued style updates in a batch and recalculate zoom-dependent paint properties. * @private */ update(parameters: EvaluationParameters) { if (!this._loaded) { return; } if (this.ambientLight) { this.ambientLight.recalculate(parameters); } if (this.directionalLight) { this.directionalLight.recalculate(parameters); } const brightness = this.calculateLightsBrightness(); parameters.brightness = brightness || 0.0; if (brightness !== this._brightness) { this._brightness = brightness; this.dispatcher.broadcast('setBrightness', brightness); } const changed = this._changes.isDirty(); if (this._changes.isDirty()) { const updatesByScope = this._changes.getLayerUpdatesByScope(); for (const scope in updatesByScope) { const {updatedIds, removedIds} = updatesByScope[scope]; if (updatedIds || removedIds) { this._updateWorkerLayers(scope, updatedIds, removedIds); } } this.updateSourceCaches(); this._updateTilesForChangedImages(); this.updateLayers(parameters); if (this.light) { this.light.updateTransitions(parameters); } if (this.ambientLight) { this.ambientLight.updateTransitions(parameters); } if (this.directionalLight) { this.directionalLight.updateTransitions(parameters); } if (this.fog) { this.fog.updateTransitions(parameters); } this._changes.reset(); } const sourcesUsedBefore = {}; for (const sourceId in this._mergedSourceCaches) { const sourceCache = this._mergedSourceCaches[sourceId]; sourcesUsedBefore[sourceId] = sourceCache.used; sourceCache.used = false; } for (const layerId of this._mergedOrder) { const layer = this._mergedLayers[layerId]; layer.recalculate(parameters, this._availableImages); if (!layer.isHidden(parameters.zoom)) { const sourceCache = this.getLayerSourceCache(layer); if (sourceCache) sourceCache.used = true; } if (!this._precompileDone && this._shouldPrecompile) { for (let i = (layer.minzoom || DEFAULT_MIN_ZOOM); i < (layer.maxzoom || DEFAULT_MAX_ZOOM); i++) { const painter = this.map.painter; if (painter) { const programIds = layer.getProgramIds(); if (!programIds) continue; for (const programId of programIds) { const params = layer.getDefaultProgramParams(programId, parameters.zoom); if (params) { painter.style = this; if (this.fog) { painter._fogVisible = true; params.overrideFog = true; painter.getOrCreateProgram(programId, params); } painter._fogVisible = false; params.overrideFog = false; painter.getOrCreateProgram(programId, params); if (this.stylesheet.terrain || (this.stylesheet.projection && this.stylesheet.projection.name === 'globe')) { params.overrideRtt = true; painter.getOrCreateProgram(programId, params); } } } } } } } if (this._shouldPrecompile) { this._precompileDone = true; } for (const sourceId in sourcesUsedBefore) { const sourceCache = this._mergedSourceCaches[sourceId]; if (sourcesUsedBefore[sourceId] !== sourceCache.used) { sourceCache.getSource().fire(new Event('data', {sourceDataType: 'visibility', dataType:'source', sourceId: sourceCache.getSource().id})); } } if (this.light) { this.light.recalculate(parameters); } if (this.terrain) { this.terrain.recalculate(parameters); } if (this.fog) { this.fog.recalculate(parameters); } this.z = parameters.zoom; if (this._markersNeedUpdate) { this._updateMarkersOpacity(); this._markersNeedUpdate = false; } if (changed) { this.fire(new Event('data', {dataType: 'style'})); } } /* * Apply any queued image changes. */ _updateTilesForChangedImages() { const updatedImages = this._changes.getUpdatedImages(); if (updatedImages.length) { for (const name in this._sourceCaches) { this._sourceCaches[name].reloadTilesForDependencies(['icons', 'patterns'], updatedImages); } this._changes.resetUpdatedImages(); } } _updateWorkerLayers(scope: string, updatedIds?: Array<string>, removedIds?: Array<string>) { const fragmentStyle = this.getFragmentStyle(scope); if (!fragmentStyle) return; this.dispatcher.broadcast('updateLayers', { layers: updatedIds ? fragmentStyle._serializeLayers(updatedIds) : [], scope, removedIds: removedIds || [], options: fragmentStyle.options }); } /** * 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): boolean { 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) => { (this: any)[op.command].apply(this, op.args); }); this.stylesheet = nextState; this.mergeAll(); this.dispatcher.broadcast('setLayers', { layers: this._serializeLayers(this._order), scope: this.scope, options: this.options }); return true; } addImage(id: string, image: StyleImage): this { if (this.getImage(id)) { return this.fire(new ErrorEvent(new Error('An image with this name already exists.'))); } this.imageManager.addImage(id, this.scope, image); this._afterImageUpdated(id); return this; } updateImage(id: string, image: StyleImage) { this.imageManager.updateImage(id, this.scope, image); } getImage(id: string): ?StyleImage { return this.imageManager.getImage(id, this.scope); } removeImage(id: string): this { if (!this.getImage(id)) { return this.fire(new ErrorEvent(new Error('No image with this name exists.'))); } this.imageManager.removeImage(id, this.scope); this._afterImageUpdated(id); return this; } _afterImageUpdated(id: string) { this._availableImages = this.imageManager.listImages(this.scope); this._changes.updateImage(id); this.dispatcher.broadcast('setImages', { scope: this.scope, images: this._availableImages }); this.fire(new Event('data', {dataType: 'style'})); } listImages(): Array<string> { this._checkLoaded(); return this._availableImages.slice(); } addModel(id: string, url: string, options: StyleSetterOptions = {}): this { this._checkLoaded(); if (this._validate(validateModel, `models.${id}`, url, null, options)) return this; this.modelManager.addModel(id, url, this.scope); this._changes.setDirty(); return this; } hasModel(id: string): boolean { return this.modelManager.hasModel(id, this.scope); } removeModel(id: string): this { if (!this.hasModel(id)) { return this.fire(new ErrorEvent(new Error('No model with this ID exists.'))); } this.modelManager.removeModel(id, this.scope); return this; } listModels(): Array<string> { this._checkLoaded(); return this.modelManager.listModels(this.scope); } addSource(id: string, source: SourceSpecification, options: StyleSetterOptions = {}): void { this._checkLoaded(); if (this.getOwnSource(id) !== undefined) { throw new Error(`There is already a source with ID "${id}".`); } 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(validateSource, `sources.${id}`, source, null, options)) return; if (this.map && this.map._collectResourceTiming) (source: any).collectResourceTiming = true; const sourceInstance = createSource(id, source, this.dispatcher, this); sourceInstance.scope = this.scope; sourceInstance.setEventedParent(this, () => ({ isSourceLoaded: this._isSourceCacheLoaded(sourceInstance.id), source: sourceInstance.serialize(), sourceId: sourceInstance.id })); const addSourceCache = (onlySymbols: boolean) => { const sourceCacheId = (onlySymbols ? 'symbol:' : 'other:') + sourceInstance.id; const sourceCacheFQID = makeFQID(sourceCacheId, this.scope); const sourceCache = this._sourceCaches[sourceCacheId] = new SourceCache(sourceCacheFQID, sourceInstance, onlySymbols); (onlySymbols ? this._symbolSourceCaches : this._otherSourceCaches)[sourceInstance.id] = sourceCache; sourceCache.onAdd(this.map); }; addSourceCache(false); if (source.type === 'vector' || source.type === 'geojson') { addSourceCache(true); } if (sourceInstance.onAdd) sourceInstance.onAdd(this.map); // Avoid triggering redundant style update after adding initial sources. if (!options.isInitialLoad) { this.mergeSources(); this._changes.setDirty(); } } /** * 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 { this._checkLoaded(); const source = this.getOwnSource(id); if (!source) { 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.`))); } } if (this.terrain && this.terrain.scope === this.scope && this.terrain.get().source === id) {