UNPKG

mapbox-gl

Version:
787 lines (638 loc) 24.7 kB
'use strict'; var Evented = require('../util/evented'); var StyleLayer = require('./style_layer'); var ImageSprite = require('./image_sprite'); var Light = require('./light'); var GlyphSource = require('../symbol/glyph_source'); var SpriteAtlas = require('../symbol/sprite_atlas'); var LineAtlas = require('../render/line_atlas'); var util = require('../util/util'); var ajax = require('../util/ajax'); var mapbox = require('../util/mapbox'); var browser = require('../util/browser'); var Dispatcher = require('../util/dispatcher'); var AnimationLoop = require('./animation_loop'); var validateStyle = require('./validate_style'); var Source = require('../source/source'); var QueryFeatures = require('../source/query_features'); var SourceCache = require('../source/source_cache'); var styleSpec = require('./style_spec'); var StyleFunction = require('./style_function'); var getWorkerPool = require('../global_worker_pool'); module.exports = Style; function Style(stylesheet, map, options) { this.map = map; this.animationLoop = (map && map.animationLoop) || new AnimationLoop(); this.dispatcher = new Dispatcher(getWorkerPool(), this); this.spriteAtlas = new SpriteAtlas(1024, 1024); this.lineAtlas = new LineAtlas(256, 512); this._layers = {}; this._order = []; this._groups = []; this.sourceCaches = {}; this.zoomHistory = {}; util.bindAll(['_redoPlacement'], this); this._resetUpdates(); options = util.extend({ validate: typeof stylesheet === 'string' ? !mapbox.isMapboxURL(stylesheet) : true }, options); this.setEventedParent(map); this.fire('dataloading', {dataType: 'style'}); var stylesheetLoaded = function(err, stylesheet) { if (err) { this.fire('error', {error: err}); return; } if (options.validate && validateStyle.emitErrors(this, validateStyle(stylesheet))) return; this._loaded = true; this.stylesheet = stylesheet; this.updateClasses(); for (var id in stylesheet.sources) { this.addSource(id, stylesheet.sources[id], options); } if (stylesheet.sprite) { this.sprite = new ImageSprite(stylesheet.sprite); this.sprite.setEventedParent(this); } this.glyphSource = new GlyphSource(stylesheet.glyphs); this._resolve(); this.fire('data', {dataType: 'style'}); this.fire('style.load'); }.bind(this); if (typeof stylesheet === 'string') { ajax.getJSON(mapbox.normalizeStyleURL(stylesheet), stylesheetLoaded); } else { browser.frame(stylesheetLoaded.bind(this, null, stylesheet)); } this.on('source.load', function(event) { var source = event.source; if (source && source.vectorLayerIds) { for (var layerId in this._layers) { var layer = this._layers[layerId]; if (layer.source === source.id) { this._validateLayer(layer); } } } }); } Style.prototype = util.inherit(Evented, { _loaded: false, _validateLayer: function(layer) { var sourceCache = this.sourceCaches[layer.source]; if (!layer.sourceLayer) return; if (!sourceCache) return; var source = sourceCache.getSource(); if (!source.vectorLayerIds) return; if (source.vectorLayerIds.indexOf(layer.sourceLayer) === -1) { this.fire('error', { error: new Error( 'Source layer "' + layer.sourceLayer + '" ' + 'does not exist on source "' + source.id + '" ' + 'as specified by style layer "' + layer.id + '"' ) }); } }, loaded: function() { if (!this._loaded) return false; if (Object.keys(this._updates.sources).length) return false; for (var id in this.sourceCaches) if (!this.sourceCaches[id].loaded()) return false; if (this.sprite && !this.sprite.loaded()) return false; return true; }, _resolve: function() { var layer, layerJSON; this._layers = {}; this._order = this.stylesheet.layers.map(function(layer) { return layer.id; }); // resolve all layers WITHOUT a ref for (var i = 0; i < this.stylesheet.layers.length; i++) { layerJSON = this.stylesheet.layers[i]; if (layerJSON.ref) continue; layer = StyleLayer.create(layerJSON); this._layers[layer.id] = layer; layer.setEventedParent(this, {layer: {id: layer.id}}); } // resolve all layers WITH a ref for (var j = 0; j < this.stylesheet.layers.length; j++) { layerJSON = this.stylesheet.layers[j]; if (!layerJSON.ref) continue; var refLayer = this.getLayer(layerJSON.ref); layer = StyleLayer.create(layerJSON, refLayer); this._layers[layer.id] = layer; layer.setEventedParent(this, {layer: {id: layer.id}}); } this._groupLayers(); this._updateWorkerLayers(); this.light = new Light(this.stylesheet.light); }, _groupLayers: function() { var group; this._groups = []; // Split into groups of consecutive top-level layers with the same source. for (var i = 0; i < this._order.length; ++i) { var layer = this._layers[this._order[i]]; if (!group || layer.source !== group.source) { group = []; group.source = layer.source; this._groups.push(group); } group.push(layer); } }, _updateWorkerLayers: function(ids) { this.dispatcher.broadcast(ids ? 'update layers' : 'set layers', this._serializeLayers(ids)); }, _serializeLayers: function(ids) { ids = ids || this._order; var serialized = []; var options = {includeRefProperties: true}; for (var i = 0; i < ids.length; i++) { serialized.push(this._layers[ids[i]].serialize(options)); } return serialized; }, _applyClasses: function(classes, options) { if (!this._loaded) return; classes = classes || []; options = options || {transition: true}; var transition = this.stylesheet.transition || {}; var layers = this._updates.allPaintProps ? this._layers : this._updates.paintProps; for (var id in layers) { var layer = this._layers[id]; var props = this._updates.paintProps[id]; if (this._updates.allPaintProps || props.all) { layer.updatePaintTransitions(classes, options, transition, this.animationLoop); } else { for (var paintName in props) { this._layers[id].updatePaintTransition(paintName, classes, options, transition, this.animationLoop); } } } this.light.updateLightTransitions(options, transition, this.animationLoop); }, _recalculate: function(z) { if (!this._loaded) return; for (var sourceId in this.sourceCaches) this.sourceCaches[sourceId].used = false; this._updateZoomHistory(z); this.rasterFadeDuration = 300; for (var layerId in this._layers) { var layer = this._layers[layerId]; layer.recalculate(z, this.zoomHistory); if (!layer.isHidden(z) && layer.source) { this.sourceCaches[layer.source].used = true; } } this.light.recalculate(z, this.zoomHistory); var maxZoomTransitionDuration = 300; if (Math.floor(this.z) !== Math.floor(z)) { this.animationLoop.set(maxZoomTransitionDuration); } this.z = z; }, _updateZoomHistory: function(z) { var zh = this.zoomHistory; if (zh.lastIntegerZoom === undefined) { // first time zh.lastIntegerZoom = Math.floor(z); zh.lastIntegerZoomTime = 0; zh.lastZoom = z; } // check whether an integer zoom level as passed since the last frame // and if yes, record it with the time. Used for transitioning patterns. if (Math.floor(zh.lastZoom) < Math.floor(z)) { zh.lastIntegerZoom = Math.floor(z); zh.lastIntegerZoomTime = Date.now(); } else if (Math.floor(zh.lastZoom) > Math.floor(z)) { zh.lastIntegerZoom = Math.floor(z + 1); zh.lastIntegerZoomTime = Date.now(); } zh.lastZoom = z; }, _checkLoaded: function () { if (!this._loaded) { throw new Error('Style is not done loading'); } }, /** * Apply queued style updates in a batch * @private */ update: function(classes, options) { if (!this._updates.changed) return this; if (this._updates.allLayers) { this._groupLayers(); this._updateWorkerLayers(); } else { var updatedIds = Object.keys(this._updates.layers); if (updatedIds.length) { this._updateWorkerLayers(updatedIds); } } var updatedSourceIds = Object.keys(this._updates.sources); var i; for (i = 0; i < updatedSourceIds.length; i++) { this._reloadSource(updatedSourceIds[i]); } for (i = 0; i < this._updates.events.length; i++) { var args = this._updates.events[i]; this.fire(args[0], args[1]); } this._applyClasses(classes, options); if (this._updates.changed) { this.fire('data', {dataType: 'style'}); } this._resetUpdates(); return this; }, _resetUpdates: function() { this._updates = { events: [], layers: {}, sources: {}, paintProps: {} }; }, addSource: function(id, source, options) { this._checkLoaded(); if (this.sourceCaches[id] !== undefined) { throw new Error('There is already a source with this ID'); } if (!source.type) { throw new Error('The type property must be defined, but the only the following properties were given: ' + Object.keys(source) + '.'); } var builtIns = ['vector', 'raster', 'geojson', 'video', 'image']; var shouldValidate = builtIns.indexOf(source.type) >= 0; if (shouldValidate && this._validate(validateStyle.source, 'sources.' + id, source, null, options)) return this; source = new SourceCache(id, source, this.dispatcher); this.sourceCaches[id] = source; source.style = this; source.setEventedParent(this, {source: source.getSource()}); if (source.onAdd) source.onAdd(this.map); this._updates.changed = true; return this; }, /** * Remove a source from this stylesheet, given its id. * @param {string} id id of the source to remove * @returns {Style} this style * @throws {Error} if no source is found with the given ID * @private */ removeSource: function(id) { this._checkLoaded(); if (this.sourceCaches[id] === undefined) { throw new Error('There is no source with this ID'); } var sourceCache = this.sourceCaches[id]; delete this.sourceCaches[id]; delete this._updates.sources[id]; sourceCache.setEventedParent(null); sourceCache.clearTiles(); if (sourceCache.onRemove) sourceCache.onRemove(this.map); this._updates.changed = true; return this; }, /** * Get a source by id. * @param {string} id id of the desired source * @returns {Object} source * @private */ getSource: function(id) { 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 {StyleLayer|Object} layer * @param {string=} before ID of an existing layer to insert before * @returns {Style} `this` * @private */ addLayer: function(layer, before, options) { this._checkLoaded(); if (!(layer instanceof StyleLayer)) { // this layer is not in the style.layers array, so we pass an impossible array index if (this._validate(validateStyle.layer, 'layers.' + layer.id, layer, {arrayIndex: -1}, options)) return this; var refLayer = layer.ref && this.getLayer(layer.ref); layer = StyleLayer.create(layer, refLayer); } this._validateLayer(layer); layer.setEventedParent(this, {layer: {id: layer.id}}); this._layers[layer.id] = layer; this._order.splice(before ? this._order.indexOf(before) : Infinity, 0, layer.id); this._updates.allLayers = true; if (layer.source) { this._updates.sources[layer.source] = true; } return this.updateClasses(layer.id); }, /** * Remove a layer from this stylesheet, given its id. * @param {string} id id of the layer to remove * @returns {Style} this style * @throws {Error} if no layer is found with the given ID * @private */ removeLayer: function(id) { this._checkLoaded(); var layer = this._layers[id]; if (layer === undefined) { throw new Error('There is no layer with this ID'); } for (var i in this._layers) { if (this._layers[i].ref === id) { this.removeLayer(i); } } layer.setEventedParent(null); delete this._layers[id]; delete this._updates.layers[id]; delete this._updates.paintProps[id]; this._order.splice(this._order.indexOf(id), 1); this._updates.allLayers = true; this._updates.changed = true; return this; }, /** * 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 * @private */ getLayer: function(id) { return this._layers[id]; }, /** * If a layer has a `ref` property that makes it derive some values * from another layer, return that referent layer. Otherwise, * returns the layer itself. * @param {string} id the layer's id * @returns {Layer} the referent layer or the layer itself * @private */ getReferentLayer: function(id) { var layer = this.getLayer(id); if (layer.ref) { layer = this.getLayer(layer.ref); } return layer; }, setLayerZoomRange: function(layerId, minzoom, maxzoom) { this._checkLoaded(); var layer = this.getReferentLayer(layerId); if (layer.minzoom === minzoom && layer.maxzoom === maxzoom) return this; if (minzoom != null) { layer.minzoom = minzoom; } if (maxzoom != null) { layer.maxzoom = maxzoom; } return this._updateLayer(layer); }, setFilter: function(layerId, filter) { this._checkLoaded(); var layer = this.getReferentLayer(layerId); if (filter !== null && this._validate(validateStyle.filter, 'layers.' + layer.id + '.filter', filter)) return this; if (util.deepEqual(layer.filter, filter)) return this; layer.filter = util.clone(filter); return this._updateLayer(layer); }, /** * Get a layer's filter object * @param {string} layer the layer to inspect * @returns {*} the layer's filter, if any * @private */ getFilter: function(layer) { return util.clone(this.getReferentLayer(layer).filter); }, setLayoutProperty: function(layerId, name, value) { this._checkLoaded(); var layer = this.getReferentLayer(layerId); if (util.deepEqual(layer.getLayoutProperty(name), value)) return this; layer.setLayoutProperty(name, value); return this._updateLayer(layer); }, /** * Get a layout property's value from a given layer * @param {string} layer the layer to inspect * @param {string} name the name of the layout property * @returns {*} the property value * @private */ getLayoutProperty: function(layer, name) { return this.getReferentLayer(layer).getLayoutProperty(name); }, setPaintProperty: function(layerId, name, value, klass) { this._checkLoaded(); var layer = this.getLayer(layerId); if (util.deepEqual(layer.getPaintProperty(name, klass), value)) return this; var wasFeatureConstant = layer.isPaintValueFeatureConstant(name); layer.setPaintProperty(name, value, klass); var isFeatureConstant = !( value && StyleFunction.isFunctionDefinition(value) && value.property !== '$zoom' && value.property !== undefined ); if (!isFeatureConstant || !wasFeatureConstant) { this._updates.layers[layerId] = true; if (layer.source) { this._updates.sources[layer.source] = true; } } return this.updateClasses(layerId, name); }, getPaintProperty: function(layer, name, klass) { return this.getLayer(layer).getPaintProperty(name, klass); }, updateClasses: function (layerId, paintName) { this._updates.changed = true; if (!layerId) { this._updates.allPaintProps = true; } else { var props = this._updates.paintProps; if (!props[layerId]) props[layerId] = {}; props[layerId][paintName || 'all'] = true; } return this; }, serialize: function() { return util.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: util.mapObject(this.sourceCaches, function(source) { return source.serialize(); }), layers: this._order.map(function(id) { return this._layers[id].serialize(); }, this) }, function(value) { return value !== undefined; }); }, _updateLayer: function (layer) { this._updates.layers[layer.id] = true; if (layer.source) { this._updates.sources[layer.source] = true; } this._updates.changed = true; return this; }, _flattenRenderedFeatures: function(sourceResults) { var features = []; for (var l = this._order.length - 1; l >= 0; l--) { var layerID = this._order[l]; for (var s = 0; s < sourceResults.length; s++) { var layerFeatures = sourceResults[s][layerID]; if (layerFeatures) { for (var f = 0; f < layerFeatures.length; f++) { features.push(layerFeatures[f]); } } } } return features; }, queryRenderedFeatures: function(queryGeometry, params, zoom, bearing) { if (params && params.filter) { this._validate(validateStyle.filter, 'queryRenderedFeatures.filter', params.filter); } var includedSources = {}; if (params && params.layers) { for (var i = 0; i < params.layers.length; i++) { var layer = this._layers[params.layers[i]]; if (!(layer instanceof StyleLayer)) { // this layer is not in the style.layers array return this.fire('error', {error: 'The layer \'' + params.layers[i] + '\' does not exist in the map\'s style and cannot be queried for features.'}); } includedSources[layer.source] = true; } } var sourceResults = []; for (var id in this.sourceCaches) { if (params.layers && !includedSources[id]) continue; var sourceCache = this.sourceCaches[id]; var results = QueryFeatures.rendered(sourceCache, this._layers, queryGeometry, params, zoom, bearing); sourceResults.push(results); } return this._flattenRenderedFeatures(sourceResults); }, querySourceFeatures: function(sourceID, params) { if (params && params.filter) { this._validate(validateStyle.filter, 'querySourceFeatures.filter', params.filter); } var sourceCache = this.sourceCaches[sourceID]; return sourceCache ? QueryFeatures.source(sourceCache, params) : []; }, addSourceType: function (name, SourceType, callback) { if (Source.getType(name)) { return callback(new Error('A source type called "' + name + '" already exists.')); } Source.setType(name, SourceType); if (!SourceType.workerSourceURL) { return callback(null, null); } this.dispatcher.broadcast('load worker source', { name: name, url: SourceType.workerSourceURL }, callback); }, getLight: function() { return this.light.getLight(); }, setLight: function(lightOptions, transitionOptions) { this._checkLoaded(); var light = this.light.getLight(); var _update = false; for (var key in lightOptions) { if (!util.deepEqual(lightOptions[key], light[key])) { _update = true; break; } } if (!_update) return this; var transition = this.stylesheet.transition || {}; this.light.setLight(lightOptions); return this.light.updateLightTransitions(transitionOptions || {transition: true}, transition, this.animationLoop); }, _validate: function(validate, key, value, props, options) { if (options && options.validate === false) { return false; } return validateStyle.emitErrors(this, validate.call(validateStyle, util.extend({ key: key, style: this.serialize(), value: value, styleSpec: styleSpec }, props))); }, _remove: function() { for (var id in this.sourceCaches) { this.sourceCaches[id].clearTiles(); } this.dispatcher.remove(); }, _reloadSource: function(id) { this.sourceCaches[id].reload(); }, _updateSources: function(transform) { for (var id in this.sourceCaches) { this.sourceCaches[id].update(transform); } }, _redoPlacement: function() { for (var id in this.sourceCaches) { if (this.sourceCaches[id].redoPlacement) this.sourceCaches[id].redoPlacement(); } }, // Callbacks from web workers 'get icons': function(mapId, params, callback) { var sprite = this.sprite; var spriteAtlas = this.spriteAtlas; if (sprite.loaded()) { spriteAtlas.setSprite(sprite); spriteAtlas.addIcons(params.icons, callback); } else { sprite.on('data', function() { spriteAtlas.setSprite(sprite); spriteAtlas.addIcons(params.icons, callback); }); } }, 'get glyphs': function(mapId, params, callback) { var stacks = params.stacks, remaining = Object.keys(stacks).length, allGlyphs = {}; for (var fontName in stacks) { this.glyphSource.getSimpleGlyphs(fontName, stacks[fontName], params.uid, done); } function done(err, glyphs, fontName) { if (err) console.error(err); allGlyphs[fontName] = glyphs; remaining--; if (remaining === 0) callback(null, allGlyphs); } } });