mapbox-gl
Version:
A WebGL interactive maps library
1,426 lines (1,190 loc) • 109 kB
JavaScript
// @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) {