mapbox-gl
Version:
A WebGL interactive maps library
351 lines (296 loc) • 12.8 kB
JavaScript
'use strict';
var util = require('../util/util');
var StyleTransition = require('./style_transition');
var StyleDeclaration = require('./style_declaration');
var styleSpec = require('./style_spec');
var validateStyle = require('./validate_style');
var parseColor = require('./parse_color');
var Evented = require('../util/evented');
module.exports = StyleLayer;
var TRANSITION_SUFFIX = '-transition';
function StyleLayer(layer, refLayer) {
this.set(layer, refLayer);
}
StyleLayer.prototype = util.inherit(Evented, {
set: function(layer, refLayer) {
this.id = layer.id;
this.ref = layer.ref;
this.metadata = layer.metadata;
this.type = (refLayer || layer).type;
this.source = (refLayer || layer).source;
this.sourceLayer = (refLayer || layer)['source-layer'];
this.minzoom = (refLayer || layer).minzoom;
this.maxzoom = (refLayer || layer).maxzoom;
this.filter = (refLayer || layer).filter;
this.paint = {};
this.layout = {};
this._paintSpecifications = styleSpec['paint_' + this.type];
this._layoutSpecifications = styleSpec['layout_' + this.type];
this._paintTransitions = {}; // {[propertyName]: StyleTransition}
this._paintTransitionOptions = {}; // {[className]: {[propertyName]: { duration:Number, delay:Number }}}
this._paintDeclarations = {}; // {[className]: {[propertyName]: StyleDeclaration}}
this._layoutDeclarations = {}; // {[propertyName]: StyleDeclaration}
this._layoutFunctions = {}; // {[propertyName]: Boolean}
var paintName, layoutName;
var options = {validate: false};
// Resolve paint declarations
for (var key in layer) {
var match = key.match(/^paint(?:\.(.*))?$/);
if (match) {
var klass = match[1] || '';
for (paintName in layer[key]) {
this.setPaintProperty(paintName, layer[key][paintName], klass, options);
}
}
}
// Resolve layout declarations
if (this.ref) {
this._layoutDeclarations = refLayer._layoutDeclarations;
} else {
for (layoutName in layer.layout) {
this.setLayoutProperty(layoutName, layer.layout[layoutName], options);
}
}
// set initial layout/paint values
for (paintName in this._paintSpecifications) {
this.paint[paintName] = this.getPaintValue(paintName);
}
for (layoutName in this._layoutSpecifications) {
this._updateLayoutValue(layoutName);
}
},
setLayoutProperty: function(name, value, options) {
if (value == null) {
delete this._layoutDeclarations[name];
} else {
var key = 'layers.' + this.id + '.layout.' + name;
if (this._validate(validateStyle.layoutProperty, key, name, value, options)) return;
this._layoutDeclarations[name] = new StyleDeclaration(this._layoutSpecifications[name], value);
}
this._updateLayoutValue(name);
},
getLayoutProperty: function(name) {
return (
this._layoutDeclarations[name] &&
this._layoutDeclarations[name].value
);
},
getLayoutValue: function(name, globalProperties, featureProperties) {
var specification = this._layoutSpecifications[name];
var declaration = this._layoutDeclarations[name];
if (declaration) {
return declaration.calculate(globalProperties, featureProperties);
} else {
return specification.default;
}
},
setPaintProperty: function(name, value, klass, options) {
var validateStyleKey = 'layers.' + this.id + (klass ? '["paint.' + klass + '"].' : '.paint.') + name;
if (util.endsWith(name, TRANSITION_SUFFIX)) {
if (!this._paintTransitionOptions[klass || '']) {
this._paintTransitionOptions[klass || ''] = {};
}
if (value === null || value === undefined) {
delete this._paintTransitionOptions[klass || ''][name];
} else {
if (this._validate(validateStyle.paintProperty, validateStyleKey, name, value, options)) return;
this._paintTransitionOptions[klass || ''][name] = value;
}
} else {
if (!this._paintDeclarations[klass || '']) {
this._paintDeclarations[klass || ''] = {};
}
if (value === null || value === undefined) {
delete this._paintDeclarations[klass || ''][name];
} else {
if (this._validate(validateStyle.paintProperty, validateStyleKey, name, value, options)) return;
this._paintDeclarations[klass || ''][name] = new StyleDeclaration(this._paintSpecifications[name], value);
}
}
},
getPaintProperty: function(name, klass) {
klass = klass || '';
if (util.endsWith(name, TRANSITION_SUFFIX)) {
return (
this._paintTransitionOptions[klass] &&
this._paintTransitionOptions[klass][name]
);
} else {
return (
this._paintDeclarations[klass] &&
this._paintDeclarations[klass][name] &&
this._paintDeclarations[klass][name].value
);
}
},
getPaintValue: function(name, globalProperties, featureProperties) {
var specification = this._paintSpecifications[name];
var transition = this._paintTransitions[name];
if (transition) {
return transition.calculate(globalProperties, featureProperties);
} else if (specification.type === 'color' && specification.default) {
return parseColor(specification.default);
} else {
return specification.default;
}
},
getPaintValueStopZoomLevels: function(name) {
var transition = this._paintTransitions[name];
if (transition) {
return transition.declaration.stopZoomLevels;
} else {
return [];
}
},
getPaintInterpolationT: function(name, zoom) {
var transition = this._paintTransitions[name];
return transition.declaration.calculateInterpolationT({ zoom: zoom });
},
isPaintValueFeatureConstant: function(name) {
var transition = this._paintTransitions[name];
if (transition) {
return transition.declaration.isFeatureConstant;
} else {
return true;
}
},
isLayoutValueFeatureConstant: function(name) {
var declaration = this._layoutDeclarations[name];
if (declaration) {
return declaration.isFeatureConstant;
} else {
return true;
}
},
isPaintValueZoomConstant: function(name) {
var transition = this._paintTransitions[name];
if (transition) {
return transition.declaration.isZoomConstant;
} else {
return true;
}
},
isHidden: function(zoom) {
if (this.minzoom && zoom < this.minzoom) return true;
if (this.maxzoom && zoom >= this.maxzoom) return true;
if (this.layout['visibility'] === 'none') return true;
return false;
},
updatePaintTransitions: function(classes, options, globalOptions, animationLoop) {
var declarations = util.extend({}, this._paintDeclarations['']);
for (var i = 0; i < classes.length; i++) {
util.extend(declarations, this._paintDeclarations[classes[i]]);
}
var name;
for (name in declarations) { // apply new declarations
this._applyPaintDeclaration(name, declarations[name], options, globalOptions, animationLoop);
}
for (name in this._paintTransitions) {
if (!(name in declarations)) // apply removed declarations
this._applyPaintDeclaration(name, null, options, globalOptions, animationLoop);
}
},
updatePaintTransition: function(name, classes, options, globalOptions, animationLoop) {
var declaration = this._paintDeclarations[''][name];
for (var i = 0; i < classes.length; i++) {
var classPaintDeclarations = this._paintDeclarations[classes[i]];
if (classPaintDeclarations && classPaintDeclarations[name]) {
declaration = classPaintDeclarations[name];
}
}
this._applyPaintDeclaration(name, declaration, options, globalOptions, animationLoop);
},
// update all zoom-dependent layout/paint values
recalculate: function(zoom, zoomHistory) {
for (var paintName in this._paintTransitions) {
this.paint[paintName] = this.getPaintValue(paintName, {zoom: zoom, zoomHistory: zoomHistory});
}
for (var layoutName in this._layoutFunctions) {
this.layout[layoutName] = this.getLayoutValue(layoutName, {zoom: zoom, zoomHistory: zoomHistory});
}
},
serialize: function(options) {
var output = {
'id': this.id,
'ref': this.ref,
'metadata': this.metadata,
'minzoom': this.minzoom,
'maxzoom': this.maxzoom
};
for (var klass in this._paintDeclarations) {
var key = klass === '' ? 'paint' : 'paint.' + klass;
output[key] = util.mapObject(this._paintDeclarations[klass], getDeclarationValue);
}
if (!this.ref || (options && options.includeRefProperties)) {
util.extend(output, {
'type': this.type,
'source': this.source,
'source-layer': this.sourceLayer,
'filter': this.filter,
'layout': util.mapObject(this._layoutDeclarations, getDeclarationValue)
});
}
return util.filterObject(output, function(value, key) {
return value !== undefined && !(key === 'layout' && !Object.keys(value).length);
});
},
// set paint transition based on a given paint declaration
_applyPaintDeclaration: function (name, declaration, options, globalOptions, animationLoop) {
var oldTransition = options.transition ? this._paintTransitions[name] : undefined;
var spec = this._paintSpecifications[name];
if (declaration === null || declaration === undefined) {
declaration = new StyleDeclaration(spec, spec.default);
}
if (oldTransition && oldTransition.declaration.json === declaration.json) return;
var transitionOptions = util.extend({
duration: 300,
delay: 0
}, globalOptions, this.getPaintProperty(name + TRANSITION_SUFFIX));
var newTransition = this._paintTransitions[name] =
new StyleTransition(spec, declaration, oldTransition, transitionOptions);
if (!newTransition.instant()) {
newTransition.loopID = animationLoop.set(newTransition.endTime - Date.now());
}
if (oldTransition) {
animationLoop.cancel(oldTransition.loopID);
}
},
// update layout value if it's constant, or mark it as zoom-dependent
_updateLayoutValue: function(name) {
var declaration = this._layoutDeclarations[name];
if (declaration && declaration.isFunction) {
this._layoutFunctions[name] = true;
} else {
delete this._layoutFunctions[name];
this.layout[name] = this.getLayoutValue(name);
}
},
_validate: function(validate, key, name, value, options) {
if (options && options.validate === false) {
return false;
}
return validateStyle.emitErrors(this, validate.call(validateStyle, {
key: key,
layerType: this.type,
objectKey: name,
value: value,
styleSpec: styleSpec,
// Workaround for https://github.com/mapbox/mapbox-gl-js/issues/2407
style: {glyphs: true, sprite: true}
}));
}
});
function getDeclarationValue(declaration) {
return declaration.value;
}
var Classes = {
background: require('./style_layer/background_style_layer'),
circle: require('./style_layer/circle_style_layer'),
fill: require('./style_layer/fill_style_layer'),
line: require('./style_layer/line_style_layer'),
raster: require('./style_layer/raster_style_layer'),
symbol: require('./style_layer/symbol_style_layer')
};
StyleLayer.create = function(layer, refLayer) {
return new Classes[(refLayer || layer).type](layer, refLayer);
};