diff --git a/build/generate-style-code.js b/build/generate-style-code.js new file mode 100644 index 00000000000..2fc9419e57d --- /dev/null +++ b/build/generate-style-code.js @@ -0,0 +1,130 @@ +'use strict'; + +require('flow-remove-types/register'); + +const fs = require('fs'); +const ejs = require('ejs'); +const spec = require('../src/style-spec/reference/v8'); +const Color = require('../src/style-spec/util/color'); + +global.camelize = function (str) { + return str.replace(/(?:^|-)(.)/g, function (_, x) { + return x.toUpperCase(); + }); +}; + +global.isDataDriven = function (property) { + return property['property-function'] === true; +}; + +global.flowType = function (property) { + switch (property.type) { + case 'boolean': + return 'boolean'; + case 'number': + return 'number'; + case 'string': + return 'string'; + case 'enum': + return Object.keys(property.values).map(JSON.stringify).join(' | '); + case 'color': + return `Color`; + case 'array': + if (property.length) { + return `[${new Array(property.length).fill(flowType({type: property.value})).join(', ')}]`; + } else { + return `Array<${flowType({type: property.value})}>`; + } + default: throw new Error(`unknown type for ${property.name}`) + } +}; + +global.propertyType = function (property) { + if (isDataDriven(property)) { + return `DataDrivenProperty<${flowType(property)}>`; + } else if (/-pattern$/.test(property.name) || property.name === 'line-dasharray') { + return `CrossFadedProperty<${flowType(property)}>`; + } else if (property.name === 'heatmap-color') { + return `HeatmapColorProperty`; + } else { + return `DataConstantProperty<${flowType(property)}>`; + } +}; + +global.runtimeType = function (property) { + switch (property.type) { + case 'boolean': + return 'BooleanType'; + case 'number': + return 'NumberType'; + case 'string': + case 'enum': + return 'StringType'; + case 'color': + return `ColorType`; + case 'array': + if (property.length) { + return `array(${runtimeType({type: property.value})}, ${property.length})`; + } else { + return `array(${runtimeType({type: property.value})})`; + } + default: throw new Error(`unknown type for ${property.name}`) + } +}; + +global.defaultValue = function (property) { + switch (property.type) { + case 'boolean': + case 'number': + case 'string': + case 'array': + case 'enum': + return JSON.stringify(property.default); + case 'color': + if (typeof property.default !== 'string') { + return JSON.stringify(property.default); + } else { + const {r, g, b, a} = Color.parse(property.default); + return `new Color(${r}, ${g}, ${b}, ${a})`; + } + default: throw new Error(`unknown type for ${property.name}`) + } +}; + +global.propertyValue = function (property, type) { + if (isDataDriven(property)) { + return `new DataDrivenProperty(styleSpec["${type}_${property.layerType}"]["${property.name}"])`; + } else if (/-pattern$/.test(property.name) || property.name === 'line-dasharray') { + return `new CrossFadedProperty(styleSpec["${type}_${property.layerType}"]["${property.name}"])`; + } else if (property.name === 'heatmap-color') { + return `new HeatmapColorProperty(styleSpec["${type}_${property.layerType}"]["${property.name}"])`; + } else { + return `new DataConstantProperty(styleSpec["${type}_${property.layerType}"]["${property.name}"])`; + } +}; + +const propertiesJs = ejs.compile(fs.readFileSync('src/style/style_layer/layer_properties.js.ejs', 'utf8'), {strict: true}); + +const layers = Object.keys(spec.layer.type.values).map((type) => { + const layoutProperties = Object.keys(spec[`layout_${type}`]).reduce((memo, name) => { + if (name !== 'visibility') { + spec[`layout_${type}`][name].name = name; + spec[`layout_${type}`][name].layerType = type; + memo.push(spec[`layout_${type}`][name]); + } + return memo; + }, []); + + const paintProperties = Object.keys(spec[`paint_${type}`]).reduce((memo, name) => { + spec[`paint_${type}`][name].name = name; + spec[`paint_${type}`][name].layerType = type; + memo.push(spec[`paint_${type}`][name]); + return memo; + }, []); + + return { type, layoutProperties, paintProperties }; +}); + +for (const layer of layers) { + fs.writeFileSync(`src/style/style_layer/${layer.type.replace('-', '_')}_style_layer_properties.js`, propertiesJs(layer)) +} diff --git a/package.json b/package.json index cca8c4bed7f..53dafeecd93 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "coveralls": "^2.11.8", "derequire": "^2.0.6", "documentation": "5.3.3", + "ejs": "^2.5.7", "envify": "^4.0.0", "eslint": "4.1.1", "eslint-config-mourner": "^2.0.0", diff --git a/src/data/bucket/line_bucket.js b/src/data/bucket/line_bucket.js index ef40d1bcfa3..9403fb1bdd7 100644 --- a/src/data/bucket/line_bucket.js +++ b/src/data/bucket/line_bucket.js @@ -12,7 +12,7 @@ const vectorTileFeatureTypes = require('@mapbox/vector-tile').VectorTileFeature. import type {Bucket, IndexedFeature, PopulateParameters, SerializedBucket} from '../bucket'; import type {ProgramInterface} from '../program_configuration'; -import type StyleLayer from '../../style/style_layer'; +import type LineStyleLayer from '../../style/style_layer/line_style_layer'; import type Point from '@mapbox/point-geometry'; import type {Segment} from '../segment'; import type {StructArray} from '../../util/struct_array'; @@ -62,7 +62,7 @@ const lineInterface = { {property: 'line-gap-width', name: 'gapwidth'}, {property: 'line-offset'}, {property: 'line-width'}, - {property: 'line-width', name: 'floorwidth', useIntegerZoom: true}, + {property: 'line-floorwidth'}, ], indexArrayType: TriangleIndexArray }; @@ -103,7 +103,7 @@ class LineBucket implements Bucket { index: number; zoom: number; overscaling: number; - layers: Array; + layers: Array; layoutVertexArray: StructArray; layoutVertexBuffer: VertexBuffer; @@ -168,10 +168,10 @@ class LineBucket implements Bucket { addFeature(feature: VectorTileFeature, geometry: Array>) { const layout = this.layers[0].layout; - const join = this.layers[0].getLayoutValue('line-join', {zoom: this.zoom}, feature); - const cap = layout['line-cap']; - const miterLimit = layout['line-miter-limit']; - const roundLimit = layout['line-round-limit']; + const join = layout.get('line-join').evaluate(feature); + const cap = layout.get('line-cap'); + const miterLimit = layout.get('line-miter-limit'); + const roundLimit = layout.get('line-round-limit'); for (const line of geometry) { this.addLine(line, feature, join, cap, miterLimit, roundLimit); diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index 8bb1e141fda..2c088f5fc01 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -1,4 +1,5 @@ // @flow + const Point = require('@mapbox/point-geometry'); const {SegmentVector} = require('../segment'); const VertexBuffer = require('../../gl/vertex_buffer'); @@ -27,6 +28,8 @@ import type { } from '../../util/struct_array'; import type SymbolStyleLayer from '../../style/style_layer/symbol_style_layer'; import type {SymbolQuad} from '../../symbol/quads'; +import type {SizeData} from '../../symbol/symbol_size'; +import type {PossiblyEvaluatedPropertyValue} from '../../style/properties'; export type SingleCollisionBox = { x1: number; @@ -365,8 +368,29 @@ class SymbolBucket implements Bucket { index: number; sdfIcons: boolean; iconsNeedLinear: boolean; - textSizeData: any; - iconSizeData: any; + + // The symbol layout process needs `text-size` evaluated at up to five different zoom levels, and + // `icon-size` at up to three: + // + // 1. `text-size` at the zoom level of the bucket. Used to calculate a per-feature size for source `text-size` + // expressions, and to calculate the box dimensions for icon-text-fit. + // 2. `icon-size` at the zoom level of the bucket. Used to calculate a per-feature size for source `icon-size` + // expressions. + // 3. `text-size` and `icon-size` at the zoom level of the bucket, plus one. Used to calculate collision boxes. + // 4. `text-size` at zoom level 18. Used for something line-symbol-placement-related. + // 5. For composite `*-size` expressions: two zoom levels of curve stops that "cover" the zoom level of the + // bucket. These go into a vertex buffer and are used by the shader to interpolate the size at render time. + // + // (1) and (2) are stored in `this.layers[0].layout`. The remainder are below. + // + textSizeData: SizeData; + iconSizeData: SizeData; + layoutTextSize: PossiblyEvaluatedPropertyValue; // (3) + layoutIconSize: PossiblyEvaluatedPropertyValue; // (3) + textMaxSize: PossiblyEvaluatedPropertyValue; // (4) + compositeTextSizes: [PossiblyEvaluatedPropertyValue, PossiblyEvaluatedPropertyValue]; // (5) + compositeIconSizes: [PossiblyEvaluatedPropertyValue, PossiblyEvaluatedPropertyValue]; // (5) + placedGlyphArray: StructArray; placedIconArray: StructArray; glyphOffsetArray: StructArray; @@ -414,13 +438,34 @@ class SymbolBucket implements Bucket { this.symbolInstances = options.symbolInstances; const layout = options.layers[0].layout; - this.sortFeaturesByY = layout['text-allow-overlap'] || layout['icon-allow-overlap'] || - layout['text-ignore-placement'] || layout['icon-ignore-placement']; + this.sortFeaturesByY = layout.get('text-allow-overlap') || layout.get('icon-allow-overlap') || + layout.get('text-ignore-placement') || layout.get('icon-ignore-placement'); } else { - const layer = this.layers[0]; - this.textSizeData = getSizeData(this.zoom, layer, 'text-size'); - this.iconSizeData = getSizeData(this.zoom, layer, 'icon-size'); + const layer: SymbolStyleLayer = this.layers[0]; + const unevaluatedLayoutValues = layer._unevaluatedLayout._values; + + this.textSizeData = getSizeData(this.zoom, unevaluatedLayoutValues['text-size']); + if (this.textSizeData.functionType === 'composite') { + const {min, max} = this.textSizeData.zoomRange; + this.compositeTextSizes = [ + unevaluatedLayoutValues['text-size'].possiblyEvaluate({zoom: min}), + unevaluatedLayoutValues['text-size'].possiblyEvaluate({zoom: max}) + ]; + } + + this.iconSizeData = getSizeData(this.zoom, unevaluatedLayoutValues['icon-size']); + if (this.iconSizeData.functionType === 'composite') { + const {min, max} = this.iconSizeData.zoomRange; + this.compositeIconSizes = [ + unevaluatedLayoutValues['icon-size'].possiblyEvaluate({zoom: min}), + unevaluatedLayoutValues['icon-size'].possiblyEvaluate({zoom: max}) + ]; + } + + this.layoutTextSize = unevaluatedLayoutValues['text-size'].possiblyEvaluate({zoom: this.zoom + 1}); + this.layoutIconSize = unevaluatedLayoutValues['icon-size'].possiblyEvaluate({zoom: this.zoom + 1}); + this.textMaxSize = unevaluatedLayoutValues['text-size'].possiblyEvaluate({zoom: 18}); } } @@ -437,12 +482,14 @@ class SymbolBucket implements Bucket { } populate(features: Array, options: PopulateParameters) { - const layer: SymbolStyleLayer = this.layers[0]; + const layer = this.layers[0]; const layout = layer.layout; - const textFont = layout['text-font']; - const hasText = (!layer.isLayoutValueFeatureConstant('text-field') || layout['text-field']) && textFont; - const hasIcon = (!layer.isLayoutValueFeatureConstant('icon-image') || layout['icon-image']); + const textFont = layout.get('text-font').join(','); + const textField = layout.get('text-field'); + const iconImage = layout.get('icon-image'); + const hasText = textField.value.kind !== 'constant' || textField.value.value.length > 0 && textFont.length > 0; + const hasIcon = iconImage.value.kind !== 'constant' || iconImage.value.value && iconImage.value.value.length > 0; this.features = []; @@ -462,13 +509,13 @@ class SymbolBucket implements Bucket { let text; if (hasText) { - text = layer.getValueAndResolveTokens('text-field', globalProperties, feature); - text = transformText(text, layer, globalProperties, feature); + text = layer.getValueAndResolveTokens('text-field', feature); + text = transformText(text, layer, feature); } let icon; if (hasIcon) { - icon = layer.getValueAndResolveTokens('icon-image', globalProperties, feature); + icon = layer.getValueAndResolveTokens('icon-image', feature); } if (!text && !icon) { @@ -494,7 +541,7 @@ class SymbolBucket implements Bucket { } if (text) { - const textAlongLine = layout['text-rotation-alignment'] === 'map' && layout['symbol-placement'] === 'line'; + const textAlongLine = layout.get('text-rotation-alignment') === 'map' && layout.get('symbol-placement') === 'line'; const allowsVerticalWritingMode = scriptDetection.allowsVerticalWritingMode(text); for (let i = 0; i < text.length; i++) { stack[text.charCodeAt(i)] = true; @@ -508,7 +555,7 @@ class SymbolBucket implements Bucket { } } - if (layout['symbol-placement'] === 'line') { + if (layout.get('symbol-placement') === 'line') { // Merge adjacent lines with the same text to improve labelling. // It's better to place labels on one long line than on many short segments. this.features = mergeLines(this.features); diff --git a/src/data/program_configuration.js b/src/data/program_configuration.js index b722cfe636d..442bba4ed6d 100644 --- a/src/data/program_configuration.js +++ b/src/data/program_configuration.js @@ -1,5 +1,7 @@ // @flow +import type {GlobalProperties} from "../style-spec/expression/index"; + const createVertexArrayType = require('./vertex_array_type'); const packUint8ToFloat = require('../shaders/encode_attribute').packUint8ToFloat; const VertexBuffer = require('../gl/vertex_buffer'); @@ -7,8 +9,9 @@ const VertexBuffer = require('../gl/vertex_buffer'); import type StyleLayer from '../style/style_layer'; import type {ViewType, StructArray, SerializedStructArray, StructArrayTypeParameters} from '../util/struct_array'; import type Program from '../render/program'; -import type {Feature} from '../style-spec/expression'; +import type {Feature, SourceExpression, CompositeExpression} from '../style-spec/expression'; import type Color from '../style-spec/util/color'; +import type {PossiblyEvaluated, PossiblyEvaluatedPropertyValue} from '../style/properties'; type LayoutAttribute = { name: string, @@ -43,12 +46,35 @@ function packColor(color: Color): [number, number] { ]; } -interface Binder { +/** + * `Binder` is the interface definition for the strategies for constructing, + * uploading, and binding paint property data as GLSL attributes. + * + * It has three implementations, one for each of the three strategies we use: + * + * * For _constant_ properties -- those whose value is a constant, or the constant + * result of evaluating a camera expression at a particular camera position -- we + * don't need a vertex buffer, and instead use a uniform. + * * For data expressions, we use a vertex buffer with a single attribute value, + * the evaluated result of the source function for the given feature. + * * For composite expressions, we use a vertex buffer with two attributes: min and + * max values covering the range of zooms at which we expect the tile to be + * displayed. These values are calculated by evaluating the composite expression for + * the given feature at strategically chosen zoom levels. In addition to this + * attribute data, we also use a uniform value which the shader uses to interpolate + * between the min and max value at the final displayed zoom level. The use of a + * uniform allows us to cheaply update the value on every frame. + * + * Note that the shader source varies depending on whether we're using a uniform or + * attribute. We dynamically compile shaders at runtime to accomodate this. + * + * @private + */ +interface Binder { property: string; + statistics: { max: number }; - populatePaintArray(layer: StyleLayer, - paintArray: StructArray, - statistics: PaintPropertyStatistics, + populatePaintArray(paintArray: StructArray, start: number, length: number, feature: Feature): void; @@ -57,21 +83,23 @@ interface Binder { setUniforms(gl: WebGLRenderingContext, program: Program, - layer: StyleLayer, - globalProperties: { zoom: number }): void; + globals: GlobalProperties, + currentValue: PossiblyEvaluatedPropertyValue): void; } -class ConstantBinder implements Binder { +class ConstantBinder implements Binder { + value: T; name: string; type: string; property: string; - useIntegerZoom: boolean; + statistics: { max: number }; - constructor(name: string, type: string, property: string, useIntegerZoom: boolean) { + constructor(value: T, name: string, type: string, property: string) { + this.value = value; this.name = name; this.type = type; this.property = property; - this.useIntegerZoom = useIntegerZoom; + this.statistics = { max: -Infinity }; } defines() { @@ -80,8 +108,11 @@ class ConstantBinder implements Binder { populatePaintArray() {} - setUniforms(gl: WebGLRenderingContext, program: Program, layer: StyleLayer, {zoom}: { zoom: number }) { - const value = layer.getPaintValue(this.property, { zoom: this.useIntegerZoom ? Math.floor(zoom) : zoom }); + setUniforms(gl: WebGLRenderingContext, + program: Program, + globals: GlobalProperties, + currentValue: PossiblyEvaluatedPropertyValue) { + const value: any = currentValue.constantOr(this.value); if (this.type === 'color') { gl.uniform4f(program.uniforms[`u_${this.name}`], value.r, value.g, value.b, value.a); } else { @@ -90,28 +121,30 @@ class ConstantBinder implements Binder { } } -class SourceFunctionBinder implements Binder { +class SourceExpressionBinder implements Binder { + expression: SourceExpression; name: string; type: string; property: string; + statistics: { max: number }; - constructor(name: string, type: string, property: string) { + constructor(expression: SourceExpression, name: string, type: string, property: string) { + this.expression = expression; this.name = name; this.type = type; this.property = property; + this.statistics = { max: -Infinity }; } defines() { return []; } - populatePaintArray(layer: StyleLayer, - paintArray: StructArray, - statistics: PaintPropertyStatistics, + populatePaintArray(paintArray: StructArray, start: number, length: number, feature: Feature) { - const value = layer.getPaintValue(this.property, {zoom: 0}, feature); + const value = this.expression.evaluate({zoom: 0}, feature); if (this.type === 'color') { const color = packColor(value); @@ -126,8 +159,7 @@ class SourceFunctionBinder implements Binder { struct[`a_${this.name}`] = value; } - const stats = statistics[this.property]; - stats.max = Math.max(stats.max, value); + this.statistics.max = Math.max(this.statistics.max, value); } } @@ -136,33 +168,35 @@ class SourceFunctionBinder implements Binder { } } -class CompositeFunctionBinder implements Binder { +class CompositeExpressionBinder implements Binder { + expression: CompositeExpression; name: string; type: string; property: string; useIntegerZoom: boolean; zoom: number; + statistics: { max: number }; - constructor(name: string, type: string, property: string, useIntegerZoom: boolean, zoom: number) { + constructor(expression: CompositeExpression, name: string, type: string, property: string, useIntegerZoom: boolean, zoom: number) { + this.expression = expression; this.name = name; this.type = type; this.property = property; this.useIntegerZoom = useIntegerZoom; this.zoom = zoom; + this.statistics = { max: -Infinity }; } defines() { return []; } - populatePaintArray(layer: StyleLayer, - paintArray: StructArray, - statistics: PaintPropertyStatistics, + populatePaintArray(paintArray: StructArray, start: number, length: number, feature: Feature) { - const min = layer.getPaintValue(this.property, {zoom: this.zoom }, feature); - const max = layer.getPaintValue(this.property, {zoom: this.zoom + 1}, feature); + const min = this.expression.evaluate({zoom: this.zoom }, feature); + const max = this.expression.evaluate({zoom: this.zoom + 1}, feature); if (this.type === 'color') { const minColor = packColor(min); @@ -181,14 +215,20 @@ class CompositeFunctionBinder implements Binder { struct[`a_${this.name}1`] = max; } - const stats = statistics[this.property]; - stats.max = Math.max(stats.max, min, max); + this.statistics.max = Math.max(this.statistics.max, min, max); } } - setUniforms(gl: WebGLRenderingContext, program: Program, layer: StyleLayer, {zoom}: { zoom: number }) { - const f = layer.getPaintInterpolationFactor(this.property, this.useIntegerZoom ? Math.floor(zoom) : zoom, this.zoom, this.zoom + 1); - gl.uniform1f(program.uniforms[`a_${this.name}_t`], f); + interpolationFactor(currentZoom: number) { + if (this.useIntegerZoom) { + return this.expression.interpolationFactor(Math.floor(currentZoom), this.zoom, this.zoom + 1); + } else { + return this.expression.interpolationFactor(currentZoom, this.zoom, this.zoom + 1); + } + } + + setUniforms(gl: WebGLRenderingContext, program: Program, globals: GlobalProperties) { + gl.uniform1f(program.uniforms[`a_${this.name}_t`], this.interpolationFactor(globals.zoom)); } } @@ -219,7 +259,7 @@ export type SerializedProgramConfiguration = { * @private */ class ProgramConfiguration { - binders: { [string]: Binder }; + binders: { [string]: Binder }; cacheKey: string; interface: ?ProgramInterface; PaintVertexArray: Class; @@ -240,15 +280,16 @@ class ProgramConfiguration { for (const attribute of programInterface.paintAttributes || []) { const property = attribute.property; - const useIntegerZoom = attribute.useIntegerZoom || false; const name = attribute.name || property.replace(`${layer.type}-`, '').replace(/-/g, '_'); - const type = layer._paintSpecifications[property].type; + const value: PossiblyEvaluatedPropertyValue = (layer.paint: any).get(property); + const type = value.property.specification.type; + const useIntegerZoom = value.property.useIntegerZoom; - if (layer.isPaintValueFeatureConstant(property)) { - self.binders[name] = new ConstantBinder(name, type, property, useIntegerZoom); + if (value.value.kind === 'constant') { + self.binders[name] = new ConstantBinder(value.value, name, type, property); self.cacheKey += `/u_${name}`; - } else if (layer.isPaintValueZoomConstant(property)) { - self.binders[name] = new SourceFunctionBinder(name, type, property); + } else if (value.value.kind === 'source') { + self.binders[name] = new SourceExpressionBinder(value.value, name, type, property); self.cacheKey += `/a_${name}`; attributes.push({ name: `a_${name}`, @@ -256,7 +297,7 @@ class ProgramConfiguration { components: type === 'color' ? 2 : 1 }); } else { - self.binders[name] = new CompositeFunctionBinder(name, type, property, useIntegerZoom, zoom); + self.binders[name] = new CompositeExpressionBinder(value.value, name, type, property, useIntegerZoom, zoom); self.cacheKey += `/z_${name}`; attributes.push({ name: `a_${name}`, @@ -273,29 +314,25 @@ class ProgramConfiguration { return self; } - static createBasicFill() { + static forBackgroundColor(color: Color, opacity: number) { const self = new ProgramConfiguration(); - self.binders.color = new ConstantBinder('color', 'color', 'fill-color', false); + self.binders.color = new ConstantBinder(color, 'color', 'color', 'background-color'); self.cacheKey += `/u_color`; - self.binders.opacity = new ConstantBinder('opacity', 'number', 'fill-opacity', false); + self.binders.opacity = new ConstantBinder(opacity, 'opacity', 'number', 'background-opacity'); self.cacheKey += `/u_opacity`; return self; } - // Since this object is accessed frequently during populatePaintArray, it - // is helpful to initialize it ahead of time to avoid recalculating - // 'hidden class' optimizations to take effect - createPaintPropertyStatistics() { - const paintPropertyStatistics: PaintPropertyStatistics = {}; - for (const name in this.binders) { - paintPropertyStatistics[this.binders[name].property] = { - max: -Infinity - }; - } - return paintPropertyStatistics; + static forBackgroundPattern(opacity: number) { + const self = new ProgramConfiguration(); + + self.binders.opacity = new ConstantBinder(opacity, 'opacity', 'number', 'background-opacity'); + self.cacheKey += `/u_opacity`; + + return self; } populatePaintArray(length: number, feature: Feature) { @@ -307,8 +344,7 @@ class ProgramConfiguration { for (const name in this.binders) { this.binders[name].populatePaintArray( - this.layer, paintArray, - this.paintPropertyStatistics, + paintArray, start, length, feature); } @@ -322,9 +358,10 @@ class ProgramConfiguration { return result; } - setUniforms(gl: WebGLRenderingContext, program: Program, layer: StyleLayer, globalProperties: { zoom: number }) { + setUniforms(gl: WebGLRenderingContext, program: Program, properties: PossiblyEvaluated, globals: GlobalProperties) { for (const name in this.binders) { - this.binders[name].setUniforms(gl, program, layer, globalProperties); + const binder = this.binders[name]; + binder.setUniforms(gl, program, globals, properties.get(binder.property)); } } @@ -332,10 +369,16 @@ class ProgramConfiguration { if (this.paintVertexArray.length === 0) { return null; } + + const statistics: PaintPropertyStatistics = {}; + for (const name in this.binders) { + statistics[this.binders[name].property] = this.binders[name].statistics; + } + return { array: this.paintVertexArray.serialize(transferables), type: this.paintVertexArray.constructor.serialize(), - statistics: this.paintPropertyStatistics + statistics }; } @@ -375,7 +418,6 @@ class ProgramConfigurationSet { for (const layer of layers) { const programConfiguration = ProgramConfiguration.createDynamic(programInterface, layer, zoom); programConfiguration.paintVertexArray = new programConfiguration.PaintVertexArray(); - programConfiguration.paintPropertyStatistics = programConfiguration.createPaintPropertyStatistics(); this.programConfigurations[layer.id] = programConfiguration; } } diff --git a/src/render/draw_background.js b/src/render/draw_background.js index 6eb52573c82..fbd06f4f076 100644 --- a/src/render/draw_background.js +++ b/src/render/draw_background.js @@ -1,23 +1,27 @@ // @flow const pattern = require('./pattern'); +const {ProgramConfiguration} = require('../data/program_configuration'); +const {PossiblyEvaluated, PossiblyEvaluatedPropertyValue} = require('../style/properties'); +const fillLayerPaintProperties = require('../style/style_layer/fill_style_layer_properties').paint; import type Painter from './painter'; import type SourceCache from '../source/source_cache'; import type BackgroundStyleLayer from '../style/style_layer/background_style_layer'; -import type Color from '../style-spec/util/color'; module.exports = drawBackground; function drawBackground(painter: Painter, sourceCache: SourceCache, layer: BackgroundStyleLayer) { - if (layer.isOpacityZero(painter.transform.zoom)) return; + const color = layer.paint.get('background-color'); + const opacity = layer.paint.get('background-opacity'); + + if (opacity === 0) return; const gl = painter.gl; const transform = painter.transform; const tileSize = transform.tileSize; - const color: Color = layer.paint['background-color']; - const image = layer.paint['background-pattern']; - const opacity = layer.paint['background-opacity']; + const image = layer.paint.get('background-pattern'); + const globals = {zoom: transform.zoom}; const pass = (!image && color.a === 1 && opacity === 1) ? 'opaque' : 'translucent'; if (painter.renderPass !== pass) return; @@ -26,20 +30,28 @@ function drawBackground(painter: Painter, sourceCache: SourceCache, layer: Backg painter.setDepthSublayer(0); + const properties = new PossiblyEvaluated(fillLayerPaintProperties); + + (properties._values: any)['background-color'] = new PossiblyEvaluatedPropertyValue( + fillLayerPaintProperties.properties['fill-color'], {kind: 'constant', value: color}, globals); + (properties._values: any)['background-opacity'] = new PossiblyEvaluatedPropertyValue( + fillLayerPaintProperties.properties['fill-opacity'], {kind: 'constant', value: opacity}, globals); + let program; if (image) { if (pattern.isPatternMissing(image, painter)) return; - program = painter.useProgram('fillPattern', painter.basicFillProgramConfiguration); + const configuration = ProgramConfiguration.forBackgroundPattern(opacity); + program = painter.useProgram('fillPattern', configuration); + configuration.setUniforms(gl, program, properties, globals); pattern.prepare(image, painter, program); painter.tileExtentPatternVAO.bind(gl, program, painter.tileExtentBuffer); } else { - program = painter.useProgram('fill', painter.basicFillProgramConfiguration); - gl.uniform4f(program.uniforms.u_color, color.r, color.g, color.b, color.a); + const configuration = ProgramConfiguration.forBackgroundColor(color, opacity); + program = painter.useProgram('fill', configuration); + configuration.setUniforms(gl, program, properties, globals); painter.tileExtentVAO.bind(gl, program, painter.tileExtentBuffer); } - gl.uniform1f(program.uniforms.u_opacity, opacity); - const coords = transform.coveringTiles({tileSize}); for (const coord of coords) { diff --git a/src/render/draw_circle.js b/src/render/draw_circle.js index a8223ffc8a9..28379470009 100644 --- a/src/render/draw_circle.js +++ b/src/render/draw_circle.js @@ -12,7 +12,14 @@ module.exports = drawCircles; function drawCircles(painter: Painter, sourceCache: SourceCache, layer: CircleStyleLayer, coords: Array) { if (painter.renderPass !== 'translucent') return; - if (layer.isOpacityZero(painter.transform.zoom)) return; + + const opacity = layer.paint.get('circle-opacity'); + const strokeWidth = layer.paint.get('circle-stroke-width'); + const strokeOpacity = layer.paint.get('circle-stroke-opacity'); + + if (opacity.constantOr(1) === 0 && (strokeWidth.constantOr(1) === 0 || strokeOpacity.constantOr(1) === 0)) { + return; + } const gl = painter.gl; @@ -32,11 +39,11 @@ function drawCircles(painter: Painter, sourceCache: SourceCache, layer: CircleSt const programConfiguration = bucket.programConfigurations.get(layer.id); const program = painter.useProgram('circle', programConfiguration); - programConfiguration.setUniforms(gl, program, layer, {zoom: painter.transform.zoom}); + programConfiguration.setUniforms(gl, program, layer.paint, {zoom: painter.transform.zoom}); gl.uniform1f(program.uniforms.u_camera_to_center_distance, painter.transform.cameraToCenterDistance); - gl.uniform1i(program.uniforms.u_scale_with_map, layer.paint['circle-pitch-scale'] === 'map' ? 1 : 0); - if (layer.paint['circle-pitch-alignment'] === 'map') { + gl.uniform1i(program.uniforms.u_scale_with_map, layer.paint.get('circle-pitch-scale') === 'map' ? 1 : 0); + if (layer.paint.get('circle-pitch-alignment') === 'map') { gl.uniform1i(program.uniforms.u_pitch_with_map, 1); const pixelRatio = pixelsToTileUnits(tile, 1, painter.transform.zoom); gl.uniform2f(program.uniforms.u_extrude_scale, pixelRatio, pixelRatio); @@ -48,8 +55,8 @@ function drawCircles(painter: Painter, sourceCache: SourceCache, layer: CircleSt gl.uniformMatrix4fv(program.uniforms.u_matrix, false, painter.translatePosMatrix( coord.posMatrix, tile, - layer.paint['circle-translate'], - layer.paint['circle-translate-anchor'] + layer.paint.get('circle-translate'), + layer.paint.get('circle-translate-anchor') )); program.draw( diff --git a/src/render/draw_fill.js b/src/render/draw_fill.js index d6f9d79548a..b673f502701 100644 --- a/src/render/draw_fill.js +++ b/src/render/draw_fill.js @@ -1,26 +1,31 @@ // @flow const pattern = require('./pattern'); +const Color = require('../style-spec/util/color'); import type Painter from './painter'; import type SourceCache from '../source/source_cache'; import type FillStyleLayer from '../style/style_layer/fill_style_layer'; import type FillBucket from '../data/bucket/fill_bucket'; import type TileCoord from '../source/tile_coord'; +import type {CrossFaded} from '../style/cross_faded'; module.exports = drawFill; function drawFill(painter: Painter, sourceCache: SourceCache, layer: FillStyleLayer, coords: Array) { - if (layer.isOpacityZero(painter.transform.zoom)) return; + const color = layer.paint.get('fill-color'); + const opacity = layer.paint.get('fill-opacity'); + + if (opacity.constantOr(1) === 0) { + return; + } const gl = painter.gl; gl.enable(gl.STENCIL_TEST); - const pass = (!layer.paint['fill-pattern'] && - layer.isPaintValueFeatureConstant('fill-color') && - layer.isPaintValueFeatureConstant('fill-opacity') && - layer.paint['fill-color'].a === 1 && - layer.paint['fill-opacity'] === 1) ? 'opaque' : 'translucent'; + const pass = (!layer.paint.get('fill-pattern') && + color.constantOr(Color.transparent).a === 1 && + opacity.constantOr(0) === 1) ? 'opaque' : 'translucent'; // Draw fill if (painter.renderPass === pass) { @@ -32,7 +37,7 @@ function drawFill(painter: Painter, sourceCache: SourceCache, layer: FillStyleLa } // Draw stroke - if (painter.renderPass === 'translucent' && layer.paint['fill-antialias']) { + if (painter.renderPass === 'translucent' && layer.paint.get('fill-antialias')) { painter.lineWidth(2); painter.depthMask(false); @@ -50,7 +55,7 @@ function drawFill(painter: Painter, sourceCache: SourceCache, layer: FillStyleLa } function drawFillTiles(painter, sourceCache, layer, coords, drawFn) { - if (pattern.isPatternMissing(layer.paint['fill-pattern'], painter)) return; + if (pattern.isPatternMissing(layer.paint.get('fill-pattern'), painter)) return; let firstTile = true; for (const coord of coords) { @@ -68,7 +73,7 @@ function drawFillTile(painter, sourceCache, layer, tile, coord, bucket, firstTil const gl = painter.gl; const programConfiguration = bucket.programConfigurations.get(layer.id); - const program = setFillProgram('fill', layer.paint['fill-pattern'], painter, programConfiguration, layer, tile, coord, firstTile); + const program = setFillProgram('fill', layer.paint.get('fill-pattern'), painter, programConfiguration, layer, tile, coord, firstTile); program.draw( gl, @@ -83,9 +88,9 @@ function drawFillTile(painter, sourceCache, layer, tile, coord, bucket, firstTil function drawStrokeTile(painter, sourceCache, layer, tile, coord, bucket, firstTile) { const gl = painter.gl; const programConfiguration = bucket.programConfigurations.get(layer.id); - const usePattern = layer.paint['fill-pattern'] && !layer.getPaintProperty('fill-outline-color'); + const pattern = layer.getPaintProperty('fill-outline-color') ? null : layer.paint.get('fill-pattern'); - const program = setFillProgram('fillOutline', usePattern, painter, programConfiguration, layer, tile, coord, firstTile); + const program = setFillProgram('fillOutline', pattern, painter, programConfiguration, layer, tile, coord, firstTile); gl.uniform2f(program.uniforms.u_world, gl.drawingBufferWidth, gl.drawingBufferHeight); program.draw( @@ -98,26 +103,26 @@ function drawStrokeTile(painter, sourceCache, layer, tile, coord, bucket, firstT programConfiguration); } -function setFillProgram(programId, usePattern, painter, programConfiguration, layer, tile, coord, firstTile) { +function setFillProgram(programId, pat: ?CrossFaded, painter, programConfiguration, layer, tile, coord, firstTile) { let program; const prevProgram = painter.currentProgram; - if (!usePattern) { + if (!pat) { program = painter.useProgram(programId, programConfiguration); if (firstTile || program !== prevProgram) { - programConfiguration.setUniforms(painter.gl, program, layer, {zoom: painter.transform.zoom}); + programConfiguration.setUniforms(painter.gl, program, layer.paint, {zoom: painter.transform.zoom}); } } else { program = painter.useProgram(`${programId}Pattern`, programConfiguration); if (firstTile || program !== prevProgram) { - programConfiguration.setUniforms(painter.gl, program, layer, {zoom: painter.transform.zoom}); - pattern.prepare(layer.paint['fill-pattern'], painter, program); + programConfiguration.setUniforms(painter.gl, program, layer.paint, {zoom: painter.transform.zoom}); + pattern.prepare(pat, painter, program); } pattern.setTile(tile, painter, program); } painter.gl.uniformMatrix4fv(program.uniforms.u_matrix, false, painter.translatePosMatrix( coord.posMatrix, tile, - layer.paint['fill-translate'], - layer.paint['fill-translate-anchor'] + layer.paint.get('fill-translate'), + layer.paint.get('fill-translate-anchor') )); return program; } diff --git a/src/render/draw_fill_extrusion.js b/src/render/draw_fill_extrusion.js index 502172ff679..2c763034ee6 100644 --- a/src/render/draw_fill_extrusion.js +++ b/src/render/draw_fill_extrusion.js @@ -15,7 +15,9 @@ import type TileCoord from '../source/tile_coord'; module.exports = draw; function draw(painter: Painter, source: SourceCache, layer: FillExtrusionStyleLayer, coords: Array) { - if (layer.isOpacityZero(painter.transform.zoom)) return; + if (layer.paint.get('fill-extrusion-opacity') === 0) { + return; + } if (painter.renderPass === '3d') { const gl = painter.gl; @@ -47,7 +49,7 @@ function drawExtrusionTexture(painter, layer) { gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, renderedTexture.texture); - gl.uniform1f(program.uniforms.u_opacity, layer.paint['fill-extrusion-opacity']); + gl.uniform1f(program.uniforms.u_opacity, layer.paint.get('fill-extrusion-opacity')); gl.uniform1i(program.uniforms.u_image, 0); const matrix = mat4.create(); @@ -67,11 +69,11 @@ function drawExtrusion(painter, source, layer, coord) { const gl = painter.gl; - const image = layer.paint['fill-extrusion-pattern']; + const image = layer.paint.get('fill-extrusion-pattern'); const programConfiguration = bucket.programConfigurations.get(layer.id); const program = painter.useProgram(image ? 'fillExtrusionPattern' : 'fillExtrusion', programConfiguration); - programConfiguration.setUniforms(gl, program, layer, {zoom: painter.transform.zoom}); + programConfiguration.setUniforms(gl, program, layer.paint, {zoom: painter.transform.zoom}); if (image) { if (pattern.isPatternMissing(image, painter)) return; @@ -83,8 +85,8 @@ function drawExtrusion(painter, source, layer, coord) { painter.gl.uniformMatrix4fv(program.uniforms.u_matrix, false, painter.translatePosMatrix( coord.posMatrix, tile, - layer.paint['fill-extrusion-translate'], - layer.paint['fill-extrusion-translate-anchor'] + layer.paint.get('fill-extrusion-translate'), + layer.paint.get('fill-extrusion-translate-anchor') )); setLight(program, painter); @@ -103,13 +105,18 @@ function setLight(program, painter) { const gl = painter.gl; const light = painter.style.light; - const _lp = light.calculated.position, - lightPos = [_lp.x, _lp.y, _lp.z]; + const _lp = light.properties.get('position'); + const lightPos = [_lp.x, _lp.y, _lp.z]; + const lightMat = mat3.create(); - if (light.calculated.anchor === 'viewport') mat3.fromRotation(lightMat, -painter.transform.angle); + if (light.properties.get('anchor') === 'viewport') { + mat3.fromRotation(lightMat, -painter.transform.angle); + } vec3.transformMat3(lightPos, lightPos, lightMat); + const color = light.properties.get('color'); + gl.uniform3fv(program.uniforms.u_lightpos, lightPos); - gl.uniform1f(program.uniforms.u_lightintensity, light.calculated.intensity); - gl.uniform3f(program.uniforms.u_lightcolor, light.calculated.color.r, light.calculated.color.g, light.calculated.color.b); + gl.uniform1f(program.uniforms.u_lightintensity, light.properties.get('intensity')); + gl.uniform3f(program.uniforms.u_lightcolor, color.r, color.g, color.b); } diff --git a/src/render/draw_heatmap.js b/src/render/draw_heatmap.js index 3e7309c1836..aab25f197de 100644 --- a/src/render/draw_heatmap.js +++ b/src/render/draw_heatmap.js @@ -14,7 +14,9 @@ module.exports = drawHeatmap; function drawHeatmap(painter: Painter, sourceCache: SourceCache, layer: HeatmapStyleLayer, coords: Array) { if (painter.isOpaquePass) return; - if (layer.isOpacityZero(painter.transform.zoom)) return; + if (layer.paint.get('heatmap-opacity') === 0) { + return; + } const gl = painter.gl; @@ -48,12 +50,12 @@ function drawHeatmap(painter: Painter, sourceCache: SourceCache, layer: HeatmapS const programConfiguration = bucket.programConfigurations.get(layer.id); const program = painter.useProgram('heatmap', programConfiguration); const {zoom} = painter.transform; - programConfiguration.setUniforms(gl, program, layer, {zoom}); - gl.uniform1f(program.uniforms.u_radius, layer.getPaintValue('heatmap-radius', {zoom})); + programConfiguration.setUniforms(gl, program, layer.paint, {zoom}); + gl.uniform1f(program.uniforms.u_radius, layer.paint.get('heatmap-radius')); gl.uniform1f(program.uniforms.u_extrude_scale, pixelsToTileUnits(tile, 1, zoom)); - gl.uniform1f(program.uniforms.u_intensity, layer.getPaintValue('heatmap-intensity', {zoom})); + gl.uniform1f(program.uniforms.u_intensity, layer.paint.get('heatmap-intensity')); gl.uniformMatrix4fv(program.uniforms.u_matrix, false, coord.posMatrix); program.draw( @@ -130,7 +132,7 @@ function renderTextureToMap(gl, painter, layer) { gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, layer.heatmapTexture); - const opacity = layer.getPaintValue('heatmap-opacity', {zoom: painter.transform.zoom}); + const opacity = layer.paint.get('heatmap-opacity'); gl.uniform1f(program.uniforms.u_opacity, opacity); gl.uniform1i(program.uniforms.u_image, 1); gl.uniform1i(program.uniforms.u_color_ramp, 2); diff --git a/src/render/draw_line.js b/src/render/draw_line.js index ab9ec3bc79d..0e066df3058 100644 --- a/src/render/draw_line.js +++ b/src/render/draw_line.js @@ -11,19 +11,19 @@ import type TileCoord from '../source/tile_coord'; module.exports = function drawLine(painter: Painter, sourceCache: SourceCache, layer: LineStyleLayer, coords: Array) { if (painter.renderPass !== 'translucent') return; - if (layer.isOpacityZero(painter.transform.zoom)) return; + + const opacity = layer.paint.get('line-opacity'); + if (opacity.constantOr(1) === 0) return; + painter.setDepthSublayer(0); painter.depthMask(false); const gl = painter.gl; gl.enable(gl.STENCIL_TEST); - // don't draw zero-width lines - if (layer.paint['line-width'] <= 0) return; - const programId = - layer.paint['line-dasharray'] ? 'lineSDF' : - layer.paint['line-pattern'] ? 'linePattern' : 'line'; + layer.paint.get('line-dasharray') ? 'lineSDF' : + layer.paint.get('line-pattern') ? 'linePattern' : 'line'; let prevTileZoom; let firstTile = true; @@ -40,7 +40,7 @@ module.exports = function drawLine(painter: Painter, sourceCache: SourceCache, l const tileRatioChanged = prevTileZoom !== tile.coord.z; if (programChanged) { - programConfiguration.setUniforms(painter.gl, program, layer, {zoom: painter.transform.zoom}); + programConfiguration.setUniforms(painter.gl, program, layer.paint, {zoom: painter.transform.zoom}); } drawLineTile(program, painter, tile, bucket, layer, coord, programConfiguration, programChanged, tileRatioChanged); prevTileZoom = tile.coord.z; @@ -50,8 +50,8 @@ module.exports = function drawLine(painter: Painter, sourceCache: SourceCache, l function drawLineTile(program, painter, tile, bucket, layer, coord, programConfiguration, programChanged, tileRatioChanged) { const gl = painter.gl; - const dasharray = layer.paint['line-dasharray']; - const image = layer.paint['line-pattern']; + const dasharray = layer.paint.get('line-dasharray'); + const image = layer.paint.get('line-pattern'); let posA, posB, imagePosA, imagePosB; @@ -59,8 +59,8 @@ function drawLineTile(program, painter, tile, bucket, layer, coord, programConfi const tileRatio = 1 / pixelsToTileUnits(tile, 1, painter.transform.tileZoom); if (dasharray) { - posA = painter.lineAtlas.getDash(dasharray.from, layer.layout['line-cap'] === 'round'); - posB = painter.lineAtlas.getDash(dasharray.to, layer.layout['line-cap'] === 'round'); + posA = painter.lineAtlas.getDash(dasharray.from, layer.layout.get('line-cap') === 'round'); + posB = painter.lineAtlas.getDash(dasharray.to, layer.layout.get('line-cap') === 'round'); const widthA = posA.width * dasharray.fromScale; const widthB = posB.width * dasharray.toScale; @@ -110,7 +110,7 @@ function drawLineTile(program, painter, tile, bucket, layer, coord, programConfi painter.enableTileClippingMask(coord); - const posMatrix = painter.translatePosMatrix(coord.posMatrix, tile, layer.paint['line-translate'], layer.paint['line-translate-anchor']); + const posMatrix = painter.translatePosMatrix(coord.posMatrix, tile, layer.paint.get('line-translate'), layer.paint.get('line-translate-anchor')); gl.uniformMatrix4fv(program.uniforms.u_matrix, false, posMatrix); gl.uniform1f(program.uniforms.u_ratio, 1 / pixelsToTileUnits(tile, 1, painter.transform.zoom)); diff --git a/src/render/draw_raster.js b/src/render/draw_raster.js index 14ef5d8371a..0869429c607 100644 --- a/src/render/draw_raster.js +++ b/src/render/draw_raster.js @@ -11,27 +11,26 @@ import type TileCoord from '../source/tile_coord'; module.exports = drawRaster; function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterStyleLayer, coords: Array) { - const zoom = painter.transform.zoom; if (painter.renderPass !== 'translucent') return; - if (layer.isOpacityZero(zoom)) return; + if (layer.paint.get('raster-opacity') === 0) return; const gl = painter.gl; const source = sourceCache.getSource(); const program = painter.useProgram('raster'); gl.enable(gl.DEPTH_TEST); - painter.depthMask(layer.getPaintValue('raster-opacity', {zoom: zoom}) === 1); + painter.depthMask(layer.paint.get('raster-opacity') === 1); // Change depth function to prevent double drawing in areas where tiles overlap. gl.depthFunc(gl.LESS); gl.disable(gl.STENCIL_TEST); // Constant parameters. - gl.uniform1f(program.uniforms.u_brightness_low, layer.getPaintValue('raster-brightness-min', {zoom: zoom})); - gl.uniform1f(program.uniforms.u_brightness_high, layer.getPaintValue('raster-brightness-max', {zoom: zoom})); - gl.uniform1f(program.uniforms.u_saturation_factor, saturationFactor(layer.getPaintValue('raster-saturation', {zoom: zoom}))); - gl.uniform1f(program.uniforms.u_contrast_factor, contrastFactor(layer.getPaintValue('raster-contrast', {zoom: zoom}))); - gl.uniform3fv(program.uniforms.u_spin_weights, spinWeights(layer.getPaintValue('raster-hue-rotate', {zoom: zoom}))); + gl.uniform1f(program.uniforms.u_brightness_low, layer.paint.get('raster-brightness-min')); + gl.uniform1f(program.uniforms.u_brightness_high, layer.paint.get('raster-brightness-max')); + gl.uniform1f(program.uniforms.u_saturation_factor, saturationFactor(layer.paint.get('raster-saturation'))); + gl.uniform1f(program.uniforms.u_contrast_factor, contrastFactor(layer.paint.get('raster-contrast'))); + gl.uniform3fv(program.uniforms.u_spin_weights, spinWeights(layer.paint.get('raster-hue-rotate'))); gl.uniform1f(program.uniforms.u_buffer_scale, 1); gl.uniform1i(program.uniforms.u_image0, 0); gl.uniform1i(program.uniforms.u_image1, 1); @@ -45,7 +44,7 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty const tile = sourceCache.getTile(coord); const posMatrix = painter.transform.calculatePosMatrix(coord, sourceCache.getSource().maxzoom); - tile.registerFadeDuration(painter.style.animationLoop, layer.getPaintValue('raster-fade-duration', {zoom: zoom})); + tile.registerFadeDuration(layer.paint.get('raster-fade-duration')); gl.uniformMatrix4fv(program.uniforms.u_matrix, false, posMatrix); @@ -72,7 +71,7 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty gl.uniform2fv(program.uniforms.u_tl_parent, parentTL || [0, 0]); gl.uniform1f(program.uniforms.u_scale_parent, parentScaleBy || 1); gl.uniform1f(program.uniforms.u_fade_t, fade.mix); - gl.uniform1f(program.uniforms.u_opacity, fade.opacity * layer.getPaintValue('raster-opacity', {zoom: zoom})); + gl.uniform1f(program.uniforms.u_opacity, fade.opacity * layer.paint.get('raster-opacity')); if (source instanceof ImageSource) { @@ -124,7 +123,7 @@ function saturationFactor(saturation) { } function getFadeValues(tile, parentTile, sourceCache, layer, transform) { - const fadeDuration = layer.getPaintValue('raster-fade-duration', {zoom: transform.zoom}); + const fadeDuration = layer.paint.get('raster-fade-duration'); if (fadeDuration > 0) { const now = Date.now(); diff --git a/src/render/draw_symbol.js b/src/render/draw_symbol.js index d1333041250..0908efd684a 100644 --- a/src/render/draw_symbol.js +++ b/src/render/draw_symbol.js @@ -6,6 +6,7 @@ const symbolProjection = require('../symbol/projection'); const symbolSize = require('../symbol/symbol_size'); const mat4 = require('@mapbox/gl-matrix').mat4; const identityMat4 = mat4.identity(new Float32Array(16)); +const symbolLayoutProperties = require('../style/style_layer/symbol_style_layer_properties').layout; import type Painter from './painter'; import type SourceCache from '../source/source_cache'; @@ -26,23 +27,23 @@ function drawSymbols(painter: Painter, sourceCache: SourceCache, layer: SymbolSt painter.setDepthSublayer(0); painter.depthMask(false); - if (!layer.isOpacityZero(painter.transform.zoom, 'icon-opacity')) { + if (layer.paint.get('icon-opacity').constantOr(1) !== 0) { drawLayerSymbols(painter, sourceCache, layer, coords, false, - layer.paint['icon-translate'], - layer.paint['icon-translate-anchor'], - layer.layout['icon-rotation-alignment'], - layer.layout['icon-pitch-alignment'], - layer.layout['icon-keep-upright'] + layer.paint.get('icon-translate'), + layer.paint.get('icon-translate-anchor'), + layer.layout.get('icon-rotation-alignment'), + layer.layout.get('icon-pitch-alignment'), + layer.layout.get('icon-keep-upright') ); } - if (!layer.isOpacityZero(painter.transform.zoom, 'text-opacity')) { + if (layer.paint.get('text-opacity').constantOr(1) !== 0) { drawLayerSymbols(painter, sourceCache, layer, coords, true, - layer.paint['text-translate'], - layer.paint['text-translate-anchor'], - layer.layout['text-rotation-alignment'], - layer.layout['text-pitch-alignment'], - layer.layout['text-keep-upright'] + layer.paint.get('text-translate'), + layer.paint.get('text-translate-anchor'), + layer.layout.get('text-rotation-alignment'), + layer.layout.get('text-pitch-alignment'), + layer.layout.get('text-keep-upright') ); } @@ -59,7 +60,7 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate const rotateWithMap = rotationAlignment === 'map'; const pitchWithMap = pitchAlignment === 'map'; - const alongLine = rotateWithMap && layer.layout['symbol-placement'] === 'line'; + const alongLine = rotateWithMap && layer.layout.get('symbol-placement') === 'line'; // Line label rotation happens in `updateLineLabels` // Pitched point labels are automatically rotated by the labelPlaneMatrix projection // Unpitched point labels need to have their rotation applied after projection @@ -89,7 +90,7 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate if (!program) { program = painter.useProgram(isSDF ? 'symbolSDF' : 'symbolIcon', programConfiguration); - programConfiguration.setUniforms(gl, program, layer, {zoom: painter.transform.zoom}); + programConfiguration.setUniforms(gl, program, layer.paint, {zoom: painter.transform.zoom}); setSymbolDrawState(program, painter, layer, isText, rotateInShader, pitchWithMap, sizeData); } @@ -101,10 +102,7 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate tile.glyphAtlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); gl.uniform2fv(program.uniforms.u_texsize, tile.glyphAtlasTexture.size); } else { - const iconScaled = !layer.isLayoutValueFeatureConstant('icon-size') || - !layer.isLayoutValueZoomConstant('icon-size') || - layer.getLayoutValue('icon-size', { zoom: tr.zoom }) !== 1 || - bucket.iconsNeedLinear; + const iconScaled = layer.layout.get('icon-size').constantOr(0) !== 1 || bucket.iconsNeedLinear; const iconTransformed = pitchWithMap || tr.pitch !== 0; tile.iconAtlasTexture.bind(isSDF || painter.options.rotating || painter.options.zooming || iconScaled || iconTransformed ? @@ -123,7 +121,7 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate if (alongLine) { gl.uniformMatrix4fv(program.uniforms.u_label_plane_matrix, false, identityMat4); - symbolProjection.updateLineLabels(bucket, coord.posMatrix, painter, isText, labelPlaneMatrix, glCoordMatrix, pitchWithMap, keepUpright, s, layer); + symbolProjection.updateLineLabels(bucket, coord.posMatrix, painter, isText, labelPlaneMatrix, glCoordMatrix, pitchWithMap, keepUpright); } else { gl.uniformMatrix4fv(program.uniforms.u_label_plane_matrix, false, labelPlaneMatrix); } @@ -154,7 +152,7 @@ function setSymbolDrawState(program, painter, layer, isText, rotateInShader, pit gl.uniform1f(program.uniforms.u_camera_to_center_distance, tr.cameraToCenterDistance); - const size = symbolSize.evaluateSizeForZoom(sizeData, tr, layer, isText); + const size = symbolSize.evaluateSizeForZoom(sizeData, tr.zoom, symbolLayoutProperties.properties[isText ? 'text-size' : 'icon-size']); if (size.uSizeT !== undefined) gl.uniform1f(program.uniforms.u_size_t, size.uSizeT); if (size.uSize !== undefined) gl.uniform1f(program.uniforms.u_size, size.uSize); @@ -168,8 +166,7 @@ function drawTileSymbols(program, programConfiguration, painter, layer, tile, bu const tr = painter.transform; if (isSDF) { - const haloWidthProperty = `${isText ? 'text' : 'icon'}-halo-width`; - const hasHalo = !layer.isPaintValueFeatureConstant(haloWidthProperty) || layer.paint[haloWidthProperty]; + const hasHalo = layer.paint.get(isText ? 'text-halo-width' : 'icon-halo-width').constantOr(1) !== 0; const gammaScale = (pitchWithMap ? Math.cos(tr._pitch) * tr.cameraToCenterDistance : 1); gl.uniform1f(program.uniforms.u_gamma_scale, gammaScale); diff --git a/src/render/painter.js b/src/render/painter.js index 59090506cfb..5a682349608 100644 --- a/src/render/painter.js +++ b/src/render/painter.js @@ -62,7 +62,6 @@ class Painter { numSublayers: number; depthEpsilon: number; lineWidthRange: [number, number]; - basicFillProgramConfiguration: ProgramConfiguration; emptyProgramConfiguration: ProgramConfiguration; width: number; height: number; @@ -110,7 +109,6 @@ class Painter { this.lineWidthRange = gl.getParameter(gl.ALIASED_LINE_WIDTH_RANGE); - this.basicFillProgramConfiguration = ProgramConfiguration.createBasicFill(); this.emptyProgramConfiguration = new ProgramConfiguration(); this.crossTileSymbolIndex = new CrossTileSymbolIndex(); @@ -250,7 +248,7 @@ class Painter { gl.stencilFunc(gl.ALWAYS, id, 0xFF); - const program = this.useProgram('fill', this.basicFillProgramConfiguration); + const program = this.useProgram('fill', this.emptyProgramConfiguration); gl.uniformMatrix4fv(program.uniforms.u_matrix, false, coord.posMatrix); // Draw the clipping mask diff --git a/src/render/pattern.js b/src/render/pattern.js index 5d9edaf0e61..13a1a91cdbb 100644 --- a/src/render/pattern.js +++ b/src/render/pattern.js @@ -6,21 +6,14 @@ const pixelsToTileUnits = require('../source/pixels_to_tile_units'); import type Painter from './painter'; import type Program from './program'; import type TileCoord from '../source/tile_coord'; - -type CrossFaded = { - from: T, - to: T, - fromScale: number, - toScale: number, - t: number -}; +import type {CrossFaded} from '../style/cross_faded'; /** * Checks whether a pattern image is needed, and if it is, whether it is not loaded. * * @returns true if a needed image is missing and rendering needs to be skipped. */ -exports.isPatternMissing = function(image: CrossFaded, painter: Painter): boolean { +exports.isPatternMissing = function(image: ?CrossFaded, painter: Painter): boolean { if (!image) return false; const imagePosA = painter.imageManager.getPattern(image.from); const imagePosB = painter.imageManager.getPattern(image.to); diff --git a/src/shaders/README.md b/src/shaders/README.md index f87948e9d5c..266c8e987bc 100644 --- a/src/shaders/README.md +++ b/src/shaders/README.md @@ -37,8 +37,6 @@ When using pragmas, the following requirements apply. - `initialize` pragmas must be in function scope - all pragma-defined variables defined and initialized in the fragment shader must also be defined and initialized in the vertex shader because `attribute`s are not accessible from the fragment shader -To see concretely how pragmas are applied, you can use the `describe_shaders` utility to create a markdown file showing how a given style JSON is compiled into a collection of shaders. - ## Util The `util.glsl` file is automatically included in all shaders by the compiler. diff --git a/src/shaders/describe_shaders b/src/shaders/describe_shaders deleted file mode 100755 index 5e909e9a0a4..00000000000 --- a/src/shaders/describe_shaders +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -// Shows (and describes) the shaders that would be generated for the given -// style JSON -// Usage: describe_shaders path/to/style.json zoom - -require('flow-remove-types/register'); -const fs = require('fs'); -const deref = require('../style-spec/deref'); -const util = require('../util/util'); -const shaders = require('../render/shaders'); -const ProgramConfiguration = require('../data/program_configuration'); -const StyleLayer = require('../style/style_layer'); -const AnimationLoop = require('../style/animation_loop'); - -const fillProgramInterface = require('../data/bucket/fill_bucket').programInterface; -const circleProgramInterface = require('../data/bucket/circle_bucket').programInterface; -const lineProgramInterface = require('../data/bucket/line_bucket').programInterface; -const fillExtrusionProgramInterface = require('../data/bucket/fill_extrusion_bucket').programInterface; - -const programInterfaces = util.extend({ - fill: fillProgramInterface, - circle: circleProgramInterface, - line: lineProgramInterface, - fillExtrusion: fillExtrusionProgramInterface, -}, require('../data/bucket/symbol_bucket').programInterfaces); - -const style = JSON.parse(fs.readFileSync(process.argv[2])); -style.layers = deref(style.layers); -const zoom = parseFloat(process.argv[3]); - -const programs = {}; - -style.layers.forEach((layer) => { - const shader = layer.type; - const definition = shaders[shader]; - if (layer.type === 'symbol') { - addProgramInfo(layer, programInterfaces['glyph'], shaders['symbolSDF']); - // output both types of icon shaders, since the choice is made at runtime - addProgramInfo(layer, programInterfaces['icon'], shaders['symbolSDF']); - addProgramInfo(layer, programInterfaces['icon'], shaders['symbolIcon']); - } else { - const programInterface = programInterfaces[layer.type]; - if (programInterface) addProgramInfo(layer, programInterface, definition); - } -}); - -function addProgramInfo(layer, programInterface, shaderDefinition) { - const styleLayer = new StyleLayer(layer); - styleLayer.updatePaintTransitions([], {}, { zoom: zoom }, new AnimationLoop(), {}); - const configuration = ProgramConfiguration.createDynamic( - programInterface, styleLayer, zoom); - const key = `${layer.type}${configuration.cacheKey || ''}`; - const program = programs[key] = programs[key] || { layers: [] }; - program.layers.push(layer); - if (!program.shaderSource) { - program.shaderSource = createShaderSource(shaderDefinition, configuration); - } -} - -for (const key in programs) { - const layers = programs[key].layers; - const shaders = programs[key].shaderSource; - console.log(` -## ${key} - -### Layers -${layers.map(layer => `* ${layer.id}`).join('\n')} - -### Vertex Shader -\`\`\`glsl -${shaders.vertexSource} -\`\`\` - -### Fragment Shader -\`\`\`glsl -${shaders.fragmentSource} -\`\`\` -`); -} - - -function createShaderSource(definition, configuration) { - const definesSource = `#define MAPBOX_GL_JS\n#define DEVICE_PIXEL_RATIO \${browser.devicePixelRatio.toFixed(1)}\n`; - const vertexSource = configuration.applyPragmas(definesSource + shaders.prelude.vertexSource + definition.vertexSource, 'vertex'); - const fragmentSource = configuration.applyPragmas(definesSource + shaders.prelude.fragmentSource + definition.fragmentSource, 'fragment'); - return {vertexSource, fragmentSource}; -} diff --git a/src/source/canvas_source.js b/src/source/canvas_source.js index 32144cc39d4..da24acd55a7 100644 --- a/src/source/canvas_source.js +++ b/src/source/canvas_source.js @@ -45,6 +45,7 @@ class CanvasSource extends ImageSource { canvasData: ?ImageData; play: () => void; pause: () => void; + _playing: boolean; constructor(id: string, options: CanvasSourceSpecification, dispatcher: Dispatcher, eventedParent: Evented) { super(id, options, dispatcher, eventedParent); @@ -72,19 +73,13 @@ class CanvasSource extends ImageSource { this.height = this.canvas.height; if (this._hasInvalidDimensions()) return this.fire('error', new Error('Canvas dimensions cannot be less than or equal to zero.')); - let loopID; - this.play = function() { - if (loopID === undefined) { - loopID = this.map.style.animationLoop.set(Infinity); - this.map._rerender(); - } + this._playing = true; + this.map._rerender(); }; this.pause = function() { - if (loopID !== undefined) { - loopID = this.map.style.animationLoop.cancel(loopID); - } + this._playing = false; }; this._finishLoading(); @@ -151,6 +146,10 @@ class CanvasSource extends ImageSource { }; } + hasTransition() { + return this._playing; + } + _hasInvalidDimensions() { for (const x of [this.canvas.width, this.canvas.height]) { if (isNaN(x) || x <= 0) return true; diff --git a/src/source/geojson_source.js b/src/source/geojson_source.js index a20c993cf47..7613e454d46 100644 --- a/src/source/geojson_source.js +++ b/src/source/geojson_source.js @@ -238,6 +238,10 @@ class GeoJSONSource extends Evented implements Source { data: this._data }); } + + hasTransition() { + return false; + } } function resolveURL(url) { diff --git a/src/source/image_source.js b/src/source/image_source.js index 1a7a4a3b38e..c1646e4eb1b 100644 --- a/src/source/image_source.js +++ b/src/source/image_source.js @@ -243,6 +243,10 @@ class ImageSource extends Evented implements Source { coordinates: this.coordinates }; } + + hasTransition() { + return false; + } } module.exports = ImageSource; diff --git a/src/source/raster_tile_source.js b/src/source/raster_tile_source.js index 431367380e2..a310f2ce9e4 100644 --- a/src/source/raster_tile_source.js +++ b/src/source/raster_tile_source.js @@ -133,6 +133,10 @@ class RasterTileSource extends Evented implements Source { if (tile.texture) this.map.painter.saveTileTexture(tile.texture); callback(); } + + hasTransition() { + return false; + } } module.exports = RasterTileSource; diff --git a/src/source/source.js b/src/source/source.js index 28669696936..502f15c35d9 100644 --- a/src/source/source.js +++ b/src/source/source.js @@ -53,6 +53,8 @@ export interface Source { reparseOverscaled?: boolean, vectorLayerIds?: Array, + hasTransition(): boolean; + fire(type: string, data: Object): mixed; +onAdd?: (map: Map) => void; diff --git a/src/source/source_cache.js b/src/source/source_cache.js index 1e55d4d8cb3..1dc87769616 100644 --- a/src/source/source_cache.js +++ b/src/source/source_cache.js @@ -707,6 +707,23 @@ class SourceCache extends Evented { } return coords; } + + hasTransition() { + if (this._source.hasTransition()) { + return true; + } + + if (isRasterType(this._source.type)) { + for (const id in this._tiles) { + const tile = this._tiles[id]; + if (tile.fadeEndTime !== undefined && tile.fadeEndTime >= Date.now()) { + return true; + } + } + } + + return false; + } } SourceCache.maxOverzooming = 10; diff --git a/src/source/tile.js b/src/source/tile.js index ee8032845fe..c2434a36395 100644 --- a/src/source/tile.js +++ b/src/source/tile.js @@ -105,13 +105,12 @@ class Tile { this.state = 'loading'; } - registerFadeDuration(animationLoop: any, duration: number) { + registerFadeDuration(duration: number) { const fadeEndTime = duration + this.timeAdded; if (fadeEndTime < Date.now()) return; if (this.fadeEndTime && fadeEndTime < this.fadeEndTime) return; this.fadeEndTime = fadeEndTime; - animationLoop.set(this.fadeEndTime - Date.now()); } wasRequested() { @@ -212,7 +211,7 @@ class Tile { if (bucket && bucket instanceof SymbolBucket && collisionBoxArray) { const posMatrix = collisionIndex.transform.calculatePosMatrix(this.coord, this.sourceMaxZoom); - const pitchWithMap = bucket.layers[0].layout['text-pitch-alignment'] === 'map'; + const pitchWithMap = bucket.layers[0].layout.get('text-pitch-alignment') === 'map'; const textPixelRatio = EXTENT / this.tileSize; // text size is not meant to be affected by scale const pixelRatio = pixelsToTileUnits(this, 1, collisionIndex.transform.zoom); diff --git a/src/source/vector_tile_source.js b/src/source/vector_tile_source.js index 5568043c350..fa46011de98 100644 --- a/src/source/vector_tile_source.js +++ b/src/source/vector_tile_source.js @@ -141,6 +141,10 @@ class VectorTileSource extends Evented implements Source { tile.unloadVectorData(); this.dispatcher.send('removeTile', { uid: tile.uid, type: this.type, source: this.id }, undefined, tile.workerID); } + + hasTransition() { + return false; + } } module.exports = VectorTileSource; diff --git a/src/source/video_source.js b/src/source/video_source.js index 6c8caddfd49..d046f5d099d 100644 --- a/src/source/video_source.js +++ b/src/source/video_source.js @@ -63,19 +63,12 @@ class VideoSource extends ImageSource { this.video = video; this.video.loop = true; - let loopID; - - // start repainting when video starts playing + // Start repainting when video starts playing. hasTransition() will then return + // true to trigger additional frames as long as the videos continues playing. this.video.addEventListener('playing', () => { - loopID = this.map.style.animationLoop.set(Infinity); this.map._rerender(); }); - // stop repainting when video stops - this.video.addEventListener('pause', () => { - this.map.style.animationLoop.cancel(loopID); - }); - if (this.map) { this.video.play(); } @@ -130,6 +123,10 @@ class VideoSource extends ImageSource { coordinates: this.coordinates }; } + + hasTransition() { + return this.video && !this.video.paused; + } } module.exports = VideoSource; diff --git a/src/source/worker_tile.js b/src/source/worker_tile.js index 18a9a76fe66..fc2836f5f79 100644 --- a/src/source/worker_tile.js +++ b/src/source/worker_tile.js @@ -13,6 +13,7 @@ const {makeGlyphAtlas} = require('../render/glyph_atlas'); import type TileCoord from './tile_coord'; import type {Bucket} from '../data/bucket'; import type Actor from '../util/actor'; +import type StyleLayer from '../style/style_layer'; import type StyleLayerIndex from '../style/style_layer_index'; import type {StyleImage} from '../style/style_image'; import type {StyleGlyph} from '../style/style_glyph'; @@ -93,11 +94,9 @@ class WorkerTile { assert(layer.source === this.source); if (layer.minzoom && this.zoom < Math.floor(layer.minzoom)) continue; if (layer.maxzoom && this.zoom >= layer.maxzoom) continue; - if (layer.layout && layer.layout.visibility === 'none') continue; + if (layer.visibility === 'none') continue; - for (const layer of family) { - layer.recalculate(this.zoom); - } + recalculateLayers(family, this.zoom); const bucket = buckets[layer.id] = layer.createBucket({ index: featureIndex.bucketLayerIDs.length, @@ -155,7 +154,7 @@ class WorkerTile { for (const key in buckets) { const bucket = buckets[key]; if (bucket instanceof SymbolBucket) { - recalculateLayers(bucket, this.zoom); + recalculateLayers(bucket.layers, this.zoom); performSymbolLayout(bucket, glyphMap, glyphAtlas.positions, imageMap, imageAtlas.positions, this.showCollisionBoxes); } } @@ -179,10 +178,19 @@ class WorkerTile { } } -function recalculateLayers(bucket: SymbolBucket, zoom: number) { +function recalculateLayers(layers: $ReadOnlyArray, zoom: number) { // Layers are shared and may have been used by a WorkerTile with a different zoom. - for (const layer of bucket.layers) { - layer.recalculate(zoom); + for (const layer of layers) { + layer.recalculate({ + zoom, + now: Number.MAX_VALUE, + defaultFadeDuration: 0, + zoomHistory: { + lastIntegerZoom: 0, + lastIntegerZoomTime: 0, + lastZoom: 0 + } + }); } } diff --git a/src/style-spec/expression/index.js b/src/style-spec/expression/index.js index 8a6a29e90dc..446c072692c 100644 --- a/src/style-spec/expression/index.js +++ b/src/style-spec/expression/index.js @@ -103,24 +103,24 @@ function createExpression(expression: mixed, return success({ evaluate, parsed }); } -type ConstantExpression = { +export type ConstantExpression = { kind: 'constant', evaluate: (globals: GlobalProperties, feature?: Feature) => any }; -type SourceExpression = { +export type SourceExpression = { kind: 'source', evaluate: (globals: GlobalProperties, feature?: Feature) => any, }; -type CameraExpression = { +export type CameraExpression = { kind: 'camera', evaluate: (globals: GlobalProperties, feature?: Feature) => any, interpolationFactor: (input: number, lower: number, upper: number) => number, zoomStops: Array }; -type CompositeExpression = { +export type CompositeExpression = { kind: 'composite', evaluate: (globals: GlobalProperties, feature?: Feature) => any, interpolationFactor: (input: number, lower: number, upper: number) => number, @@ -178,10 +178,38 @@ function createPropertyExpression(expression: mixed, { kind: 'composite', parsed, evaluate, interpolationFactor, zoomStops }); } +const {isFunction, createFunction} = require('../function'); +const {Color} = require('./values'); + +function normalizePropertyExpression(value: PropertyValueSpecification, specification: StylePropertySpecification): StylePropertyExpression { + if (isFunction(value)) { + return createFunction(value, specification); + + } else if (isExpression(value)) { + const expression = createPropertyExpression(value, specification); + if (expression.result === 'error') { + // this should have been caught in validation + throw new Error(expression.value.map(err => `${err.key}: ${err.message}`).join(', ')); + } + return expression.value; + + } else { + let constant: any = value; + if (typeof value === 'string' && specification.type === 'color') { + constant = Color.parse(value); + } + return { + kind: 'constant', + evaluate: () => constant + }; + } +} + module.exports = { isExpression, createExpression, - createPropertyExpression + createPropertyExpression, + normalizePropertyExpression }; // Zoom-dependent expressions may only use ["zoom"] as the input to a top-level "step" or "interpolate" @@ -250,9 +278,6 @@ function getExpectedType(spec: StylePropertySpecification): Type | null { return types[spec.type] || null; } -const {isFunction} = require('../function'); -const {Color} = require('./values'); - function getDefaultValue(spec: StylePropertySpecification): Value { if (spec.type === 'color' && isFunction(spec.default)) { // Special case for heatmap-color: it uses the 'default:' to define a diff --git a/src/style-spec/util/color.js b/src/style-spec/util/color.js index 907284ae87c..3e19d2980a0 100644 --- a/src/style-spec/util/color.js +++ b/src/style-spec/util/color.js @@ -19,6 +19,10 @@ class Color { this.a = a; } + static black: Color; + static white: Color; + static transparent: Color; + static parse(input: ?string): Color | void { if (!input) { return undefined; @@ -46,4 +50,8 @@ class Color { } } +Color.black = new Color(0, 0, 0, 1); +Color.white = new Color(1, 1, 1, 1); +Color.transparent = new Color(0, 0, 0, 0); + module.exports = Color; diff --git a/src/style/animation_loop.js b/src/style/animation_loop.js deleted file mode 100644 index 0b893605d76..00000000000 --- a/src/style/animation_loop.js +++ /dev/null @@ -1,35 +0,0 @@ -// @flow - -class AnimationLoop { - n: number; - times: Array<{ id: number, time: number }>; - - constructor() { - this.n = 0; - this.times = []; - } - - // Are all animations done? - stopped() { - this.times = this.times.filter((t) => { - return t.time >= (new Date()).getTime(); - }); - return !this.times.length; - } - - // Add a new animation that will run t milliseconds - // Returns an id that can be used to cancel it layer - set(t: number) { - this.times.push({ id: this.n, time: t + (new Date()).getTime() }); - return this.n++; - } - - // Cancel an animation - cancel(n: number) { - this.times = this.times.filter((t) => { - return t.id !== n; - }); - } -} - -module.exports = AnimationLoop; diff --git a/src/style/cross_faded.js b/src/style/cross_faded.js new file mode 100644 index 00000000000..2267976cd82 --- /dev/null +++ b/src/style/cross_faded.js @@ -0,0 +1,9 @@ +// @flow + +export type CrossFaded = { + from: T, + to: T, + fromScale: number, + toScale: number, + t: number +}; diff --git a/src/style/light.js b/src/style/light.js index 328f0f839c0..a6634bf6dfe 100644 --- a/src/style/light.js +++ b/src/style/light.js @@ -1,145 +1,121 @@ // @flow +import type {StylePropertySpecification} from "../style-spec/style-spec"; + const styleSpec = require('../style-spec/reference/latest'); const util = require('../util/util'); const Evented = require('../util/evented'); const validateStyle = require('./validate_style'); -const StyleDeclaration = require('./style_declaration'); -const StyleTransition = require('./style_transition'); +const {sphericalToCartesian} = require('../util/util'); +const Color = require('../style-spec/util/color'); +const interpolate = require('../style-spec/util/interpolate'); + +const { + Properties, + Transitionable, + Transitioning, + PossiblyEvaluated, + DataConstantProperty +} = require('./properties'); + +import type {Property, PropertyValue, EvaluationParameters} from './properties'; + +type LightPosition = { + x: number, + y: number, + z: number +}; + +class LightPositionProperty implements Property<[number, number, number], LightPosition> { + specification: StylePropertySpecification; + + constructor() { + this.specification = styleSpec.light.position; + } + + possiblyEvaluate(value: PropertyValue<[number, number, number], LightPosition>, parameters: EvaluationParameters): LightPosition { + return sphericalToCartesian(value.expression.evaluate(parameters)); + } + + interpolate(a: LightPosition, b: LightPosition, t: number): LightPosition { + return { + x: interpolate.number(a.x, b.x, t), + y: interpolate.number(a.y, b.y, t), + z: interpolate.number(a.z, b.z, t), + }; + } +} -import type AnimationLoop from './animation_loop'; -import type {GlobalProperties} from '../style-spec/expression'; +type Props = {| + "anchor": DataConstantProperty<"map" | "viewport">, + "position": LightPositionProperty, + "color": DataConstantProperty, + "intensity": DataConstantProperty, +|}; + +const properties: Properties = new Properties({ + "anchor": new DataConstantProperty(styleSpec.light.anchor), + "position": new LightPositionProperty(), + "color": new DataConstantProperty(styleSpec.light.color), + "intensity": new DataConstantProperty(styleSpec.light.intensity), +}); const TRANSITION_SUFFIX = '-transition'; -const properties = ['anchor', 'color', 'position', 'intensity']; -const specifications = styleSpec.light; /* * Represents the light used to light extruded features. */ class Light extends Evented { - _declarations: {[string]: StyleDeclaration}; - _transitions: {[string]: StyleTransition}; - _transitionOptions: {[string]: TransitionSpecification}; - calculated: {[string]: any}; + _transitionable: Transitionable; + _transitioning: Transitioning; + properties: PossiblyEvaluated; constructor(lightOptions?: LightSpecification) { super(); - this.set(lightOptions); - } - - set(lightOpts) { - if (this._validate(validateStyle.light, lightOpts)) return; - this._declarations = {}; - this._transitions = {}; - this._transitionOptions = {}; - this.calculated = {}; - - lightOpts = util.extend({ - anchor: specifications.anchor.default, - color: specifications.color.default, - position: specifications.position.default, - intensity: specifications.intensity.default - }, lightOpts); - - for (const prop of properties) { - this._declarations[prop] = new StyleDeclaration(specifications[prop], lightOpts[prop]); - } - - return this; + this._transitionable = new Transitionable(properties); + this.setLight(lightOptions); + this._transitioning = this._transitionable.untransitioned(); } getLight() { - return { - anchor: this.getLightProperty('anchor'), - color: this.getLightProperty('color'), - position: this.getLightProperty('position'), - intensity: this.getLightProperty('intensity') - }; - } - - getLightProperty(property: string) { - if (util.endsWith(property, TRANSITION_SUFFIX)) { - return ( - this._transitionOptions[property] - ); - } else { - return ( - this._declarations[property] && - this._declarations[property].value - ); - } - } - - getLightValue(property: string, globalProperties: GlobalProperties) { - if (property === 'position') { - const calculated: any = this._transitions[property].calculate(globalProperties), - cartesian = util.sphericalToCartesian(calculated); - return { - x: cartesian[0], - y: cartesian[1], - z: cartesian[2] - }; - } - - return this._transitions[property].calculate(globalProperties); + return this._transitionable.serialize(); } setLight(options?: LightSpecification) { - if (this._validate(validateStyle.light, options)) return; - - for (const key in options) { - const value = options[key]; + if (this._validate(validateStyle.light, options)) { + return; + } - if (util.endsWith(key, TRANSITION_SUFFIX)) { - this._transitionOptions[key] = value; - } else if (value === null || value === undefined) { - delete this._declarations[key]; + for (const name in options) { + const value = options[name]; + if (util.endsWith(name, TRANSITION_SUFFIX)) { + this._transitionable.setTransition(name.slice(0, -TRANSITION_SUFFIX.length), value); } else { - this._declarations[key] = new StyleDeclaration(specifications[key], value); + this._transitionable.setValue(name, value); } } } - recalculate(zoom: number) { - for (const property in this._declarations) { - this.calculated[property] = this.getLightValue(property, {zoom: zoom}); + updateTransitions(options: {transition?: boolean}, transition: TransitionSpecification) { + if (options.transition === false) { + this._transitioning = this._transitionable.untransitioned(); + } else { + this._transitioning = this._transitionable.transitioned({ + now: Date.now(), + transition + }, this._transitioning); } } - _applyLightDeclaration(property: string, declaration: StyleDeclaration, options: {}, globalOptions: {}, animationLoop: AnimationLoop) { - const oldTransition = options.transition ? this._transitions[property] : undefined; - const spec = specifications[property]; - - if (declaration === null || declaration === undefined) { - declaration = new StyleDeclaration(spec, spec.default); - } - - if (oldTransition && oldTransition.declaration.json === declaration.json) return; - - const transitionOptions = util.extend({ - duration: 300, - delay: 0 - }, globalOptions, this.getLightProperty(property + TRANSITION_SUFFIX)); - const newTransition = this._transitions[property] = - new StyleTransition(spec, declaration, oldTransition, transitionOptions); - if (!newTransition.instant()) { - newTransition.loopID = animationLoop.set(newTransition.endTime - Date.now()); - } - - if (oldTransition) { - animationLoop.cancel(oldTransition.loopID); - } + hasTransition() { + return this._transitioning.hasTransition(); } - updateLightTransitions(options: {}, globalOptions: {}, animationLoop: AnimationLoop) { - let property; - for (property in this._declarations) { - this._applyLightDeclaration(property, this._declarations[property], options, globalOptions, animationLoop); - } + recalculate(parameters: EvaluationParameters) { + this.properties = this._transitioning.possiblyEvaluate(parameters); } - _validate(validate, value) { + _validate(validate, value: mixed) { return validateStyle.emitErrors(this, validate.call(validateStyle, util.extend({ value: value, // Workaround for https://github.com/mapbox/mapbox-gl-js/issues/2407 diff --git a/src/style/properties.js b/src/style/properties.js new file mode 100644 index 00000000000..38efac92413 --- /dev/null +++ b/src/style/properties.js @@ -0,0 +1,695 @@ +// @flow + +const assert = require('assert'); +const {extend, easeCubicInOut} = require('../util/util'); +const interpolate = require('../style-spec/util/interpolate'); +const {normalizePropertyExpression} = require('../style-spec/expression'); +const Color = require('../style-spec/util/color'); + +import type {StylePropertySpecification} from '../style-spec/style-spec'; +import type {CrossFaded} from './cross_faded'; +import type {ZoomHistory} from './style'; + +import type { + Feature, + GlobalProperties, + StylePropertyExpression, + SourceExpression, + CompositeExpression +} from '../style-spec/expression'; + +type TimePoint = number; + +export type EvaluationParameters = GlobalProperties & { + now?: TimePoint, + defaultFadeDuration?: number, + zoomHistory?: ZoomHistory +}; + +/** + * Implements a number of classes that define state and behavior for paint and layout properties, most + * importantly their respective evaluation chains: + * + * Transitionable paint property value + * → Transitioning paint property value + * → Possibly evaluated paint property value + * → Fully evaluated paint property value + * + * Layout property value + * → Possibly evaluated layout property value + * → Fully evaluated layout property value + * + * @module + * @private + */ + +/** + * Implementations of the `Property` interface: + * + * * Hold metadata about a property that's independent of any specific value: stuff like the type of the value, + * the default value, etc. This comes from the style specification JSON. + * * Define behavior that needs to be polymorphic across different properties: "possibly evaluating" + * an input value (see below), and interpolating between two possibly-evaluted values. + * + * The type `T` is the fully-evaluated value type (e.g. `number`, `string`, `Color`). + * The type `R` is the intermediate "possibly evaluated" value type. See below. + * + * There are two main implementations of the interface -- one for properties that allow data-driven values, + * and one for properties that don't. There are a few "special case" implementations as well: one for properties + * which cross-fade between two values rather than interpolating, one for `heatmap-color`, and one for + * `light-position`. + * + * @private + */ +export interface Property { + specification: StylePropertySpecification; + possiblyEvaluate(value: PropertyValue, parameters: EvaluationParameters): R; + interpolate(a: R, b: R, t: number): R; +} + +/** + * `PropertyValue` represents the value part of a property key-value unit. It's used to represent both + * paint and layout property values, and regardless of whether or not their property supports data-driven + * expressions. + * + * `PropertyValue` stores the raw input value as seen in a style or a runtime styling API call, i.e. one of the + * following: + * + * * A constant value of the type appropriate for the property + * * A function which produces a value of that type (but functions are quasi-deprecated in favor of expressions) + * * An expression which produces a value of that type + * * "undefined"/"not present", in which case the property is assumed to take on its default value. + * + * In addition to storing the original input value, `PropertyValue` also stores a normalized representation, + * effectively treating functions as if they are expressions, and constant or default values as if they are + * (constant) expressions. + * + * @private + */ +class PropertyValue { + property: Property; + value: PropertyValueSpecification | void; + expression: StylePropertyExpression; + + constructor(property: Property, value: PropertyValueSpecification | void) { + this.property = property; + this.value = value; + this.expression = normalizePropertyExpression(value === undefined ? property.specification.default : value, property.specification); + } + + isDataDriven(): boolean { + return this.expression.kind === 'source' || this.expression.kind === 'composite'; + } + + possiblyEvaluate(parameters: EvaluationParameters): R { + return this.property.possiblyEvaluate(this, parameters); + } +} + +// ------- Transitionable ------- + +type TransitionParameters = { + now: TimePoint, + transition: TransitionSpecification +}; + +/** + * Paint properties are _transitionable_: they can change in a fluid manner, interpolating or cross-fading between + * old and new value. The duration of the transition, and the delay before it begins, is configurable. + * + * `TransitionablePropertyValue` is a compositional class that stores both the property value and that transition + * configuration. + * + * A `TransitionablePropertyValue` can calculate the next step in the evaluation chain for paint property values: + * `TransitioningPropertyValue`. + * + * @private + */ +class TransitionablePropertyValue { + property: Property; + value: PropertyValue; + transition: TransitionSpecification | void; + + constructor(property: Property) { + this.property = property; + this.value = new PropertyValue(property, undefined); + } + + transitioned(parameters: TransitionParameters, + prior: TransitioningPropertyValue): TransitioningPropertyValue { + return new TransitioningPropertyValue(this.property, this.value, prior, // eslint-disable-line no-use-before-define + extend({}, this.transition, parameters.transition), parameters.now); + } + + untransitioned(): TransitioningPropertyValue { + return new TransitioningPropertyValue(this.property, this.value, null, {}, 0); // eslint-disable-line no-use-before-define + } +} + +/** + * A helper type: given an object type `Properties` whose values are each of type `Property`, it calculates + * an object type with the same keys and values of type `TransitionablePropertyValue`. + * + * @private + */ +type TransitionablePropertyValues + = $Exact<$ObjMap(p: Property) => TransitionablePropertyValue>> + +/** + * `Transitionable` stores a map of all (property name, `TransitionablePropertyValue`) pairs for paint properties of a + * given layer type. It can calculate the `TransitioningPropertyValue`s for all of them at once, producing a + * `Transitioning` instance for the same set of properties. + * + * @private + */ +class Transitionable { + _properties: Properties; + _values: TransitionablePropertyValues; + + constructor(properties: Properties) { + this._properties = properties; + this._values = (Object.create(properties.defaultTransitionablePropertyValues): any); + } + + getValue(name: S): PropertyValueSpecification | void { + return this._values[name].value.value; + } + + setValue(name: S, value: PropertyValueSpecification | void) { + if (!this._values.hasOwnProperty(name)) { + this._values[name] = new TransitionablePropertyValue(this._values[name].property); + } + // Note that we do not _remove_ an own property in the case where a value is being reset + // to the default: the transition might still be non-default. + this._values[name].value = new PropertyValue(this._values[name].property, value === null ? undefined : value); + } + + getTransition(name: S): TransitionSpecification | void { + return this._values[name].transition; + } + + setTransition(name: S, value: TransitionSpecification | void) { + if (!this._values.hasOwnProperty(name)) { + this._values[name] = new TransitionablePropertyValue(this._values[name].property); + } + this._values[name].transition = value || undefined; + } + + serialize() { + const result: any = {}; + for (const property of Object.keys(this._values)) { + const value = this.getValue(property); + if (value !== undefined) { + result[property] = value; + } + + const transition = this.getTransition(property); + if (transition !== undefined) { + result[`${property}-transition`] = transition; + } + } + return result; + } + + transitioned(parameters: TransitionParameters, prior: Transitioning): Transitioning { + const result = new Transitioning(this._properties); // eslint-disable-line no-use-before-define + for (const property of Object.keys(this._values)) { + result._values[property] = this._values[property].transitioned(parameters, prior._values[property]); + } + return result; + } + + untransitioned(): Transitioning { + const result = new Transitioning(this._properties); // eslint-disable-line no-use-before-define + for (const property of Object.keys(this._values)) { + result._values[property] = this._values[property].untransitioned(); + } + return result; + } +} + +// ------- Transitioning ------- + +/** + * `TransitioningPropertyValue` implements the first of two intermediate steps in the evaluation chain of a paint + * property value. In this step, transitions between old and new values are handled: as long as the transition is in + * progress, `TransitioningPropertyValue` maintains a reference to the prior value, and interpolates between it and + * the new value based on the current time and the configured transition duration and delay. The product is the next + * step in the evaluation chain: the "possibly evaluated" result type `R`. See below for more on this concept. + * + * @private + */ +class TransitioningPropertyValue { + property: Property; + value: PropertyValue; + prior: ?TransitioningPropertyValue; + begin: TimePoint; + end: TimePoint; + + constructor(property: Property, + value: PropertyValue, + prior: ?TransitioningPropertyValue, + transition: TransitionSpecification, + now: TimePoint) { + this.property = property; + this.value = value; + this.begin = now + transition.delay || 0; + this.end = this.begin + transition.duration || 0; + if (transition.delay || transition.duration) { + this.prior = prior; + } + } + + possiblyEvaluate(parameters: EvaluationParameters): R { + const now = parameters.now || 0; + const finalValue = this.value.possiblyEvaluate(parameters); + const prior = this.prior; + if (!prior) { + // No prior value. + return finalValue; + } else if (now > this.end) { + // Transition from prior value is now complete. + this.prior = null; + return finalValue; + } else if (this.value.isDataDriven()) { + // Transitions to data-driven properties are not supported. + // We snap immediately to the data-driven value so that, when we perform layout, + // we see the data-driven function and can use it to populate vertex buffers. + this.prior = null; + return finalValue; + } else if (now < this.begin) { + // Transition hasn't started yet. + return prior.possiblyEvaluate(parameters); + } else { + // Interpolate between recursively-calculated prior value and final. + const t = (now - this.begin) / (this.end - this.begin); + return this.property.interpolate(prior.possiblyEvaluate(parameters), finalValue, easeCubicInOut(t)); + } + } +} + +/** + * A helper type: given an object type `Properties` whose values are each of type `Property`, it calculates + * an object type with the same keys and values of type `TransitioningPropertyValue`. + * + * @private + */ +type TransitioningPropertyValues + = $Exact<$ObjMap(p: Property) => TransitioningPropertyValue>> + +/** + * `Transitioning` stores a map of all (property name, `TransitioningPropertyValue`) pairs for paint properties of a + * given layer type. It can calculate the possibly-evaluated values for all of them at once, producing a + * `PossiblyEvaluated` instance for the same set of properties. + * + * @private + */ +class Transitioning { + _properties: Properties; + _values: TransitioningPropertyValues; + + constructor(properties: Properties) { + this._properties = properties; + this._values = (Object.create(properties.defaultTransitioningPropertyValues): any); + } + + possiblyEvaluate(parameters: EvaluationParameters): PossiblyEvaluated { + const result = new PossiblyEvaluated(this._properties); // eslint-disable-line no-use-before-define + for (const property of Object.keys(this._values)) { + result._values[property] = this._values[property].possiblyEvaluate(parameters); + } + return result; + } + + hasTransition() { + for (const property of Object.keys(this._values)) { + if (this._values[property].prior) { + return true; + } + } + return false; + } +} + +// ------- Layout ------- + +/** + * A helper type: given an object type `Properties` whose values are each of type `Property`, it calculates + * an object type with the same keys and values of type `PropertyValue`. + * + * @private + */ +type PropertyValues + = $Exact<$ObjMap(p: Property) => PropertyValue>> + +/** + * Because layout properties are not transitionable, they have a simpler representation and evaluation chain than + * paint properties: `PropertyValue`s are possibly evaluated, producing possibly evaluated values, which are then + * fully evaluated. + * + * `Layout` stores a map of all (property name, `PropertyValue`) pairs for layout properties of a + * given layer type. It can calculate the possibly-evaluated values for all of them at once, producing a + * `PossiblyEvaluated` instance for the same set of properties. + * + * @private + */ +class Layout { + _properties: Properties; + _values: PropertyValues; + + constructor(properties: Properties) { + this._properties = properties; + this._values = (Object.create(properties.defaultPropertyValues): any); + } + + getValue(name: S) { + return this._values[name].value; + } + + setValue(name: S, value: *) { + this._values[name] = new PropertyValue(this._values[name].property, value === null ? undefined : value); + } + + serialize() { + const result: any = {}; + for (const property of Object.keys(this._values)) { + const value = this.getValue(property); + if (value !== undefined) { + result[property] = value; + } + } + return result; + } + + possiblyEvaluate(parameters: EvaluationParameters): PossiblyEvaluated { + const result = new PossiblyEvaluated(this._properties); // eslint-disable-line no-use-before-define + for (const property of Object.keys(this._values)) { + result._values[property] = this._values[property].possiblyEvaluate(parameters); + } + return result; + } +} + +// ------- PossiblyEvaluated ------- + +/** + * "Possibly evaluated value" is an intermediate stage in the evaluation chain for both paint and layout property + * values. The purpose of this stage is to optimize away unnecessary recalculations for data-driven properties. Code + * which uses data-driven property values must assume that the value is dependent on feature data, and request that it + * be evaluated for each feature. But when that property value is in fact a constant or camera function, the calculation + * will not actually depend on the feature, and we can benefit from returning the prior result of having done the + * evaluation once, ahead of time, in an intermediate step whose inputs are just the value and "global" parameters + * such as current zoom level. + * + * `PossiblyEvaluatedValue` represents the three possible outcomes of this step: if the input value was a constant or + * camera expression, then the "possibly evaluated" result is a constant value. Otherwise, the input value was either + * a source or composite expression, and we must defer final evaluation until supplied a feature. We separate + * the source and composite cases because they are handled differently when generating GL attributes, buffers, and + * uniforms. + * + * Note that `PossiblyEvaluatedValue` (and `PossiblyEvaluatedPropertyValue`, below) are _not_ used for properties that + * do not allow data-driven values. For such properties, we know that the "possibly evaluated" result is always a constant + * scalar value. See below. + * + * @private + */ +type PossiblyEvaluatedValue = + | {kind: 'constant', value: T} + | SourceExpression + | CompositeExpression; + +/** + * `PossiblyEvaluatedPropertyValue` is used for data-driven paint and layout property values. It holds a + * `PossiblyEvaluatedValue` and the `GlobalProperties` that were used to generate it. You're not allowed to supply + * a different set of `GlobalProperties` when performing the final evaluation because they would be ignored in the + * case where the input value was a constant or camera function. + * + * @private + */ +class PossiblyEvaluatedPropertyValue { + property: DataDrivenProperty; + value: PossiblyEvaluatedValue; + globals: GlobalProperties; + + constructor(property: DataDrivenProperty, value: PossiblyEvaluatedValue, globals: GlobalProperties) { + this.property = property; + this.value = value; + this.globals = globals; + } + + isConstant(): boolean { + return this.value.kind === 'constant'; + } + + constantOr(value: T): T { + if (this.value.kind === 'constant') { + return this.value.value; + } else { + return value; + } + } + + evaluate(feature: Feature): T { + return this.property.evaluate(this.value, this.globals, feature); + } +} + +/** + * A helper type: given an object type `Properties` whose values are each of type `Property`, it calculates + * an object type with the same keys, and values of type `R`. + * + * For properties that don't allow data-driven values, `R` is a scalar type such as `number`, `string`, or `Color`. + * For data-driven properties, it is `PossiblyEvaluatedPropertyValue`. Critically, the type definitions are set up + * in a way that allows flow to know which of these two cases applies for any given property name, and if you attempt + * to use a `PossiblyEvaluatedPropertyValue` as if it was a scalar, or vice versa, you will get a type error. (However, + * there's at least one case in which flow fails to produce a type error that you should be aware of: in a context such + * as `layer.paint.get('foo-opacity') === 0`, if `foo-opacity` is data-driven, than the left-hand side is of type + * `PossiblyEvaluatedPropertyValue`, but flow will not complain about comparing this to a number using `===`. + * See https://github.com/facebook/flow/issues/2359.) + * + * There's also a third, special case possiblity for `R`: for cross-faded properties, it's `?CrossFaded`. + * + * @private + */ +type PossiblyEvaluatedPropertyValues + = $Exact<$ObjMap(p: Property) => R>> + +/** + * `PossiblyEvaluated` stores a map of all (property name, `R`) pairs for paint or layout properties of a + * given layer type. + */ +class PossiblyEvaluated { + _properties: Properties; + _values: PossiblyEvaluatedPropertyValues; + + constructor(properties: Properties) { + this._properties = properties; + this._values = (Object.create(properties.defaultPossiblyEvaluatedValues): any); + } + + get(name: S): $ElementType, S> { + return this._values[name]; + } +} + +/** + * An implementation of `Property` for properties that do not permit data-driven (source or composite) expressions. + * This restriction allows us to declare statically that the result of possibly evaluating this kind of property + * is in fact always the scalar type `T`, and can be used without further evaluating the value on a per-feature basis. + * + * @private + */ +class DataConstantProperty implements Property { + specification: StylePropertySpecification; + + constructor(specification: StylePropertySpecification) { + this.specification = specification; + } + + possiblyEvaluate(value: PropertyValue, parameters: EvaluationParameters): T { + assert(!value.isDataDriven()); + return value.expression.evaluate(parameters); + } + + interpolate(a: T, b: T, t: number): T { + const interp: ?(a: T, b: T, t: number) => T = (interpolate: any)[this.specification.type]; + if (interp) { + return interp(a, b, t); + } else { + return a; + } + } +} + +/** + * An implementation of `Property` for properties that permit data-driven (source or composite) expressions. + * The result of possibly evaluating this kind of property is `PossiblyEvaluatedPropertyValue`; obtaining + * a scalar value `T` requires further evaluation on a per-feature basis. + * + * @private + */ +class DataDrivenProperty implements Property> { + specification: StylePropertySpecification; + useIntegerZoom: boolean; + + constructor(specification: StylePropertySpecification, useIntegerZoom: boolean = false) { + this.specification = specification; + this.useIntegerZoom = useIntegerZoom; + } + + possiblyEvaluate(value: PropertyValue>, parameters: EvaluationParameters): PossiblyEvaluatedPropertyValue { + if (this.useIntegerZoom) { + parameters = extend({}, parameters, {zoom: Math.floor(parameters.zoom)}); + } + if (value.expression.kind === 'constant' || value.expression.kind === 'camera') { + return new PossiblyEvaluatedPropertyValue(this, {kind: 'constant', value: value.expression.evaluate(parameters)}, parameters); + } else { + return new PossiblyEvaluatedPropertyValue(this, value.expression, parameters); + } + } + + interpolate(a: PossiblyEvaluatedPropertyValue, + b: PossiblyEvaluatedPropertyValue, + t: number): PossiblyEvaluatedPropertyValue { + // If either possibly-evaluated value is non-constant, give up: we aren't able to interpolate data-driven values. + if (a.value.kind !== 'constant' || b.value.kind !== 'constant') { + return a; + } + + // Special case hack solely for fill-outline-color. + if (a.value.value === undefined || a.value.value === undefined) + return (undefined: any); + + const interp: ?(a: T, b: T, t: number) => T = (interpolate: any)[this.specification.type]; + if (interp) { + return new PossiblyEvaluatedPropertyValue(this, {kind: 'constant', value: interp(a.value.value, b.value.value, t)}, a.globals); + } else { + return a; + } + } + + evaluate(value: PossiblyEvaluatedValue, globals: GlobalProperties, feature: Feature): T { + if (this.useIntegerZoom) { + globals = extend({}, globals, {zoom: Math.floor(globals.zoom)}); + } + if (value.kind === 'constant') { + return value.value; + } else { + return value.evaluate(globals, feature); + } + } +} + +/** + * An implementation of `Property` for `*-pattern` and `line-dasharray`, which are transitioned by cross-fading + * rather than interpolation. + * + * @private + */ +class CrossFadedProperty implements Property> { + specification: StylePropertySpecification; + + constructor(specification: StylePropertySpecification) { + this.specification = specification; + } + + possiblyEvaluate(value: PropertyValue>, parameters: EvaluationParameters): ?CrossFaded { + if (value.value === undefined) { + return undefined; + } else if (value.expression.kind === 'constant') { + const constant = value.expression.evaluate(parameters); + return this._calculate(constant, constant, constant, parameters); + } else { + assert(!value.isDataDriven()); + return this._calculate( + value.expression.evaluate({zoom: parameters.zoom - 1.0}), + value.expression.evaluate({zoom: parameters.zoom}), + value.expression.evaluate({zoom: parameters.zoom + 1.0}), + parameters); + } + } + + _calculate(min: T, mid: T, max: T, parameters: any): ?CrossFaded { + const z = parameters.zoom; + const fraction = z - Math.floor(z); + const d = parameters.defaultFadeDuration; + const t = d !== 0 ? Math.min((parameters.now - parameters.zoomHistory.lastIntegerZoomTime) / d, 1) : 1; + return z > parameters.zoomHistory.lastIntegerZoom ? + { from: min, to: mid, fromScale: 2, toScale: 1, t: fraction + (1 - fraction) * t } : + { from: max, to: mid, fromScale: 0.5, toScale: 1, t: 1 - (1 - t) * fraction }; + } + + interpolate(a: ?CrossFaded): ?CrossFaded { + return a; + } +} + +/** + * An implementation of `Property` for `heatmap-color`. Evaluation and interpolation are no-ops: the real + * evaluation happens in HeatmapStyleLayer. + * + * @private + */ +class HeatmapColorProperty implements Property { + specification: StylePropertySpecification; + + constructor(specification: StylePropertySpecification) { + this.specification = specification; + } + + possiblyEvaluate() {} + interpolate() {} +} + +/** + * `Properties` holds objects containing default values for the layout or paint property set of a given + * layer type. These objects are immutable, and they are used as the prototypes for the `_values` members of + * `Transitionable`, `Transitioning`, `Layout`, and `PossiblyEvaluated`. This allows these classes to avoid + * doing work in the common case where a property has no explicit value set and should be considered to take + * on the default value: using `for (const property of Object.keys(this._values))`, they can iterate over + * only the _own_ properties of `_values`, skipping repeated calculation of transitions and possible/final + * evaluations for defaults, the result of which will always be the same. + * + * @private + */ +class Properties { + properties: Props; + defaultPropertyValues: PropertyValues; + defaultTransitionablePropertyValues: TransitionablePropertyValues; + defaultTransitioningPropertyValues: TransitioningPropertyValues; + defaultPossiblyEvaluatedValues: PossiblyEvaluatedPropertyValues; + + constructor(properties: Props) { + this.properties = properties; + this.defaultPropertyValues = ({}: any); + this.defaultTransitionablePropertyValues = ({}: any); + this.defaultTransitioningPropertyValues = ({}: any); + this.defaultPossiblyEvaluatedValues = ({}: any); + + for (const property in properties) { + const prop = properties[property]; + const defaultPropertyValue = this.defaultPropertyValues[property] = + new PropertyValue(prop, undefined); + const defaultTransitionablePropertyValue = this.defaultTransitionablePropertyValues[property] = + new TransitionablePropertyValue(prop); + this.defaultTransitioningPropertyValues[property] = + defaultTransitionablePropertyValue.untransitioned(); + this.defaultPossiblyEvaluatedValues[property] = + defaultPropertyValue.possiblyEvaluate(({}: any)); + } + } +} + +module.exports = { + PropertyValue, + Transitionable, + Transitioning, + Layout, + PossiblyEvaluatedPropertyValue, + PossiblyEvaluated, + DataConstantProperty, + DataDrivenProperty, + CrossFadedProperty, + HeatmapColorProperty, + Properties +}; diff --git a/src/style/query_utils.js b/src/style/query_utils.js index 84a87f50ddb..4a9931150ff 100644 --- a/src/style/query_utils.js +++ b/src/style/query_utils.js @@ -2,13 +2,16 @@ const Point = require('@mapbox/point-geometry'); -import type StyleLayer from './style_layer'; - -function getMaximumPaintValue(property: string, layer: StyleLayer, bucket: *) { - if (layer.isPaintValueFeatureConstant(property)) { - return layer.paint[property]; +import type {PossiblyEvaluatedPropertyValue} from "./properties"; +import type StyleLayer from '../style/style_layer'; +import type {Bucket} from '../data/bucket'; + +function getMaximumPaintValue(property: string, layer: StyleLayer, bucket: Bucket): number { + const value = ((layer.paint: any).get(property): PossiblyEvaluatedPropertyValue).value; + if (value.kind === 'constant') { + return value.value; } else { - return bucket.programConfigurations.get(layer.id) + return (bucket: any).programConfigurations.get(layer.id) .paintPropertyStatistics[property].max; } } diff --git a/src/style/style.js b/src/style/style.js index 6ee38275f6e..06fb013fbb2 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -13,7 +13,6 @@ const ajax = require('../util/ajax'); const mapbox = require('../util/mapbox'); const browser = require('../util/browser'); const Dispatcher = require('../util/dispatcher'); -const AnimationLoop = require('./animation_loop'); const validateStyle = require('./validate_style'); const getSourceType = require('../source/source').getType; const setSourceType = require('../source/source').setType; @@ -75,7 +74,6 @@ export type ZoomHistory = { class Style extends Evented { map: Map; stylesheet: StyleSpecification; - animationLoop: AnimationLoop; dispatcher: Dispatcher; imageManager: ImageManager; glyphManager: GlyphManager; @@ -104,7 +102,6 @@ class Style extends Evented { super(); this.map = map; - this.animationLoop = (map && map.animationLoop) || new AnimationLoop(); this.dispatcher = new Dispatcher(getWorkerPool(), this); this.imageManager = new ImageManager(); this.glyphManager = new GlyphManager(map._transformRequest, options.localIdeographFontFamily); @@ -281,24 +278,19 @@ class Style extends Evented { if (!this._loaded) return; options = options || {transition: true}; - const transition = this.stylesheet.transition || {}; + + const transition = util.extend({ + duration: 300, + delay: 0 + }, this.stylesheet.transition); const layers = this._updatedAllPaintProps ? this._layers : this._updatedPaintProps; for (const id in layers) { - const layer = this._layers[id]; - const props = this._updatedPaintProps[id]; - - if (this._updatedAllPaintProps || props.all) { - layer.updatePaintTransitions(options, transition, this.animationLoop, this.zoomHistory); - } else { - for (const paintName in props) { - this._layers[id].updatePaintTransition(paintName, options, transition, this.animationLoop, this.zoomHistory); - } - } + this._layers[id].updatePaintTransitions(options, transition); } - this.light.updateLightTransitions(options, transition, this.animationLoop); + this.light.updateTransitions(options, transition); } _recalculate(z: number) { @@ -307,25 +299,44 @@ class Style extends Evented { for (const sourceId in this.sourceCaches) this.sourceCaches[sourceId].used = false; - this._updateZoomHistory(z); + const parameters = { + zoom: z, + now: Date.now(), + defaultFadeDuration: 300, + zoomHistory: this._updateZoomHistory(z) + }; for (const layerId of this._order) { const layer = this._layers[layerId]; - layer.recalculate(z); + layer.recalculate(parameters); if (!layer.isHidden(z) && layer.source) { this.sourceCaches[layer.source].used = true; } } - this.light.recalculate(z); + this.light.recalculate(parameters); + this.z = z; + } - const maxZoomTransitionDuration = 300; - if (Math.floor(this.z) !== Math.floor(z)) { - this.animationLoop.set(maxZoomTransitionDuration); + hasTransitions() { + if (this.light.hasTransition()) { + return true; } - this.z = z; + for (const id in this.sourceCaches) { + if (this.sourceCaches[id].hasTransition()) { + return true; + } + } + + for (const id in this._layers) { + if (this._layers[id].hasTransition()) { + return true; + } + } + + return false; } _updateZoomHistory(z: number) { @@ -351,6 +362,7 @@ class Style extends Evented { } zh.lastZoom = z; + return zh; } _checkLoaded() { @@ -774,11 +786,11 @@ class Style extends Evented { if (util.deepEqual(layer.getPaintProperty(name), value)) return; - const wasFeatureConstant = layer.isPaintValueFeatureConstant(name); + const wasDataDriven = layer._transitionablePaint._values[name].value.isDataDriven(); layer.setPaintProperty(name, value); - const isFeatureConstant = layer.isPaintValueFeatureConstant(name); + const isDataDriven = layer._transitionablePaint._values[name].value.isDataDriven(); - if (!isFeatureConstant || !wasFeatureConstant) { + if (isDataDriven || wasDataDriven) { this._updateLayer(layer); } @@ -909,7 +921,7 @@ class Style extends Evented { return this.light.getLight(); } - setLight(lightOptions: LightSpecification, transitionOptions?: {}) { + setLight(lightOptions: LightSpecification, options: ?{transition?: boolean}) { this._checkLoaded(); const light = this.light.getLight(); @@ -922,10 +934,15 @@ class Style extends Evented { } if (!_update) return; - const transition = this.stylesheet.transition || {}; + options = options || {transition: true}; + + const transition = util.extend({ + duration: 300, + delay: 0 + }, this.stylesheet.transition); this.light.setLight(lightOptions); - this.light.updateLightTransitions(transitionOptions || {transition: true}, transition, this.animationLoop); + this.light.updateTransitions(options, transition); } _validate(validate: ({}) => void, key: string, value: any, props: any, options?: {validate?: boolean}) { diff --git a/src/style/style_declaration.js b/src/style/style_declaration.js deleted file mode 100644 index a02c4a29e04..00000000000 --- a/src/style/style_declaration.js +++ /dev/null @@ -1,80 +0,0 @@ -// @flow - -const Color = require('../style-spec/util/color'); -const {isFunction, createFunction} = require('../style-spec/function'); -const {isExpression, createPropertyExpression} = require('../style-spec/expression'); -const util = require('../util/util'); - -import type {StylePropertyExpression, Feature, GlobalProperties} from '../style-spec/expression'; - -function normalizeToExpression(parameters, propertySpec): StylePropertyExpression { - if (isFunction(parameters)) { - return createFunction(parameters, propertySpec); - } else if (isExpression(parameters)) { - const expression = createPropertyExpression(parameters, propertySpec); - if (expression.result === 'error') { - // this should have been caught in validation - throw new Error(expression.value.map(err => `${err.key}: ${err.message}`).join(', ')); - } - return expression.value; - } else { - if (typeof parameters === 'string' && propertySpec.type === 'color') { - parameters = Color.parse(parameters); - } - return { - kind: 'constant', - evaluate: () => parameters - }; - } -} - -/** - * A style property declaration - * @private - */ -class StyleDeclaration { - value: any; - json: mixed; - minimum: number; - expression: StylePropertyExpression; - - constructor(reference: any, value: any) { - this.value = util.clone(value); - - // immutable representation of value. used for comparison - this.json = JSON.stringify(this.value); - - this.minimum = reference.minimum; - this.expression = normalizeToExpression(this.value, reference); - } - - isFeatureConstant() { - return this.expression.kind === 'constant' || this.expression.kind === 'camera'; - } - - isZoomConstant() { - return this.expression.kind === 'constant' || this.expression.kind === 'source'; - } - - calculate(globals: GlobalProperties, feature?: Feature) { - const value = this.expression.evaluate(globals, feature); - if (this.minimum !== undefined && value < this.minimum) { - return this.minimum; - } - return value; - } - - /** - * Calculate the interpolation factor for the given zoom stops and current - * zoom level. - */ - interpolationFactor(zoom: number, lower: number, upper: number) { - if (this.expression.kind === 'constant' || this.expression.kind === 'source') { - return 0; - } else { - return this.expression.interpolationFactor(zoom, lower, upper); - } - } -} - -module.exports = StyleDeclaration; diff --git a/src/style/style_layer.js b/src/style/style_layer.js index b7b8c473e43..47332cfc5e2 100644 --- a/src/style/style_layer.js +++ b/src/style/style_layer.js @@ -1,19 +1,23 @@ // @flow + const util = require('../util/util'); -const StyleTransition = require('./style_transition'); -const StyleDeclaration = require('./style_declaration'); const styleSpec = require('../style-spec/reference/latest'); const validateStyle = require('./validate_style'); -const Color = require('./../style-spec/util/color'); const Evented = require('../util/evented'); +const { + Layout, + Transitionable, + Transitioning, + Properties +} = require('./properties'); + import type {Bucket, BucketParameters} from '../data/bucket'; import type Point from '@mapbox/point-geometry'; -import type {Feature, GlobalProperties} from '../style-spec/expression'; import type RenderTexture from '../render/render_texture'; -import type AnimationLoop from './animation_loop'; import type {FeatureFilter} from '../style-spec/feature_filter'; +import type {EvaluationParameters} from './properties'; const TRANSITION_SUFFIX = '-transition'; @@ -28,20 +32,18 @@ class StyleLayer extends Evented { minzoom: ?number; maxzoom: ?number; filter: mixed; - paint: { [string]: any }; - layout: { [string]: any }; + visibility: 'visible' | 'none'; + + _unevaluatedLayout: Layout; + +layout: mixed; + + _transitionablePaint: Transitionable; + _transitioningPaint: Transitioning; + +paint: mixed; viewportFrame: ?RenderTexture; _featureFilter: FeatureFilter; - _paintSpecifications: any; - _layoutSpecifications: any; - _paintTransitions: {[string]: StyleTransition}; - _paintTransitionOptions: {[string]: TransitionSpecification}; - _paintDeclarations: {[string]: StyleDeclaration}; - _layoutDeclarations: {[string]: StyleDeclaration}; - _layoutFunctions: {[string]: boolean}; - +createBucket: (parameters: BucketParameters) => Bucket; +queryRadius: (bucket: Bucket) => number; +queryIntersectsFeature: (queryGeometry: Array>, @@ -51,7 +53,7 @@ class StyleLayer extends Evented { bearing: number, pixelsToTileUnits: number) => boolean; - constructor(layer: LayerSpecification) { + constructor(layer: LayerSpecification, properties: {layout?: Properties<*>, paint: Properties<*>}) { super(); this.id = layer.id; @@ -59,6 +61,7 @@ class StyleLayer extends Evented { this.type = layer.type; this.minzoom = layer.minzoom; this.maxzoom = layer.maxzoom; + this.visibility = 'visible'; if (layer.type !== 'background') { this.source = layer.source; @@ -66,171 +69,98 @@ class StyleLayer extends Evented { this.filter = layer.filter; } - this.paint = {}; - this.layout = {}; - this._featureFilter = () => true; - this._paintSpecifications = styleSpec[`paint_${this.type}`]; - this._layoutSpecifications = styleSpec[`layout_${this.type}`]; - - this._paintTransitions = {}; // {[propertyName]: StyleTransition} - this._paintTransitionOptions = {}; // {[propertyName]: { duration:Number, delay:Number }} - this._paintDeclarations = {}; // {[propertyName]: StyleDeclaration} - this._layoutDeclarations = {}; // {[propertyName]: StyleDeclaration} - this._layoutFunctions = {}; // {[propertyName]: Boolean} - - let paintName, layoutName; - const options = {validate: false}; - - // Resolve paint declarations - for (paintName in layer.paint) { - this.setPaintProperty(paintName, layer.paint[paintName], options); + if (properties.layout) { + this._unevaluatedLayout = new Layout(properties.layout); } - // Resolve layout declarations - for (layoutName in layer.layout) { - this.setLayoutProperty(layoutName, layer.layout[layoutName], options); - } + this._transitionablePaint = new Transitionable(properties.paint); - // set initial layout/paint values - for (paintName in this._paintSpecifications) { - this.paint[paintName] = this.getPaintValue(paintName, {zoom: 0}); + for (const property in layer.paint) { + this.setPaintProperty(property, layer.paint[property], {validate: false}); } - for (layoutName in this._layoutSpecifications) { - this._updateLayoutValue(layoutName); + for (const property in layer.layout) { + this.setLayoutProperty(property, layer.layout[property], {validate: false}); } - } - setLayoutProperty(name: string, value: mixed, options: {validate: boolean}) { - if (value == null) { - delete this._layoutDeclarations[name]; - } else { - const 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); + this._transitioningPaint = this._transitionablePaint.untransitioned(); } getLayoutProperty(name: string) { - return ( - this._layoutDeclarations[name] && - this._layoutDeclarations[name].value - ); - } - - getLayoutValue(name: string, globals: GlobalProperties, feature?: Feature): any { - const specification = this._layoutSpecifications[name]; - const declaration = this._layoutDeclarations[name]; - - // Avoid attempting to calculate a value for data-driven properties if `feature` is undefined. - if (declaration && (declaration.isFeatureConstant() || feature)) { - return declaration.calculate(globals, feature); - } else { - return specification.default; + if (name === 'visibility') { + return this.visibility; } - } - setPaintProperty(name: string, value: any, options: any) { - const validateStyleKey = `layers.${this.id}.paint.${name}`; + return this._unevaluatedLayout.getValue(name); + } - if (util.endsWith(name, TRANSITION_SUFFIX)) { - if (value === null || value === undefined) { - delete this._paintTransitionOptions[name]; - } else { - if (this._validate(validateStyle.paintProperty, validateStyleKey, name, value, options)) return; - this._paintTransitionOptions[name] = value; + setLayoutProperty(name: string, value: mixed, options: {validate: boolean}) { + if (value !== null && value !== undefined) { + const key = `layers.${this.id}.layout.${name}`; + if (this._validate(validateStyle.layoutProperty, key, name, value, options)) { + return; } - } else if (value === null || value === undefined) { - delete this._paintDeclarations[name]; - } else { - if (this._validate(validateStyle.paintProperty, validateStyleKey, name, value, options)) return; - this._paintDeclarations[name] = new StyleDeclaration(this._paintSpecifications[name], value); } + + if (name === 'visibility') { + this.visibility = value === 'none' ? value : 'visible'; + return; + } + + this._unevaluatedLayout.setValue(name, value); } getPaintProperty(name: string) { if (util.endsWith(name, TRANSITION_SUFFIX)) { - return ( - this._paintTransitionOptions[name] - ); + return this._transitionablePaint.getTransition(name.slice(0, -TRANSITION_SUFFIX.length)); } else { - return ( - this._paintDeclarations[name] && - this._paintDeclarations[name].value - ); + return this._transitionablePaint.getValue(name); } } - getPaintValue(name: string, globals: GlobalProperties, feature?: Feature): any { - const specification = this._paintSpecifications[name]; - const transition = this._paintTransitions[name]; + setPaintProperty(name: string, value: mixed, options: {validate: boolean}) { + if (value !== null && value !== undefined) { + const key = `layers.${this.id}.paint.${name}`; + if (this._validate(validateStyle.paintProperty, key, name, value, options)) { + return; + } + } - // Avoid attempting to calculate a value for data-driven properties if `feature` is undefined. - if (transition && (transition.declaration.isFeatureConstant() || feature)) { - return transition.calculate(globals, feature); - } else if (specification.type === 'color' && specification.default) { - return Color.parse(specification.default); + if (util.endsWith(name, TRANSITION_SUFFIX)) { + this._transitionablePaint.setTransition(name.slice(0, -TRANSITION_SUFFIX.length), (value: any) || undefined); } else { - return specification.default; + this._transitionablePaint.setValue(name, value); } } - getPaintInterpolationFactor(name: string, input: number, lower: number, upper: number) { - const declaration = this._paintDeclarations[name]; - return declaration ? declaration.interpolationFactor(input, lower, upper) : 0; - } - - isPaintValueFeatureConstant(name: string) { - const declaration = this._paintDeclarations[name]; - return !declaration || declaration.isFeatureConstant(); - } - - isPaintValueZoomConstant(name: string) { - const declaration = this._paintDeclarations[name]; - return !declaration || declaration.isZoomConstant(); - } - isHidden(zoom: number) { 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; + return this.visibility === 'none'; } - updatePaintTransitions(options: {transition?: boolean}, - globalOptions?: TransitionSpecification, - animationLoop?: AnimationLoop, - zoomHistory?: any) { - let name; - for (name in this._paintDeclarations) { // apply new declarations - this._applyPaintDeclaration(name, this._paintDeclarations[name], options, globalOptions, animationLoop, zoomHistory); - } - for (name in this._paintTransitions) { - if (!(name in this._paintDeclarations)) // apply removed declarations - this._applyPaintDeclaration(name, null, options, globalOptions, animationLoop, zoomHistory); + updatePaintTransitions(options: {transition?: boolean}, transition: TransitionSpecification) { + if (options.transition === false) { + this._transitioningPaint = this._transitionablePaint.untransitioned(); + } else { + this._transitioningPaint = this._transitionablePaint.transitioned({ + now: Date.now(), + transition + }, this._transitioningPaint); } } - updatePaintTransition(name: string, - options: {transition?: boolean}, - globalOptions: TransitionSpecification, - animationLoop: AnimationLoop, - zoomHistory: any) { - const declaration = this._paintDeclarations[name]; - this._applyPaintDeclaration(name, declaration, options, globalOptions, animationLoop, zoomHistory); + hasTransition() { + return this._transitioningPaint.hasTransition(); } - // update all zoom-dependent layout/paint values - recalculate(zoom: number) { - for (const paintName in this._paintTransitions) { - this.paint[paintName] = this.getPaintValue(paintName, {zoom: zoom}); - } - for (const layoutName in this._layoutFunctions) { - this.layout[layoutName] = this.getLayoutValue(layoutName, {zoom: zoom}); + recalculate(parameters: EvaluationParameters) { + if (this._unevaluatedLayout) { + (this: any).layout = this._unevaluatedLayout.possiblyEvaluate(parameters); } + + (this: any).paint = this._transitioningPaint.possiblyEvaluate(parameters); } serialize() { @@ -243,10 +173,15 @@ class StyleLayer extends Evented { 'minzoom': this.minzoom, 'maxzoom': this.maxzoom, 'filter': this.filter, - 'layout': util.mapObject(this._layoutDeclarations, getDeclarationValue), - 'paint': util.mapObject(this._paintDeclarations, getDeclarationValue) + 'layout': this._unevaluatedLayout && this._unevaluatedLayout.serialize(), + 'paint': this._transitionablePaint && this._transitionablePaint.serialize() }; + if (this.visibility === 'none') { + output.layout = output.layout || {}; + output.layout.visibility = 'none'; + } + return util.filterObject(output, (value, key) => { return value !== undefined && !(key === 'layout' && !Object.keys(value).length) && @@ -254,52 +189,6 @@ class StyleLayer extends Evented { }); } - // set paint transition based on a given paint declaration - _applyPaintDeclaration(name: string, - declaration: StyleDeclaration | null | void, - options: {transition?: boolean}, - globalOptions?: TransitionSpecification, - animationLoop?: AnimationLoop, - zoomHistory?: any) { - const oldTransition = options.transition ? this._paintTransitions[name] : undefined; - const spec = this._paintSpecifications[name]; - - if (declaration === null || declaration === undefined) { - declaration = new StyleDeclaration(spec, spec.default); - } - - if (oldTransition && oldTransition.declaration.json === declaration.json) return; - - const transitionOptions = util.extend({ - duration: 300, - delay: 0 - }, globalOptions, this.getPaintProperty(name + TRANSITION_SUFFIX)); - - const newTransition = this._paintTransitions[name] = - new StyleTransition(spec, declaration, oldTransition, transitionOptions, zoomHistory); - - if (!animationLoop) { - return; - } - 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(name: string) { - const declaration = this._layoutDeclarations[name]; - if (!declaration || (declaration.isZoomConstant() && declaration.isFeatureConstant())) { - delete this._layoutFunctions[name]; - this.layout[name] = this.getLayoutValue(name, {zoom: 0}); - } else { - this._layoutFunctions[name] = true; - } - } - _validate(validate: Function, key: string, name: string, value: mixed, options: {validate: boolean}) { if (options && options.validate === false) { return false; @@ -340,7 +229,3 @@ const subclasses = { StyleLayer.create = function(layer: LayerSpecification) { return new subclasses[layer.type](layer); }; - -function getDeclarationValue(declaration) { - return declaration.value; -} diff --git a/src/style/style_layer/background_style_layer.js b/src/style/style_layer/background_style_layer.js index 04770de6c43..4d48cc533f5 100644 --- a/src/style/style_layer/background_style_layer.js +++ b/src/style/style_layer/background_style_layer.js @@ -1,10 +1,23 @@ // @flow const StyleLayer = require('../style_layer'); +const properties = require('./background_style_layer_properties'); + +const { + Transitionable, + Transitioning, + PossiblyEvaluated +} = require('../properties'); + +import type {PaintProps} from './background_style_layer_properties'; class BackgroundStyleLayer extends StyleLayer { - isOpacityZero(zoom: number) { - return this.getPaintValue('background-opacity', { zoom: zoom }) === 0; + _transitionablePaint: Transitionable; + _transitioningPaint: Transitioning; + paint: PossiblyEvaluated; + + constructor(layer: LayerSpecification) { + super(layer, properties); } } diff --git a/src/style/style_layer/background_style_layer_properties.js b/src/style/style_layer/background_style_layer_properties.js new file mode 100644 index 00000000000..d86d3d2d897 --- /dev/null +++ b/src/style/style_layer/background_style_layer_properties.js @@ -0,0 +1,30 @@ +// This file is generated. Edit build/generate-style-code.js, then run `node build/generate-style-code.js`. +// @flow +/* eslint-disable */ + +const styleSpec = require('../../style-spec/reference/latest'); + +const { + Properties, + DataConstantProperty, + DataDrivenProperty, + CrossFadedProperty, + HeatmapColorProperty +} = require('../properties'); + +import type Color from '../../style-spec/util/color'; + + +export type PaintProps = {| + "background-color": DataConstantProperty, + "background-pattern": CrossFadedProperty, + "background-opacity": DataConstantProperty, +|}; + +const paint: Properties = new Properties({ + "background-color": new DataConstantProperty(styleSpec["paint_background"]["background-color"]), + "background-pattern": new CrossFadedProperty(styleSpec["paint_background"]["background-pattern"]), + "background-opacity": new DataConstantProperty(styleSpec["paint_background"]["background-opacity"]), +}); + +module.exports = { paint }; diff --git a/src/style/style_layer/circle_style_layer.js b/src/style/style_layer/circle_style_layer.js index b79e5c87f0b..fc9cf6e58b9 100644 --- a/src/style/style_layer/circle_style_layer.js +++ b/src/style/style_layer/circle_style_layer.js @@ -4,29 +4,36 @@ const StyleLayer = require('../style_layer'); const CircleBucket = require('../../data/bucket/circle_bucket'); const {multiPolygonIntersectsBufferedMultiPoint} = require('../../util/intersection_tests'); const {getMaximumPaintValue, translateDistance, translate} = require('../query_utils'); +const properties = require('./circle_style_layer_properties'); + +const { + Transitionable, + Transitioning, + PossiblyEvaluated +} = require('../properties'); import type {Bucket, BucketParameters} from '../../data/bucket'; import type Point from '@mapbox/point-geometry'; +import type {PaintProps} from './circle_style_layer_properties'; class CircleStyleLayer extends StyleLayer { - createBucket(parameters: BucketParameters) { - return new CircleBucket(parameters); + _transitionablePaint: Transitionable; + _transitioningPaint: Transitioning; + paint: PossiblyEvaluated; + + constructor(layer: LayerSpecification) { + super(layer, properties); } - isOpacityZero(zoom: number) { - return this.isPaintValueFeatureConstant('circle-opacity') && - this.getPaintValue('circle-opacity', { zoom: zoom }) === 0 && - ((this.isPaintValueFeatureConstant('circle-stroke-width') && - this.getPaintValue('circle-stroke-width', { zoom: zoom }) === 0) || - (this.isPaintValueFeatureConstant('circle-stroke-opacity') && - this.getPaintValue('circle-stroke-opacity', { zoom: zoom }) === 0)); + createBucket(parameters: BucketParameters) { + return new CircleBucket(parameters); } queryRadius(bucket: Bucket): number { const circleBucket: CircleBucket = (bucket: any); return getMaximumPaintValue('circle-radius', this, circleBucket) + getMaximumPaintValue('circle-stroke-width', this, circleBucket) + - translateDistance(this.paint['circle-translate']); + translateDistance(this.paint.get('circle-translate')); } queryIntersectsFeature(queryGeometry: Array>, @@ -36,11 +43,11 @@ class CircleStyleLayer extends StyleLayer { bearing: number, pixelsToTileUnits: number): boolean { const translatedPolygon = translate(queryGeometry, - this.getPaintValue('circle-translate', {zoom}, feature), - this.getPaintValue('circle-translate-anchor', {zoom}, feature), + this.paint.get('circle-translate'), + this.paint.get('circle-translate-anchor'), bearing, pixelsToTileUnits); - const radius = this.getPaintValue('circle-radius', {zoom}, feature) * pixelsToTileUnits; - const stroke = this.getPaintValue('circle-stroke-width', {zoom}, feature) * pixelsToTileUnits; + const radius = this.paint.get('circle-radius').evaluate(feature) * pixelsToTileUnits; + const stroke = this.paint.get('circle-stroke-width').evaluate(feature) * pixelsToTileUnits; return multiPolygonIntersectsBufferedMultiPoint(translatedPolygon, geometry, radius + stroke); } } diff --git a/src/style/style_layer/circle_style_layer_properties.js b/src/style/style_layer/circle_style_layer_properties.js new file mode 100644 index 00000000000..17788b25910 --- /dev/null +++ b/src/style/style_layer/circle_style_layer_properties.js @@ -0,0 +1,46 @@ +// This file is generated. Edit build/generate-style-code.js, then run `node build/generate-style-code.js`. +// @flow +/* eslint-disable */ + +const styleSpec = require('../../style-spec/reference/latest'); + +const { + Properties, + DataConstantProperty, + DataDrivenProperty, + CrossFadedProperty, + HeatmapColorProperty +} = require('../properties'); + +import type Color from '../../style-spec/util/color'; + + +export type PaintProps = {| + "circle-radius": DataDrivenProperty, + "circle-color": DataDrivenProperty, + "circle-blur": DataDrivenProperty, + "circle-opacity": DataDrivenProperty, + "circle-translate": DataConstantProperty<[number, number]>, + "circle-translate-anchor": DataConstantProperty<"map" | "viewport">, + "circle-pitch-scale": DataConstantProperty<"map" | "viewport">, + "circle-pitch-alignment": DataConstantProperty<"map" | "viewport">, + "circle-stroke-width": DataDrivenProperty, + "circle-stroke-color": DataDrivenProperty, + "circle-stroke-opacity": DataDrivenProperty, +|}; + +const paint: Properties = new Properties({ + "circle-radius": new DataDrivenProperty(styleSpec["paint_circle"]["circle-radius"]), + "circle-color": new DataDrivenProperty(styleSpec["paint_circle"]["circle-color"]), + "circle-blur": new DataDrivenProperty(styleSpec["paint_circle"]["circle-blur"]), + "circle-opacity": new DataDrivenProperty(styleSpec["paint_circle"]["circle-opacity"]), + "circle-translate": new DataConstantProperty(styleSpec["paint_circle"]["circle-translate"]), + "circle-translate-anchor": new DataConstantProperty(styleSpec["paint_circle"]["circle-translate-anchor"]), + "circle-pitch-scale": new DataConstantProperty(styleSpec["paint_circle"]["circle-pitch-scale"]), + "circle-pitch-alignment": new DataConstantProperty(styleSpec["paint_circle"]["circle-pitch-alignment"]), + "circle-stroke-width": new DataDrivenProperty(styleSpec["paint_circle"]["circle-stroke-width"]), + "circle-stroke-color": new DataDrivenProperty(styleSpec["paint_circle"]["circle-stroke-color"]), + "circle-stroke-opacity": new DataDrivenProperty(styleSpec["paint_circle"]["circle-stroke-opacity"]), +}); + +module.exports = { paint }; diff --git a/src/style/style_layer/fill_extrusion_style_layer.js b/src/style/style_layer/fill_extrusion_style_layer.js index a216f5fdf35..60051be2a35 100644 --- a/src/style/style_layer/fill_extrusion_style_layer.js +++ b/src/style/style_layer/fill_extrusion_style_layer.js @@ -4,31 +4,33 @@ const StyleLayer = require('../style_layer'); const FillExtrusionBucket = require('../../data/bucket/fill_extrusion_bucket'); const {multiPolygonIntersectsMultiPolygon} = require('../../util/intersection_tests'); const {translateDistance, translate} = require('../query_utils'); +const properties = require('./fill_extrusion_style_layer_properties'); + +const { + Transitionable, + Transitioning, + PossiblyEvaluated +} = require('../properties'); -import type {Feature, GlobalProperties} from '../../style-spec/expression'; import type {BucketParameters} from '../../data/bucket'; import type Point from '@mapbox/point-geometry'; +import type {PaintProps} from './fill_extrusion_style_layer_properties'; class FillExtrusionStyleLayer extends StyleLayer { + _transitionablePaint: Transitionable; + _transitioningPaint: Transitioning; + paint: PossiblyEvaluated; - getPaintValue(name: string, globals: GlobalProperties, feature?: Feature) { - const value = super.getPaintValue(name, globals, feature); - if (name === 'fill-extrusion-color' && value) { - value.a = 1; - } - return value; + constructor(layer: LayerSpecification) { + super(layer, properties); } createBucket(parameters: BucketParameters) { return new FillExtrusionBucket(parameters); } - isOpacityZero(zoom: number) { - return this.getPaintValue('fill-extrusion-opacity', { zoom: zoom }) === 0; - } - queryRadius(): number { - return translateDistance(this.paint['fill-extrusion-translate']); + return translateDistance(this.paint.get('fill-extrusion-translate')); } queryIntersectsFeature(queryGeometry: Array>, @@ -38,14 +40,14 @@ class FillExtrusionStyleLayer extends StyleLayer { bearing: number, pixelsToTileUnits: number): boolean { const translatedPolygon = translate(queryGeometry, - this.getPaintValue('fill-extrusion-translate', {zoom}, feature), - this.getPaintValue('fill-extrusion-translate-anchor', {zoom}, feature), + this.paint.get('fill-extrusion-translate'), + this.paint.get('fill-extrusion-translate-anchor'), bearing, pixelsToTileUnits); return multiPolygonIntersectsMultiPolygon(translatedPolygon, geometry); } has3DPass() { - return this.paint['fill-extrusion-opacity'] !== 0 && this.layout['visibility'] !== 'none'; + return this.paint.get('fill-extrusion-opacity') !== 0 && this.visibility !== 'none'; } resize(gl: WebGLRenderingContext) { diff --git a/src/style/style_layer/fill_extrusion_style_layer_properties.js b/src/style/style_layer/fill_extrusion_style_layer_properties.js new file mode 100644 index 00000000000..785f2e9a6cd --- /dev/null +++ b/src/style/style_layer/fill_extrusion_style_layer_properties.js @@ -0,0 +1,38 @@ +// This file is generated. Edit build/generate-style-code.js, then run `node build/generate-style-code.js`. +// @flow +/* eslint-disable */ + +const styleSpec = require('../../style-spec/reference/latest'); + +const { + Properties, + DataConstantProperty, + DataDrivenProperty, + CrossFadedProperty, + HeatmapColorProperty +} = require('../properties'); + +import type Color from '../../style-spec/util/color'; + + +export type PaintProps = {| + "fill-extrusion-opacity": DataConstantProperty, + "fill-extrusion-color": DataDrivenProperty, + "fill-extrusion-translate": DataConstantProperty<[number, number]>, + "fill-extrusion-translate-anchor": DataConstantProperty<"map" | "viewport">, + "fill-extrusion-pattern": CrossFadedProperty, + "fill-extrusion-height": DataDrivenProperty, + "fill-extrusion-base": DataDrivenProperty, +|}; + +const paint: Properties = new Properties({ + "fill-extrusion-opacity": new DataConstantProperty(styleSpec["paint_fill-extrusion"]["fill-extrusion-opacity"]), + "fill-extrusion-color": new DataDrivenProperty(styleSpec["paint_fill-extrusion"]["fill-extrusion-color"]), + "fill-extrusion-translate": new DataConstantProperty(styleSpec["paint_fill-extrusion"]["fill-extrusion-translate"]), + "fill-extrusion-translate-anchor": new DataConstantProperty(styleSpec["paint_fill-extrusion"]["fill-extrusion-translate-anchor"]), + "fill-extrusion-pattern": new CrossFadedProperty(styleSpec["paint_fill-extrusion"]["fill-extrusion-pattern"]), + "fill-extrusion-height": new DataDrivenProperty(styleSpec["paint_fill-extrusion"]["fill-extrusion-height"]), + "fill-extrusion-base": new DataDrivenProperty(styleSpec["paint_fill-extrusion"]["fill-extrusion-base"]), +}); + +module.exports = { paint }; diff --git a/src/style/style_layer/fill_style_layer.js b/src/style/style_layer/fill_style_layer.js index 40c22d356ed..1cbad640f2f 100644 --- a/src/style/style_layer/fill_style_layer.js +++ b/src/style/style_layer/fill_style_layer.js @@ -4,61 +4,33 @@ const StyleLayer = require('../style_layer'); const FillBucket = require('../../data/bucket/fill_bucket'); const {multiPolygonIntersectsMultiPolygon} = require('../../util/intersection_tests'); const {translateDistance, translate} = require('../query_utils'); +const properties = require('./fill_style_layer_properties'); + +const { + Transitionable, + Transitioning, + PossiblyEvaluated +} = require('../properties'); -import type {Feature, GlobalProperties} from '../../style-spec/expression'; import type {BucketParameters} from '../../data/bucket'; import type Point from '@mapbox/point-geometry'; +import type {PaintProps} from './fill_style_layer_properties'; +import type {EvaluationParameters} from '../properties'; class FillStyleLayer extends StyleLayer { + _transitionablePaint: Transitionable; + _transitioningPaint: Transitioning; + paint: PossiblyEvaluated; - getPaintValue(name: string, globals: GlobalProperties, feature?: Feature) { - if (name === 'fill-outline-color') { - // Special-case handling of undefined fill-outline-color values - if (this.getPaintProperty('fill-outline-color') === undefined) { - return super.getPaintValue('fill-color', globals, feature); - } - - // Handle transitions from fill-outline-color: undefined - let transition = this._paintTransitions['fill-outline-color']; - while (transition) { - const declaredValue = ( - transition && - transition.declaration && - transition.declaration.value - ); - - if (!declaredValue) { - return super.getPaintValue('fill-color', globals, feature); - } - - transition = transition.oldTransition; - } - } - - return super.getPaintValue(name, globals, feature); + constructor(layer: LayerSpecification) { + super(layer, properties); } - getPaintInterpolationFactor(name: string, ...args: *) { - if (name === 'fill-outline-color' && this.getPaintProperty('fill-outline-color') === undefined) { - return super.getPaintInterpolationFactor('fill-color', ...args); - } else { - return super.getPaintInterpolationFactor(name, ...args); - } - } - - isPaintValueFeatureConstant(name: string) { - if (name === 'fill-outline-color' && this.getPaintProperty('fill-outline-color') === undefined) { - return super.isPaintValueFeatureConstant('fill-color'); - } else { - return super.isPaintValueFeatureConstant(name); - } - } + recalculate(parameters: EvaluationParameters) { + this.paint = this._transitioningPaint.possiblyEvaluate(parameters); - isPaintValueZoomConstant(name: string) { - if (name === 'fill-outline-color' && this.getPaintProperty('fill-outline-color') === undefined) { - return super.isPaintValueZoomConstant('fill-color'); - } else { - return super.isPaintValueZoomConstant(name); + if (this._transitionablePaint.getValue('fill-outline-color') === undefined) { + this.paint._values['fill-outline-color'] = this.paint._values['fill-color']; } } @@ -66,13 +38,8 @@ class FillStyleLayer extends StyleLayer { return new FillBucket(parameters); } - isOpacityZero(zoom: number) { - return this.isPaintValueFeatureConstant('fill-opacity') && - this.getPaintValue('fill-opacity', { zoom: zoom }) === 0; - } - queryRadius(): number { - return translateDistance(this.paint['fill-translate']); + return translateDistance(this.paint.get('fill-translate')); } queryIntersectsFeature(queryGeometry: Array>, @@ -82,8 +49,8 @@ class FillStyleLayer extends StyleLayer { bearing: number, pixelsToTileUnits: number): boolean { const translatedPolygon = translate(queryGeometry, - this.getPaintValue('fill-translate', {zoom}, feature), - this.getPaintValue('fill-translate-anchor', {zoom}, feature), + this.paint.get('fill-translate'), + this.paint.get('fill-translate-anchor'), bearing, pixelsToTileUnits); return multiPolygonIntersectsMultiPolygon(translatedPolygon, geometry); } diff --git a/src/style/style_layer/fill_style_layer_properties.js b/src/style/style_layer/fill_style_layer_properties.js new file mode 100644 index 00000000000..cc428ca8502 --- /dev/null +++ b/src/style/style_layer/fill_style_layer_properties.js @@ -0,0 +1,38 @@ +// This file is generated. Edit build/generate-style-code.js, then run `node build/generate-style-code.js`. +// @flow +/* eslint-disable */ + +const styleSpec = require('../../style-spec/reference/latest'); + +const { + Properties, + DataConstantProperty, + DataDrivenProperty, + CrossFadedProperty, + HeatmapColorProperty +} = require('../properties'); + +import type Color from '../../style-spec/util/color'; + + +export type PaintProps = {| + "fill-antialias": DataConstantProperty, + "fill-opacity": DataDrivenProperty, + "fill-color": DataDrivenProperty, + "fill-outline-color": DataDrivenProperty, + "fill-translate": DataConstantProperty<[number, number]>, + "fill-translate-anchor": DataConstantProperty<"map" | "viewport">, + "fill-pattern": CrossFadedProperty, +|}; + +const paint: Properties = new Properties({ + "fill-antialias": new DataConstantProperty(styleSpec["paint_fill"]["fill-antialias"]), + "fill-opacity": new DataDrivenProperty(styleSpec["paint_fill"]["fill-opacity"]), + "fill-color": new DataDrivenProperty(styleSpec["paint_fill"]["fill-color"]), + "fill-outline-color": new DataDrivenProperty(styleSpec["paint_fill"]["fill-outline-color"]), + "fill-translate": new DataConstantProperty(styleSpec["paint_fill"]["fill-translate"]), + "fill-translate-anchor": new DataConstantProperty(styleSpec["paint_fill"]["fill-translate-anchor"]), + "fill-pattern": new CrossFadedProperty(styleSpec["paint_fill"]["fill-pattern"]), +}); + +module.exports = { paint }; diff --git a/src/style/style_layer/heatmap_style_layer.js b/src/style/style_layer/heatmap_style_layer.js index e62d8f49a32..2ea0fd5b83f 100644 --- a/src/style/style_layer/heatmap_style_layer.js +++ b/src/style/style_layer/heatmap_style_layer.js @@ -3,55 +3,61 @@ const StyleLayer = require('../style_layer'); const HeatmapBucket = require('../../data/bucket/heatmap_bucket'); const RGBAImage = require('../../util/image').RGBAImage; +const properties = require('./heatmap_style_layer_properties'); + +const { + Transitionable, + Transitioning, + PossiblyEvaluated +} = require('../properties'); import type Texture from '../../render/texture'; -import type Color from '../../style-spec/util/color'; +import type {PaintProps} from './heatmap_style_layer_properties'; class HeatmapStyleLayer extends StyleLayer { heatmapTexture: ?WebGLTexture; heatmapFbo: ?WebGLFramebuffer; - colorRampData: Uint8Array; colorRamp: RGBAImage; colorRampTexture: ?Texture; + _transitionablePaint: Transitionable; + _transitioningPaint: Transitioning; + paint: PossiblyEvaluated; + createBucket(options: any) { return new HeatmapBucket(options); } - isOpacityZero(zoom: number) { - return this.getPaintValue('heatmap-opacity', { zoom: zoom }) === 0; - } - constructor(layer: LayerSpecification) { - super(layer); - this.colorRampData = new Uint8Array(256 * 4); + super(layer, properties); // make sure color ramp texture is generated for default heatmap color too - if (!this.getPaintProperty('heatmap-color')) { - this.setPaintProperty('heatmap-color', this._paintSpecifications['heatmap-color'].default, ''); - } + this._updateColorRamp(); } - // we can't directly override setPaintProperty because it's asynchronous in the sense that - // getPaintValue call immediately after it won't return relevant values, it needs to wait - // until all paint transition have been updated (which usually happens once per frame) - _applyPaintDeclaration(name: any, declaration: any, options: any, globalOptions: any, animationLoop: any, zoomHistory: any) { - super._applyPaintDeclaration(name, declaration, options, globalOptions, animationLoop, zoomHistory); + setPaintProperty(name: string, value: mixed, options: {validate: boolean}) { + super.setPaintProperty(name, value, options); if (name === 'heatmap-color') { - const len = this.colorRampData.length; - for (let i = 4; i < len; i += 4) { - const pxColor: Color = this.getPaintValue('heatmap-color', {heatmapDensity: i / len, zoom: -1}); - // the colors are being unpremultiplied because getPaintValue returns - // premultiplied values, and the Texture class expects unpremultiplied ones - this.colorRampData[i + 0] = Math.floor(pxColor.r * 255 / pxColor.a); - this.colorRampData[i + 1] = Math.floor(pxColor.g * 255 / pxColor.a); - this.colorRampData[i + 2] = Math.floor(pxColor.b * 255 / pxColor.a); - this.colorRampData[i + 3] = Math.floor(pxColor.a * 255); - } - this.colorRamp = RGBAImage.create({width: 256, height: 1}, this.colorRampData); - this.colorRampTexture = null; + this._updateColorRamp(); + } + } + + _updateColorRamp() { + const expression = this._transitionablePaint._values['heatmap-color'].value.expression; + const colorRampData = new Uint8Array(256 * 4); + const len = colorRampData.length; + for (let i = 4; i < len; i += 4) { + const pxColor = expression.evaluate(({heatmapDensity: i / len}: any)); + // the colors are being unpremultiplied because Color uses + // premultiplied values, and the Texture class expects unpremultiplied ones + colorRampData[i + 0] = Math.floor(pxColor.r * 255 / pxColor.a); + colorRampData[i + 1] = Math.floor(pxColor.g * 255 / pxColor.a); + colorRampData[i + 2] = Math.floor(pxColor.b * 255 / pxColor.a); + colorRampData[i + 3] = Math.floor(pxColor.a * 255); } + this.colorRamp = RGBAImage.create({width: 256, height: 1}, colorRampData); + this.colorRampTexture = null; } resize(gl: WebGLRenderingContext) { diff --git a/src/style/style_layer/heatmap_style_layer_properties.js b/src/style/style_layer/heatmap_style_layer_properties.js new file mode 100644 index 00000000000..d6eea888d39 --- /dev/null +++ b/src/style/style_layer/heatmap_style_layer_properties.js @@ -0,0 +1,34 @@ +// This file is generated. Edit build/generate-style-code.js, then run `node build/generate-style-code.js`. +// @flow +/* eslint-disable */ + +const styleSpec = require('../../style-spec/reference/latest'); + +const { + Properties, + DataConstantProperty, + DataDrivenProperty, + CrossFadedProperty, + HeatmapColorProperty +} = require('../properties'); + +import type Color from '../../style-spec/util/color'; + + +export type PaintProps = {| + "heatmap-radius": DataConstantProperty, + "heatmap-weight": DataDrivenProperty, + "heatmap-intensity": DataConstantProperty, + "heatmap-color": HeatmapColorProperty, + "heatmap-opacity": DataConstantProperty, +|}; + +const paint: Properties = new Properties({ + "heatmap-radius": new DataConstantProperty(styleSpec["paint_heatmap"]["heatmap-radius"]), + "heatmap-weight": new DataDrivenProperty(styleSpec["paint_heatmap"]["heatmap-weight"]), + "heatmap-intensity": new DataConstantProperty(styleSpec["paint_heatmap"]["heatmap-intensity"]), + "heatmap-color": new HeatmapColorProperty(styleSpec["paint_heatmap"]["heatmap-color"]), + "heatmap-opacity": new DataConstantProperty(styleSpec["paint_heatmap"]["heatmap-opacity"]), +}); + +module.exports = { paint }; diff --git a/src/style/style_layer/layer_properties.js.ejs b/src/style/style_layer/layer_properties.js.ejs new file mode 100644 index 00000000000..5cd90983daf --- /dev/null +++ b/src/style/style_layer/layer_properties.js.ejs @@ -0,0 +1,48 @@ +<% + const type = locals.type; + const layoutProperties = locals.layoutProperties; + const paintProperties = locals.paintProperties; +-%> +// This file is generated. Edit build/generate-style-code.js, then run `node build/generate-style-code.js`. +// @flow +/* eslint-disable */ + +const styleSpec = require('../../style-spec/reference/latest'); + +const { + Properties, + DataConstantProperty, + DataDrivenProperty, + CrossFadedProperty, + HeatmapColorProperty +} = require('../properties'); + +import type Color from '../../style-spec/util/color'; + +<% if (layoutProperties.length) { -%> +export type LayoutProps = {| +<% for (const property of layoutProperties) { -%> + "<%= property.name %>": <%- propertyType(property) %>, +<% } -%> +|}; + +const layout: Properties = new Properties({ +<% for (const property of layoutProperties) { -%> + "<%= property.name %>": <%- propertyValue(property, 'layout') %>, +<% } -%> +}); +<% } -%> + +export type PaintProps = {| +<% for (const property of paintProperties) { -%> + "<%= property.name %>": <%- propertyType(property) %>, +<% } -%> +|}; + +const paint: Properties = new Properties({ +<% for (const property of paintProperties) { -%> + "<%= property.name %>": <%- propertyValue(property, 'paint') %>, +<% } -%> +}); + +module.exports = { paint<% if (layoutProperties.length) { %>, layout<% } %> }; diff --git a/src/style/style_layer/line_style_layer.js b/src/style/style_layer/line_style_layer.js index dfd33b0b5df..08d7982bacd 100644 --- a/src/style/style_layer/line_style_layer.js +++ b/src/style/style_layer/line_style_layer.js @@ -6,17 +6,43 @@ const StyleLayer = require('../style_layer'); const LineBucket = require('../../data/bucket/line_bucket'); const {multiPolygonIntersectsBufferedMultiLine} = require('../../util/intersection_tests'); const {getMaximumPaintValue, translateDistance, translate} = require('../query_utils'); +const properties = require('./line_style_layer_properties'); + +const { + Transitionable, + Transitioning, + Layout, + PossiblyEvaluated, + DataDrivenProperty +} = require('../properties'); import type {Bucket, BucketParameters} from '../../data/bucket'; +import type {LayoutProps, PaintProps} from './line_style_layer_properties'; +import type {EvaluationParameters} from '../properties'; + +const lineFloorwidthProperty = new DataDrivenProperty(properties.paint.properties['line-width'].specification, true); class LineStyleLayer extends StyleLayer { - createBucket(parameters: BucketParameters) { - return new LineBucket(parameters); + _unevaluatedLayout: Layout; + layout: PossiblyEvaluated; + + _transitionablePaint: Transitionable; + _transitioningPaint: Transitioning; + paint: PossiblyEvaluated; + + constructor(layer: LayerSpecification) { + super(layer, properties); } - isOpacityZero(zoom: number) { - return this.isPaintValueFeatureConstant('line-opacity') && - this.getPaintValue('line-opacity', { zoom: zoom }) === 0; + recalculate(parameters: EvaluationParameters) { + super.recalculate(parameters); + + (this.paint._values: any)['line-floorwidth'] = + lineFloorwidthProperty.possiblyEvaluate(this._transitioningPaint._values['line-width'].value, parameters); + } + + createBucket(parameters: BucketParameters) { + return new LineBucket(parameters); } queryRadius(bucket: Bucket): number { @@ -25,7 +51,7 @@ class LineStyleLayer extends StyleLayer { getMaximumPaintValue('line-width', this, lineBucket), getMaximumPaintValue('line-gap-width', this, lineBucket)); const offset = getMaximumPaintValue('line-offset', this, lineBucket); - return width / 2 + Math.abs(offset) + translateDistance(this.paint['line-translate']); + return width / 2 + Math.abs(offset) + translateDistance(this.paint.get('line-translate')); } queryIntersectsFeature(queryGeometry: Array>, @@ -35,13 +61,13 @@ class LineStyleLayer extends StyleLayer { bearing: number, pixelsToTileUnits: number): boolean { const translatedPolygon = translate(queryGeometry, - this.getPaintValue('line-translate', {zoom}, feature), - this.getPaintValue('line-translate-anchor', {zoom}, feature), + this.paint.get('line-translate'), + this.paint.get('line-translate-anchor'), bearing, pixelsToTileUnits); const halfWidth = pixelsToTileUnits / 2 * getLineWidth( - this.getPaintValue('line-width', {zoom}, feature), - this.getPaintValue('line-gap-width', {zoom}, feature)); - const lineOffset = this.getPaintValue('line-offset', {zoom}, feature); + this.paint.get('line-width').evaluate(feature), + this.paint.get('line-gap-width').evaluate(feature)); + const lineOffset = this.paint.get('line-offset').evaluate(feature); if (lineOffset) { geometry = offsetLine(geometry, lineOffset * pixelsToTileUnits); } diff --git a/src/style/style_layer/line_style_layer_properties.js b/src/style/style_layer/line_style_layer_properties.js new file mode 100644 index 00000000000..48b9a747793 --- /dev/null +++ b/src/style/style_layer/line_style_layer_properties.js @@ -0,0 +1,57 @@ +// This file is generated. Edit build/generate-style-code.js, then run `node build/generate-style-code.js`. +// @flow +/* eslint-disable */ + +const styleSpec = require('../../style-spec/reference/latest'); + +const { + Properties, + DataConstantProperty, + DataDrivenProperty, + CrossFadedProperty, + HeatmapColorProperty +} = require('../properties'); + +import type Color from '../../style-spec/util/color'; + +export type LayoutProps = {| + "line-cap": DataConstantProperty<"butt" | "round" | "square">, + "line-join": DataDrivenProperty<"bevel" | "round" | "miter">, + "line-miter-limit": DataConstantProperty, + "line-round-limit": DataConstantProperty, +|}; + +const layout: Properties = new Properties({ + "line-cap": new DataConstantProperty(styleSpec["layout_line"]["line-cap"]), + "line-join": new DataDrivenProperty(styleSpec["layout_line"]["line-join"]), + "line-miter-limit": new DataConstantProperty(styleSpec["layout_line"]["line-miter-limit"]), + "line-round-limit": new DataConstantProperty(styleSpec["layout_line"]["line-round-limit"]), +}); + +export type PaintProps = {| + "line-opacity": DataDrivenProperty, + "line-color": DataDrivenProperty, + "line-translate": DataConstantProperty<[number, number]>, + "line-translate-anchor": DataConstantProperty<"map" | "viewport">, + "line-width": DataDrivenProperty, + "line-gap-width": DataDrivenProperty, + "line-offset": DataDrivenProperty, + "line-blur": DataDrivenProperty, + "line-dasharray": CrossFadedProperty>, + "line-pattern": CrossFadedProperty, +|}; + +const paint: Properties = new Properties({ + "line-opacity": new DataDrivenProperty(styleSpec["paint_line"]["line-opacity"]), + "line-color": new DataDrivenProperty(styleSpec["paint_line"]["line-color"]), + "line-translate": new DataConstantProperty(styleSpec["paint_line"]["line-translate"]), + "line-translate-anchor": new DataConstantProperty(styleSpec["paint_line"]["line-translate-anchor"]), + "line-width": new DataDrivenProperty(styleSpec["paint_line"]["line-width"]), + "line-gap-width": new DataDrivenProperty(styleSpec["paint_line"]["line-gap-width"]), + "line-offset": new DataDrivenProperty(styleSpec["paint_line"]["line-offset"]), + "line-blur": new DataDrivenProperty(styleSpec["paint_line"]["line-blur"]), + "line-dasharray": new CrossFadedProperty(styleSpec["paint_line"]["line-dasharray"]), + "line-pattern": new CrossFadedProperty(styleSpec["paint_line"]["line-pattern"]), +}); + +module.exports = { paint, layout }; diff --git a/src/style/style_layer/raster_style_layer.js b/src/style/style_layer/raster_style_layer.js index 87bafbc9f29..68968d8b8cf 100644 --- a/src/style/style_layer/raster_style_layer.js +++ b/src/style/style_layer/raster_style_layer.js @@ -1,10 +1,23 @@ // @flow const StyleLayer = require('../style_layer'); +const properties = require('./raster_style_layer_properties'); + +const { + Transitionable, + Transitioning, + PossiblyEvaluated +} = require('../properties'); + +import type {PaintProps} from './raster_style_layer_properties'; class RasterStyleLayer extends StyleLayer { - isOpacityZero(zoom: number) { - return this.getPaintValue('raster-opacity', { zoom: zoom }) === 0; + _transitionablePaint: Transitionable; + _transitioningPaint: Transitioning; + paint: PossiblyEvaluated; + + constructor(layer: LayerSpecification) { + super(layer, properties); } } diff --git a/src/style/style_layer/raster_style_layer_properties.js b/src/style/style_layer/raster_style_layer_properties.js new file mode 100644 index 00000000000..cf66430a380 --- /dev/null +++ b/src/style/style_layer/raster_style_layer_properties.js @@ -0,0 +1,38 @@ +// This file is generated. Edit build/generate-style-code.js, then run `node build/generate-style-code.js`. +// @flow +/* eslint-disable */ + +const styleSpec = require('../../style-spec/reference/latest'); + +const { + Properties, + DataConstantProperty, + DataDrivenProperty, + CrossFadedProperty, + HeatmapColorProperty +} = require('../properties'); + +import type Color from '../../style-spec/util/color'; + + +export type PaintProps = {| + "raster-opacity": DataConstantProperty, + "raster-hue-rotate": DataConstantProperty, + "raster-brightness-min": DataConstantProperty, + "raster-brightness-max": DataConstantProperty, + "raster-saturation": DataConstantProperty, + "raster-contrast": DataConstantProperty, + "raster-fade-duration": DataConstantProperty, +|}; + +const paint: Properties = new Properties({ + "raster-opacity": new DataConstantProperty(styleSpec["paint_raster"]["raster-opacity"]), + "raster-hue-rotate": new DataConstantProperty(styleSpec["paint_raster"]["raster-hue-rotate"]), + "raster-brightness-min": new DataConstantProperty(styleSpec["paint_raster"]["raster-brightness-min"]), + "raster-brightness-max": new DataConstantProperty(styleSpec["paint_raster"]["raster-brightness-max"]), + "raster-saturation": new DataConstantProperty(styleSpec["paint_raster"]["raster-saturation"]), + "raster-contrast": new DataConstantProperty(styleSpec["paint_raster"]["raster-contrast"]), + "raster-fade-duration": new DataConstantProperty(styleSpec["paint_raster"]["raster-fade-duration"]), +}); + +module.exports = { paint }; diff --git a/src/style/style_layer/symbol_style_layer.js b/src/style/style_layer/symbol_style_layer.js index b1c29b7d6d8..64dcfa17e59 100644 --- a/src/style/style_layer/symbol_style_layer.js +++ b/src/style/style_layer/symbol_style_layer.js @@ -5,49 +5,64 @@ const SymbolBucket = require('../../data/bucket/symbol_bucket'); const resolveTokens = require('../../util/token'); const {isExpression} = require('../../style-spec/expression'); const assert = require('assert'); +const properties = require('./symbol_style_layer_properties'); + +const { + Transitionable, + Transitioning, + Layout, + PossiblyEvaluated +} = require('../properties'); -import type {Feature, GlobalProperties} from '../../style-spec/expression'; import type {BucketParameters} from '../../data/bucket'; +import type {LayoutProps, PaintProps} from './symbol_style_layer_properties'; +import type {Feature} from '../../style-spec/expression'; +import type {EvaluationParameters} from '../properties'; class SymbolStyleLayer extends StyleLayer { + _unevaluatedLayout: Layout; + layout: PossiblyEvaluated; - getLayoutValue(name: string, globals: GlobalProperties, feature?: Feature): any { - const value = super.getLayoutValue(name, globals, feature); - if (value !== 'auto') { - return value; - } + _transitionablePaint: Transitionable; + _transitioningPaint: Transitioning; + paint: PossiblyEvaluated; - switch (name) { - case 'text-rotation-alignment': - case 'icon-rotation-alignment': - return this.getLayoutValue('symbol-placement', globals, feature) === 'line' ? 'map' : 'viewport'; - case 'text-pitch-alignment': - return this.getLayoutValue('text-rotation-alignment', globals, feature); - case 'icon-pitch-alignment': - return this.getLayoutValue('icon-rotation-alignment', globals, feature); - default: - return value; - } + constructor(layer: LayerSpecification) { + super(layer, properties); } - getLayoutDeclaration(name: string) { - return this._layoutDeclarations[name]; - } + recalculate(parameters: EvaluationParameters) { + super.recalculate(parameters); - isLayoutValueFeatureConstant(name: string) { - const declaration = this._layoutDeclarations[name]; - return !declaration || declaration.isFeatureConstant(); - } + if (this.layout.get('icon-rotation-alignment') === 'auto') { + if (this.layout.get('symbol-placement') === 'line') { + this.layout._values['icon-rotation-alignment'] = 'map'; + } else { + this.layout._values['icon-rotation-alignment'] = 'viewport'; + } + } - isLayoutValueZoomConstant(name: string) { - const declaration = this._layoutDeclarations[name]; - return !declaration || declaration.isZoomConstant(); + if (this.layout.get('text-rotation-alignment') === 'auto') { + if (this.layout.get('symbol-placement') === 'line') { + this.layout._values['text-rotation-alignment'] = 'map'; + } else { + this.layout._values['text-rotation-alignment'] = 'viewport'; + } + } + + // If unspecified, `*-pitch-alignment` inherits `*-rotation-alignment` + if (this.layout.get('text-pitch-alignment') === 'auto') { + this.layout._values['text-pitch-alignment'] = this.layout.get('text-rotation-alignment'); + } + if (this.layout.get('icon-pitch-alignment') === 'auto') { + this.layout._values['icon-pitch-alignment'] = this.layout.get('icon-rotation-alignment'); + } } - getValueAndResolveTokens(name: 'text-field' | 'icon-image', globals: GlobalProperties, feature: Feature) { - const value = this.getLayoutValue(name, globals, feature); - const declaration = this._layoutDeclarations[name]; - if (this.isLayoutValueFeatureConstant(name) && !isExpression(declaration.value)) { + getValueAndResolveTokens(name: *, feature: Feature) { + const value = this.layout.get(name).evaluate(feature); + const unevaluated = this._unevaluatedLayout._values[name]; + if (!unevaluated.isDataDriven() && !isExpression(unevaluated.value)) { return resolveTokens(feature.properties, value); } @@ -60,11 +75,6 @@ class SymbolStyleLayer extends StyleLayer { return (new SymbolBucket((parameters: any)): any); } - isOpacityZero(zoom: number, property: string) { - return this.isPaintValueFeatureConstant(property) && - this.getPaintValue(property, { zoom: zoom }) === 0; - } - queryRadius(): number { return 0; } diff --git a/src/style/style_layer/symbol_style_layer_properties.js b/src/style/style_layer/symbol_style_layer_properties.js new file mode 100644 index 00000000000..26445d387be --- /dev/null +++ b/src/style/style_layer/symbol_style_layer_properties.js @@ -0,0 +1,129 @@ +// This file is generated. Edit build/generate-style-code.js, then run `node build/generate-style-code.js`. +// @flow +/* eslint-disable */ + +const styleSpec = require('../../style-spec/reference/latest'); + +const { + Properties, + DataConstantProperty, + DataDrivenProperty, + CrossFadedProperty, + HeatmapColorProperty +} = require('../properties'); + +import type Color from '../../style-spec/util/color'; + +export type LayoutProps = {| + "symbol-placement": DataConstantProperty<"point" | "line">, + "symbol-spacing": DataConstantProperty, + "symbol-avoid-edges": DataConstantProperty, + "icon-allow-overlap": DataConstantProperty, + "icon-ignore-placement": DataConstantProperty, + "icon-optional": DataConstantProperty, + "icon-rotation-alignment": DataConstantProperty<"map" | "viewport" | "auto">, + "icon-size": DataDrivenProperty, + "icon-text-fit": DataConstantProperty<"none" | "width" | "height" | "both">, + "icon-text-fit-padding": DataConstantProperty<[number, number, number, number]>, + "icon-image": DataDrivenProperty, + "icon-rotate": DataDrivenProperty, + "icon-padding": DataConstantProperty, + "icon-keep-upright": DataConstantProperty, + "icon-offset": DataDrivenProperty<[number, number]>, + "icon-anchor": DataDrivenProperty<"center" | "left" | "right" | "top" | "bottom" | "top-left" | "top-right" | "bottom-left" | "bottom-right">, + "icon-pitch-alignment": DataConstantProperty<"map" | "viewport" | "auto">, + "text-pitch-alignment": DataConstantProperty<"map" | "viewport" | "auto">, + "text-rotation-alignment": DataConstantProperty<"map" | "viewport" | "auto">, + "text-field": DataDrivenProperty, + "text-font": DataConstantProperty>, + "text-size": DataDrivenProperty, + "text-max-width": DataDrivenProperty, + "text-line-height": DataConstantProperty, + "text-letter-spacing": DataDrivenProperty, + "text-justify": DataDrivenProperty<"left" | "center" | "right">, + "text-anchor": DataDrivenProperty<"center" | "left" | "right" | "top" | "bottom" | "top-left" | "top-right" | "bottom-left" | "bottom-right">, + "text-max-angle": DataConstantProperty, + "text-rotate": DataDrivenProperty, + "text-padding": DataConstantProperty, + "text-keep-upright": DataConstantProperty, + "text-transform": DataDrivenProperty<"none" | "uppercase" | "lowercase">, + "text-offset": DataDrivenProperty<[number, number]>, + "text-allow-overlap": DataConstantProperty, + "text-ignore-placement": DataConstantProperty, + "text-optional": DataConstantProperty, +|}; + +const layout: Properties = new Properties({ + "symbol-placement": new DataConstantProperty(styleSpec["layout_symbol"]["symbol-placement"]), + "symbol-spacing": new DataConstantProperty(styleSpec["layout_symbol"]["symbol-spacing"]), + "symbol-avoid-edges": new DataConstantProperty(styleSpec["layout_symbol"]["symbol-avoid-edges"]), + "icon-allow-overlap": new DataConstantProperty(styleSpec["layout_symbol"]["icon-allow-overlap"]), + "icon-ignore-placement": new DataConstantProperty(styleSpec["layout_symbol"]["icon-ignore-placement"]), + "icon-optional": new DataConstantProperty(styleSpec["layout_symbol"]["icon-optional"]), + "icon-rotation-alignment": new DataConstantProperty(styleSpec["layout_symbol"]["icon-rotation-alignment"]), + "icon-size": new DataDrivenProperty(styleSpec["layout_symbol"]["icon-size"]), + "icon-text-fit": new DataConstantProperty(styleSpec["layout_symbol"]["icon-text-fit"]), + "icon-text-fit-padding": new DataConstantProperty(styleSpec["layout_symbol"]["icon-text-fit-padding"]), + "icon-image": new DataDrivenProperty(styleSpec["layout_symbol"]["icon-image"]), + "icon-rotate": new DataDrivenProperty(styleSpec["layout_symbol"]["icon-rotate"]), + "icon-padding": new DataConstantProperty(styleSpec["layout_symbol"]["icon-padding"]), + "icon-keep-upright": new DataConstantProperty(styleSpec["layout_symbol"]["icon-keep-upright"]), + "icon-offset": new DataDrivenProperty(styleSpec["layout_symbol"]["icon-offset"]), + "icon-anchor": new DataDrivenProperty(styleSpec["layout_symbol"]["icon-anchor"]), + "icon-pitch-alignment": new DataConstantProperty(styleSpec["layout_symbol"]["icon-pitch-alignment"]), + "text-pitch-alignment": new DataConstantProperty(styleSpec["layout_symbol"]["text-pitch-alignment"]), + "text-rotation-alignment": new DataConstantProperty(styleSpec["layout_symbol"]["text-rotation-alignment"]), + "text-field": new DataDrivenProperty(styleSpec["layout_symbol"]["text-field"]), + "text-font": new DataConstantProperty(styleSpec["layout_symbol"]["text-font"]), + "text-size": new DataDrivenProperty(styleSpec["layout_symbol"]["text-size"]), + "text-max-width": new DataDrivenProperty(styleSpec["layout_symbol"]["text-max-width"]), + "text-line-height": new DataConstantProperty(styleSpec["layout_symbol"]["text-line-height"]), + "text-letter-spacing": new DataDrivenProperty(styleSpec["layout_symbol"]["text-letter-spacing"]), + "text-justify": new DataDrivenProperty(styleSpec["layout_symbol"]["text-justify"]), + "text-anchor": new DataDrivenProperty(styleSpec["layout_symbol"]["text-anchor"]), + "text-max-angle": new DataConstantProperty(styleSpec["layout_symbol"]["text-max-angle"]), + "text-rotate": new DataDrivenProperty(styleSpec["layout_symbol"]["text-rotate"]), + "text-padding": new DataConstantProperty(styleSpec["layout_symbol"]["text-padding"]), + "text-keep-upright": new DataConstantProperty(styleSpec["layout_symbol"]["text-keep-upright"]), + "text-transform": new DataDrivenProperty(styleSpec["layout_symbol"]["text-transform"]), + "text-offset": new DataDrivenProperty(styleSpec["layout_symbol"]["text-offset"]), + "text-allow-overlap": new DataConstantProperty(styleSpec["layout_symbol"]["text-allow-overlap"]), + "text-ignore-placement": new DataConstantProperty(styleSpec["layout_symbol"]["text-ignore-placement"]), + "text-optional": new DataConstantProperty(styleSpec["layout_symbol"]["text-optional"]), +}); + +export type PaintProps = {| + "icon-opacity": DataDrivenProperty, + "icon-color": DataDrivenProperty, + "icon-halo-color": DataDrivenProperty, + "icon-halo-width": DataDrivenProperty, + "icon-halo-blur": DataDrivenProperty, + "icon-translate": DataConstantProperty<[number, number]>, + "icon-translate-anchor": DataConstantProperty<"map" | "viewport">, + "text-opacity": DataDrivenProperty, + "text-color": DataDrivenProperty, + "text-halo-color": DataDrivenProperty, + "text-halo-width": DataDrivenProperty, + "text-halo-blur": DataDrivenProperty, + "text-translate": DataConstantProperty<[number, number]>, + "text-translate-anchor": DataConstantProperty<"map" | "viewport">, +|}; + +const paint: Properties = new Properties({ + "icon-opacity": new DataDrivenProperty(styleSpec["paint_symbol"]["icon-opacity"]), + "icon-color": new DataDrivenProperty(styleSpec["paint_symbol"]["icon-color"]), + "icon-halo-color": new DataDrivenProperty(styleSpec["paint_symbol"]["icon-halo-color"]), + "icon-halo-width": new DataDrivenProperty(styleSpec["paint_symbol"]["icon-halo-width"]), + "icon-halo-blur": new DataDrivenProperty(styleSpec["paint_symbol"]["icon-halo-blur"]), + "icon-translate": new DataConstantProperty(styleSpec["paint_symbol"]["icon-translate"]), + "icon-translate-anchor": new DataConstantProperty(styleSpec["paint_symbol"]["icon-translate-anchor"]), + "text-opacity": new DataDrivenProperty(styleSpec["paint_symbol"]["text-opacity"]), + "text-color": new DataDrivenProperty(styleSpec["paint_symbol"]["text-color"]), + "text-halo-color": new DataDrivenProperty(styleSpec["paint_symbol"]["text-halo-color"]), + "text-halo-width": new DataDrivenProperty(styleSpec["paint_symbol"]["text-halo-width"]), + "text-halo-blur": new DataDrivenProperty(styleSpec["paint_symbol"]["text-halo-blur"]), + "text-translate": new DataConstantProperty(styleSpec["paint_symbol"]["text-translate"]), + "text-translate-anchor": new DataConstantProperty(styleSpec["paint_symbol"]["text-translate-anchor"]), +}); + +module.exports = { paint, layout }; diff --git a/src/style/style_layer_index.js b/src/style/style_layer_index.js index 24cb00ec225..16db53cfe14 100644 --- a/src/style/style_layer_index.js +++ b/src/style/style_layer_index.js @@ -30,7 +30,6 @@ class StyleLayerIndex { this._layerConfigs[layerConfig.id] = layerConfig; const layer = this._layers[layerConfig.id] = StyleLayer.create(layerConfig); - layer.updatePaintTransitions({transition: false}); layer._featureFilter = featureFilter(layer.filter); } for (const id of removedIds) { @@ -46,7 +45,7 @@ class StyleLayerIndex { const layers = layerConfigs.map((layerConfig) => this._layers[layerConfig.id]); const layer = layers[0]; - if (layer.layout && layer.layout.visibility === 'none') { + if (layer.visibility === 'none') { continue; } diff --git a/src/style/style_transition.js b/src/style/style_transition.js deleted file mode 100644 index 9e9969ea86f..00000000000 --- a/src/style/style_transition.js +++ /dev/null @@ -1,113 +0,0 @@ -// @flow - -const util = require('../util/util'); -const interpolate = require('../style-spec/util/interpolate'); - -import type StyleDeclaration from './style_declaration'; -import type {Feature, GlobalProperties} from '../style-spec/expression'; - -const fakeZoomHistory = { lastIntegerZoom: 0, lastIntegerZoomTime: 0, lastZoom: 0 }; - -/* - * Represents a transition between two declarations - */ -class StyleTransition { - declaration: StyleDeclaration; - startTime: number; - endTime: number; - oldTransition: ?StyleTransition; - duration: number; - delay: number; - zoomTransitioned: boolean; - interp: Function; - zoomHistory: any; - loopID: any; - - constructor(reference: any, - declaration: StyleDeclaration, - oldTransition: ?StyleTransition, - options: TransitionSpecification, - zoomHistory?: {}) { - this.declaration = declaration; - this.startTime = this.endTime = (new Date()).getTime(); - - this.oldTransition = oldTransition; - this.duration = options.duration || 0; - this.delay = options.delay || 0; - - this.zoomTransitioned = reference.function === 'piecewise-constant' && reference.transition; - this.interp = this.zoomTransitioned ? interpZoomTransitioned : interpolate[reference.type]; - this.zoomHistory = zoomHistory || fakeZoomHistory; - - if (!this.instant()) { - this.endTime = this.startTime + this.duration + this.delay; - } - - if (oldTransition && oldTransition.endTime <= this.startTime) { - // Old transition is done running, so we can - // delete its reference to its old transition. - delete oldTransition.oldTransition; - } - } - - instant() { - return !this.oldTransition || !this.interp || (this.duration === 0 && this.delay === 0); - } - - /* - * Return the value of the transitioning property. - */ - calculate(globals: GlobalProperties, feature?: Feature, time?: number) { - const value = this._calculateTargetValue(globals, feature); - - if (this.instant()) - return value; - - time = time || Date.now(); - - if (time >= this.endTime) - return value; - - const oldValue = (this.oldTransition: any).calculate(globals, feature, this.startTime); - const t = util.easeCubicInOut((time - this.startTime - this.delay) / this.duration); - return this.interp(oldValue, value, t); - } - - _calculateTargetValue(globals: GlobalProperties, feature?: Feature) { - if (!this.zoomTransitioned) - return this.declaration.calculate(globals, feature); - - // calculate zoom transition between discrete values, such as images and dasharrays. - const z = globals.zoom; - const lastIntegerZoom = this.zoomHistory.lastIntegerZoom; - - const fromScale = z > lastIntegerZoom ? 2 : 0.5; - const from = this.declaration.calculate({zoom: z > lastIntegerZoom ? z - 1 : z + 1}, feature); - const to = this.declaration.calculate({zoom: z}, feature); - - const timeFraction = Math.min((Date.now() - this.zoomHistory.lastIntegerZoomTime) / this.duration, 1); - const zoomFraction = Math.abs(z - lastIntegerZoom); - const t = interpolate.number(timeFraction, 1, zoomFraction); - - if (from === undefined || to === undefined) - return undefined; - - return { from, fromScale, to, toScale: 1, t }; - } -} - -module.exports = StyleTransition; - -// interpolate between two values that transition with zoom, such as images and dasharrays -function interpZoomTransitioned(from, to, t) { - if (from === undefined || to === undefined) - return undefined; - - return { - from: from.to, - fromScale: from.toScale, - to: to.to, - toScale: to.toScale, - t: t - }; -} diff --git a/src/symbol/projection.js b/src/symbol/projection.js index 30a793d83e7..705df389324 100644 --- a/src/symbol/projection.js +++ b/src/symbol/projection.js @@ -4,9 +4,9 @@ const Point = require('@mapbox/point-geometry'); const {mat4, vec4} = require('@mapbox/gl-matrix'); const symbolSize = require('./symbol_size'); const {addDynamicAttributes} = require('../data/bucket/symbol_bucket'); +const symbolLayoutProperties = require('../style/style_layer/symbol_style_layer_properties').layout; import type Painter from '../render/painter'; -import type SymbolStyleLayer from '../style/style_layer/symbol_style_layer'; import type Transform from '../geo/transform'; import type SymbolBucket from '../data/bucket/symbol_bucket'; const WritingMode = require('../symbol/shaping').WritingMode; @@ -144,12 +144,11 @@ function updateLineLabels(bucket: SymbolBucket, labelPlaneMatrix: mat4, glCoordMatrix: mat4, pitchWithMap: boolean, - keepUpright: boolean, - pixelsToTileUnits: number, - layer: SymbolStyleLayer) { + keepUpright: boolean) { const sizeData = isText ? bucket.textSizeData : bucket.iconSizeData; - const partiallyEvaluatedSize = symbolSize.evaluateSizeForZoom(sizeData, painter.transform, layer, isText); + const partiallyEvaluatedSize = symbolSize.evaluateSizeForZoom(sizeData, painter.transform.zoom, + symbolLayoutProperties.properties[isText ? 'text-size' : 'icon-size']); const clippingBuffer = [256 / painter.width * 2 + 1, 256 / painter.height * 2 + 1]; diff --git a/src/symbol/quads.js b/src/symbol/quads.js index 1d25ecd8222..bda42f9b24c 100644 --- a/src/symbol/quads.js +++ b/src/symbol/quads.js @@ -5,7 +5,7 @@ const {GLYPH_PBF_BORDER} = require('../style/parse_glyph_pbf'); import type Anchor from './anchor'; import type {PositionedIcon, Shaping} from './shaping'; -import type StyleLayer from '../style/style_layer'; +import type SymbolStyleLayer from '../style/style_layer/symbol_style_layer'; import type {Feature} from '../style-spec/expression'; import type {GlyphPosition} from '../render/glyph_atlas'; @@ -48,7 +48,7 @@ export type SymbolQuad = { */ function getIconQuads(anchor: Anchor, shapedIcon: PositionedIcon, - layer: StyleLayer, + layer: SymbolStyleLayer, alongLine: boolean, shapedText: Shaping, globalProperties: Object, @@ -68,24 +68,24 @@ function getIconQuads(anchor: Anchor, let tl, tr, br, bl; // text-fit mode - if (layout['icon-text-fit'] !== 'none' && shapedText) { + if (layout.get('icon-text-fit') !== 'none' && shapedText) { const iconWidth = (right - left), iconHeight = (bottom - top), - size = layer.getLayoutValue('text-size', globalProperties, feature) / 24, + size = layout.get('text-size').evaluate(feature) / 24, textLeft = shapedText.left * size, textRight = shapedText.right * size, textTop = shapedText.top * size, textBottom = shapedText.bottom * size, textWidth = textRight - textLeft, textHeight = textBottom - textTop, - padT = layout['icon-text-fit-padding'][0], - padR = layout['icon-text-fit-padding'][1], - padB = layout['icon-text-fit-padding'][2], - padL = layout['icon-text-fit-padding'][3], - offsetY = layout['icon-text-fit'] === 'width' ? (textHeight - iconHeight) * 0.5 : 0, - offsetX = layout['icon-text-fit'] === 'height' ? (textWidth - iconWidth) * 0.5 : 0, - width = layout['icon-text-fit'] === 'width' || layout['icon-text-fit'] === 'both' ? textWidth : iconWidth, - height = layout['icon-text-fit'] === 'height' || layout['icon-text-fit'] === 'both' ? textHeight : iconHeight; + padT = layout.get('icon-text-fit-padding')[0], + padR = layout.get('icon-text-fit-padding')[1], + padB = layout.get('icon-text-fit-padding')[2], + padL = layout.get('icon-text-fit-padding')[3], + offsetY = layout.get('icon-text-fit') === 'width' ? (textHeight - iconHeight) * 0.5 : 0, + offsetX = layout.get('icon-text-fit') === 'height' ? (textWidth - iconWidth) * 0.5 : 0, + width = layout.get('icon-text-fit') === 'width' || layout.get('icon-text-fit') === 'both' ? textWidth : iconWidth, + height = layout.get('icon-text-fit') === 'height' || layout.get('icon-text-fit') === 'both' ? textHeight : iconHeight; tl = new Point(textLeft + offsetX - padL, textTop + offsetY - padT); tr = new Point(textLeft + offsetX + padR + width, textTop + offsetY - padT); br = new Point(textLeft + offsetX + padR + width, textTop + offsetY + padB + height); @@ -98,7 +98,7 @@ function getIconQuads(anchor: Anchor, bl = new Point(left, bottom); } - const angle = layer.getLayoutValue('icon-rotate', globalProperties, feature) * Math.PI / 180; + const angle = layer.layout.get('icon-rotate').evaluate(feature) * Math.PI / 180; if (angle) { const sin = Math.sin(angle), @@ -128,15 +128,15 @@ function getIconQuads(anchor: Anchor, */ function getGlyphQuads(anchor: Anchor, shaping: Shaping, - layer: StyleLayer, + layer: SymbolStyleLayer, alongLine: boolean, globalProperties: Object, feature: Feature, positions: {[number]: GlyphPosition}): Array { const oneEm = 24; - const textRotate = layer.getLayoutValue('text-rotate', globalProperties, feature) * Math.PI / 180; - const textOffset = layer.getLayoutValue('text-offset', globalProperties, feature).map((t)=> t * oneEm); + const textRotate = layer.layout.get('text-rotate').evaluate(feature) * Math.PI / 180; + const textOffset = layer.layout.get('text-offset').evaluate(feature).map((t)=> t * oneEm); const positionedGlyphs = shaping.positionedGlyphs; const quads = []; diff --git a/src/symbol/symbol_layout.js b/src/symbol/symbol_layout.js index 3e827e2cd97..d17f9a503fd 100644 --- a/src/symbol/symbol_layout.js +++ b/src/symbol/symbol_layout.js @@ -14,7 +14,6 @@ const EXTENT = require('../data/extent'); const SymbolBucket = require('../data/bucket/symbol_bucket'); import type {Shaping, PositionedIcon} from './shaping'; -import type {SizeData} from './symbol_size'; import type CollisionBoxArray from './collision_box'; import type {SymbolFeature} from '../data/bucket/symbol_bucket'; import type {StyleImage} from '../style/style_image'; @@ -46,10 +45,10 @@ function performSymbolLayout(bucket: SymbolBucket, const layout = bucket.layers[0].layout; const oneEm = 24; - const lineHeight = layout['text-line-height'] * oneEm; - const fontstack = layout['text-font'].join(','); - const textAlongLine = layout['text-rotation-alignment'] === 'map' && layout['symbol-placement'] === 'line'; - const keepUpright = layout['text-keep-upright']; + const lineHeight = layout.get('text-line-height') * oneEm; + const fontstack = layout.get('text-font').join(','); + const textAlongLine = layout.get('text-rotation-alignment') === 'map' && layout.get('symbol-placement') === 'line'; + const keepUpright = layout.get('text-keep-upright'); const glyphs = glyphMap[fontstack] || {}; const glyphPositionMap = glyphPositions[fontstack] || {}; @@ -60,13 +59,13 @@ function performSymbolLayout(bucket: SymbolBucket, const text = feature.text; if (text) { const allowsVerticalWritingMode = scriptDetection.allowsVerticalWritingMode(text); - const textOffset = bucket.layers[0].getLayoutValue('text-offset', {zoom: bucket.zoom}, feature).map((t)=> t * oneEm); - const spacing = bucket.layers[0].getLayoutValue('text-letter-spacing', {zoom: bucket.zoom}, feature) * oneEm; + const textOffset: [number, number] = (layout.get('text-offset').evaluate(feature).map((t)=> t * oneEm): any); + const spacing = layout.get('text-letter-spacing').evaluate(feature) * oneEm; const spacingIfAllowed = scriptDetection.allowsLetterSpacing(text) ? spacing : 0; - const textAnchor = bucket.layers[0].getLayoutValue('text-anchor', {zoom: bucket.zoom}, feature); - const textJustify = bucket.layers[0].getLayoutValue('text-justify', {zoom: bucket.zoom}, feature); - const maxWidth = layout['symbol-placement'] !== 'line' ? - bucket.layers[0].getLayoutValue('text-max-width', {zoom: bucket.zoom}, feature) * oneEm : + const textAnchor = layout.get('text-anchor').evaluate(feature); + const textJustify = layout.get('text-justify').evaluate(feature); + const maxWidth = layout.get('symbol-placement') !== 'line' ? + layout.get('text-max-width').evaluate(feature) * oneEm : 0; shapedTextOrientations.horizontal = shapeText(text, glyphs, maxWidth, lineHeight, textAnchor, textJustify, spacingIfAllowed, textOffset, oneEm, WritingMode.horizontal); @@ -81,8 +80,8 @@ function performSymbolLayout(bucket: SymbolBucket, if (image) { shapedIcon = shapeIcon( imagePositions[feature.icon], - bucket.layers[0].getLayoutValue('icon-offset', {zoom: bucket.zoom}, feature), - bucket.layers[0].getLayoutValue('icon-anchor', {zoom: bucket.zoom}, feature)); + layout.get('icon-offset').evaluate(feature), + layout.get('icon-anchor').evaluate(feature)); if (bucket.sdfIcons === undefined) { bucket.sdfIcons = image.sdf; } else if (bucket.sdfIcons !== image.sdf) { @@ -90,7 +89,7 @@ function performSymbolLayout(bucket: SymbolBucket, } if (image.pixelRatio !== bucket.pixelRatio) { bucket.iconsNeedLinear = true; - } else if (layout['icon-rotate'] !== 0 || !bucket.layers[0].isLayoutValueFeatureConstant('icon-rotate')) { + } else if (layout.get('icon-rotate').constantOr(1) !== 0) { bucket.iconsNeedLinear = true; } } @@ -119,34 +118,34 @@ function addFeature(bucket: SymbolBucket, shapedTextOrientations: any, shapedIcon: PositionedIcon | void, glyphPositionMap: {[number]: GlyphPosition}) { - const layoutTextSize = bucket.layers[0].getLayoutValue('text-size', {zoom: bucket.zoom + 1}, feature); - const layoutIconSize = bucket.layers[0].getLayoutValue('icon-size', {zoom: bucket.zoom + 1}, feature); - - const textOffset = bucket.layers[0].getLayoutValue('text-offset', {zoom: bucket.zoom }, feature); - const iconOffset = bucket.layers[0].getLayoutValue('icon-offset', {zoom: bucket.zoom }, feature); + const layoutTextSize = bucket.layoutTextSize.evaluate(feature); + const layoutIconSize = bucket.layoutIconSize.evaluate(feature); // To reduce the number of labels that jump around when zooming we need // to use a text-size value that is the same for all zoom levels. // bucket calculates text-size at a high zoom level so that all tiles can // use the same value when calculating anchor positions. - let textMaxSize = bucket.layers[0].getLayoutValue('text-size', {zoom: 18}, feature); + let textMaxSize = bucket.textMaxSize.evaluate(feature); if (textMaxSize === undefined) { textMaxSize = layoutTextSize; } - const layout = bucket.layers[0].layout, - glyphSize = 24, + const layout = bucket.layers[0].layout; + const textOffset = layout.get('text-offset').evaluate(feature); + const iconOffset = layout.get('icon-offset').evaluate(feature); + + const glyphSize = 24, fontScale = layoutTextSize / glyphSize, textBoxScale = bucket.tilePixelRatio * fontScale, textMaxBoxScale = bucket.tilePixelRatio * textMaxSize / glyphSize, iconBoxScale = bucket.tilePixelRatio * layoutIconSize, - symbolMinDistance = bucket.tilePixelRatio * layout['symbol-spacing'], - textPadding = layout['text-padding'] * bucket.tilePixelRatio, - iconPadding = layout['icon-padding'] * bucket.tilePixelRatio, - textMaxAngle = layout['text-max-angle'] / 180 * Math.PI, - textAlongLine = layout['text-rotation-alignment'] === 'map' && layout['symbol-placement'] === 'line', - iconAlongLine = layout['icon-rotation-alignment'] === 'map' && layout['symbol-placement'] === 'line', - symbolPlacement = layout['symbol-placement'], + symbolMinDistance = bucket.tilePixelRatio * layout.get('symbol-spacing'), + textPadding = layout.get('text-padding') * bucket.tilePixelRatio, + iconPadding = layout.get('icon-padding') * bucket.tilePixelRatio, + textMaxAngle = layout.get('text-max-angle') / 180 * Math.PI, + textAlongLine = layout.get('text-rotation-alignment') === 'map' && layout.get('symbol-placement') === 'line', + iconAlongLine = layout.get('icon-rotation-alignment') === 'map' && layout.get('symbol-placement') === 'line', + symbolPlacement = layout.get('symbol-placement'), textRepeatDistance = symbolMinDistance / 2; const addSymbolAtAnchor = (line, anchor) => { @@ -219,11 +218,19 @@ function addTextVertices(bucket: SymbolBucket, const glyphQuads = getGlyphQuads(anchor, shapedText, layer, textAlongLine, globalProperties, feature, glyphPositionMap); - const textSizeData = getSizeVertexData(layer, - bucket.zoom, - bucket.textSizeData, - 'text-size', - feature); + const sizeData = bucket.textSizeData; + let textSizeData = null; + + if (sizeData.functionType === 'source') { + textSizeData = [ + 10 * layer.layout.get('text-size').evaluate(feature) + ]; + } else if (sizeData.functionType === 'composite') { + textSizeData = [ + 10 * bucket.compositeTextSizes[0].evaluate(feature), + 10 * bucket.compositeTextSizes[1].evaluate(feature) + ]; + } bucket.addSymbols( bucket.text, @@ -303,11 +310,19 @@ function addSymbol(bucket: SymbolBucket, numIconVertices = iconQuads.length * 4; - const iconSizeData = getSizeVertexData(layer, - bucket.zoom, - bucket.iconSizeData, - 'icon-size', - feature); + const sizeData = bucket.iconSizeData; + let iconSizeData = null; + + if (sizeData.functionType === 'source') { + iconSizeData = [ + 10 * layer.layout.get('icon-size').evaluate(feature) + ]; + } else if (sizeData.functionType === 'composite') { + iconSizeData = [ + 10 * bucket.compositeIconSizes[0].evaluate(feature), + 10 * bucket.compositeIconSizes[1].evaluate(feature) + ]; + } bucket.addSymbols( bucket.icon, @@ -370,18 +385,3 @@ function anchorIsTooClose(bucket: any, text: string, repeatDistance: number, anc compareText[text].push(anchor); return false; } - -function getSizeVertexData(layer: SymbolStyleLayer, tileZoom: number, sizeData: SizeData, sizeProperty: string, feature: SymbolFeature) { - if (sizeData.functionType === 'source') { - return [ - 10 * layer.getLayoutValue(sizeProperty, ({}: any), feature) - ]; - } else if (sizeData.functionType === 'composite') { - const zoomRange = sizeData.coveringZoomRange; - return [ - 10 * layer.getLayoutValue(sizeProperty, {zoom: zoomRange[0]}, feature), - 10 * layer.getLayoutValue(sizeProperty, {zoom: zoomRange[1]}, feature) - ]; - } - return null; -} diff --git a/src/symbol/symbol_placement.js b/src/symbol/symbol_placement.js index 69368921a25..629a0aad542 100644 --- a/src/symbol/symbol_placement.js +++ b/src/symbol/symbol_placement.js @@ -1,6 +1,7 @@ // @flow const symbolSize = require('./symbol_size'); +const symbolLayoutProperties = require('../style/style_layer/symbol_style_layer_properties').layout; import type SymbolBucket, {SymbolInstance} from '../data/bucket/symbol_bucket'; import type OpacityState from './opacity_state'; @@ -152,20 +153,22 @@ function performSymbolPlacement(bucket: SymbolBucket, collisionIndex: CollisionI } } - const partiallyEvaluatedTextSize = symbolSize.evaluateSizeForZoom(bucket.textSizeData, collisionIndex.transform, layer, true); - const pitchWithMap = bucket.layers[0].layout['text-pitch-alignment'] === 'map'; + const partiallyEvaluatedTextSize = symbolSize.evaluateSizeForZoom(bucket.textSizeData, collisionIndex.transform.zoom, + symbolLayoutProperties.properties['text-size']); + const pitchWithMap = layout.get('text-pitch-alignment') === 'map'; for (const symbolInstance of bucket.symbolInstances) { const hasText = !(symbolInstance.textBoxStartIndex === symbolInstance.textBoxEndIndex); const hasIcon = !(symbolInstance.iconBoxStartIndex === symbolInstance.iconBoxEndIndex); + + const iconWithoutText = layout.get('text-optional') || !hasText, + textWithoutIcon = layout.get('icon-optional') || !hasIcon; + if (!symbolInstance.collisionArrays) { symbolInstance.collisionArrays = bucket.deserializeCollisionBoxes(collisionBoxArray, symbolInstance.textBoxStartIndex, symbolInstance.textBoxEndIndex, symbolInstance.iconBoxStartIndex, symbolInstance.iconBoxEndIndex); } - const iconWithoutText = layout['text-optional'] || !hasText, - textWithoutIcon = layout['icon-optional'] || !hasIcon; - let placedGlyphBox = []; let placedIconBox = []; let placedGlyphCircles = []; @@ -181,12 +184,12 @@ function performSymbolPlacement(bucket: SymbolBucket, collisionIndex: CollisionI // starts rendering as close as possible to its final state? if (symbolInstance.collisionArrays.textBox) { placedGlyphBox = collisionIndex.placeCollisionBox(symbolInstance.collisionArrays.textBox, - layout['text-allow-overlap'], textPixelRatio, posMatrix); + layout.get('text-allow-overlap'), textPixelRatio, posMatrix); } if (symbolInstance.collisionArrays.iconBox) { placedIconBox = collisionIndex.placeCollisionBox(symbolInstance.collisionArrays.iconBox, - layout['icon-allow-overlap'], textPixelRatio, posMatrix); + layout.get('icon-allow-overlap'), textPixelRatio, posMatrix); } const textCircles = symbolInstance.collisionArrays.textCircles; @@ -194,7 +197,7 @@ function performSymbolPlacement(bucket: SymbolBucket, collisionIndex: CollisionI const placedSymbol = (bucket.placedGlyphArray.get(symbolInstance.placedTextSymbolIndices[0]): any); const fontSize = symbolSize.evaluateSizeForFeature(bucket.textSizeData, partiallyEvaluatedTextSize, placedSymbol); placedGlyphCircles = collisionIndex.placeCollisionCircles(textCircles, - layout['text-allow-overlap'], + layout.get('text-allow-overlap'), scale, textPixelRatio, symbolInstance.key, @@ -210,7 +213,7 @@ function performSymbolPlacement(bucket: SymbolBucket, collisionIndex: CollisionI // In theory there should always be at least one circle placed // in this case, but for now quirks in text-anchor // and text-offset may prevent that from being true. - placedCircles = layout['text-allow-overlap'] || placedGlyphCircles.length > 0; + placedCircles = layout.get('text-allow-overlap') || placedGlyphCircles.length > 0; } } @@ -234,7 +237,7 @@ function performSymbolPlacement(bucket: SymbolBucket, collisionIndex: CollisionI updateCollisionBox(collisionDebugBoxArray, placeGlyph); } if (placeGlyph) { - collisionIndex.insertCollisionBox(placedGlyphBox, layout['text-ignore-placement'], tileID, sourceID, symbolInstance.textBoxStartIndex); + collisionIndex.insertCollisionBox(placedGlyphBox, layout.get('text-ignore-placement'), tileID, sourceID, symbolInstance.textBoxStartIndex); } } if (symbolInstance.collisionArrays.iconBox) { @@ -242,7 +245,7 @@ function performSymbolPlacement(bucket: SymbolBucket, collisionIndex: CollisionI updateCollisionBox(collisionDebugBoxArray, placeIcon); } if (placeIcon) { - collisionIndex.insertCollisionBox(placedIconBox, layout['icon-ignore-placement'], tileID, sourceID, symbolInstance.iconBoxStartIndex); + collisionIndex.insertCollisionBox(placedIconBox, layout.get('icon-ignore-placement'), tileID, sourceID, symbolInstance.iconBoxStartIndex); } } if (symbolInstance.collisionArrays.textCircles) { @@ -250,7 +253,7 @@ function performSymbolPlacement(bucket: SymbolBucket, collisionIndex: CollisionI updateCollisionCircles(collisionDebugCircleArray, symbolInstance.collisionArrays.textCircles, placeGlyph, symbolInstance.isDuplicate); } if (placeGlyph) { - collisionIndex.insertCollisionCircles(placedGlyphCircles, layout['text-ignore-placement'], tileID, sourceID, symbolInstance.textBoxStartIndex); + collisionIndex.insertCollisionCircles(placedGlyphCircles, layout.get('text-ignore-placement'), tileID, sourceID, symbolInstance.textBoxStartIndex); } } diff --git a/src/symbol/symbol_size.js b/src/symbol/symbol_size.js index 3bf1a9024c5..abaf68403dc 100644 --- a/src/symbol/symbol_size.js +++ b/src/symbol/symbol_size.js @@ -1,10 +1,11 @@ // @flow +const {normalizePropertyExpression} = require('../style-spec/expression'); const interpolate = require('../style-spec/util/interpolate'); const util = require('../util/util'); -import type SymbolStyleLayer from '../style/style_layer/symbol_style_layer'; -import type StyleDeclaration from '../style/style_declaration'; +import type {Property, PropertyValue, PossiblyEvaluatedPropertyValue} from '../style/properties'; +import type {CameraExpression, CompositeExpression} from '../style-spec/expression/index'; module.exports = { getSizeData, @@ -15,60 +16,72 @@ module.exports = { export type SizeData = { functionType: 'constant', layoutSize: number +} | { + functionType: 'source' } | { functionType: 'camera', layoutSize: number, - coveringZoomRange: [number, number], - coveringStopValues: [number, number] -} | { - functionType: 'source' + zoomRange: {min: number, max: number}, + sizeRange: {min: number, max: number}, + propertyValue: PropertyValueSpecification } | { functionType: 'composite', - coveringZoomRange: [number, number] + zoomRange: {min: number, max: number}, + propertyValue: PropertyValueSpecification }; // For {text,icon}-size, get the bucket-level data that will be needed by // the painter to set symbol-size-related uniforms -function getSizeData(tileZoom: number, layer: SymbolStyleLayer, sizeProperty: string): SizeData { - const declaration: StyleDeclaration = layer.getLayoutDeclaration(sizeProperty); - const isFeatureConstant = !declaration || declaration.isFeatureConstant(); - - if (!declaration || declaration.isZoomConstant()) { - return isFeatureConstant ? { +function getSizeData(tileZoom: number, value: PropertyValue>): SizeData { + const {expression} = value; + if (expression.kind === 'constant') { + return { functionType: 'constant', - layoutSize: layer.getLayoutValue(sizeProperty, {zoom: tileZoom + 1}) - } : { functionType: 'source' }; - } - - // calculate covering zoom stops for zoom-dependent values - const levels = (declaration.expression: any).zoomStops; - - let lower = 0; - while (lower < levels.length && levels[lower] <= tileZoom) lower++; - lower = Math.max(0, lower - 1); - let upper = lower; - while (upper < levels.length && levels[upper] < tileZoom + 1) upper++; - upper = Math.min(levels.length - 1, upper); - - const coveringZoomRange: [number, number] = [levels[lower], levels[upper]]; - - if (!isFeatureConstant) { + layoutSize: expression.evaluate({zoom: tileZoom + 1}) + }; + } else if (expression.kind === 'source') { return { - functionType: 'composite', - coveringZoomRange + functionType: 'source' }; } else { - // for camera functions, also save off the function values - // evaluated at the covering zoom levels - return { - functionType: 'camera', - layoutSize: layer.getLayoutValue(sizeProperty, {zoom: tileZoom + 1}), - coveringZoomRange, - coveringStopValues: [ - layer.getLayoutValue(sizeProperty, {zoom: levels[lower]}), - layer.getLayoutValue(sizeProperty, {zoom: levels[upper]}) - ] + // calculate covering zoom stops for zoom-dependent values + const levels = expression.zoomStops; + + let lower = 0; + while (lower < levels.length && levels[lower] <= tileZoom) lower++; + lower = Math.max(0, lower - 1); + let upper = lower; + while (upper < levels.length && levels[upper] < tileZoom + 1) upper++; + upper = Math.min(levels.length - 1, upper); + + const zoomRange = { + min: levels[lower], + max: levels[upper] }; + + // We'd like to be able to use CameraExpression or CompositeExpression in these + // return types rather than ExpressionSpecification, but the former are not + // transferrable across Web Worker boundaries. + if (expression.kind === 'composite') { + return { + functionType: 'composite', + zoomRange, + propertyValue: (value.value: any) + }; + } else { + // for camera functions, also save off the function values + // evaluated at the covering zoom levels + return { + functionType: 'camera', + layoutSize: expression.evaluate({zoom: tileZoom + 1}), + zoomRange, + sizeRange: { + min: expression.evaluate({zoom: zoomRange.min}), + max: expression.evaluate({zoom: zoomRange.max}) + }, + propertyValue: (value.value: any) + }; + } } } @@ -85,37 +98,41 @@ function evaluateSizeForFeature(sizeData: SizeData, } } -function evaluateSizeForZoom(sizeData: SizeData, - tr: { zoom: number }, - layer: SymbolStyleLayer, - isText: boolean) { - const sizeUniforms = {}; - if (sizeData.functionType === 'composite') { - const declaration = layer.getLayoutDeclaration( - isText ? 'text-size' : 'icon-size'); - const t = declaration.interpolationFactor( - tr.zoom, - sizeData.coveringZoomRange[0], - sizeData.coveringZoomRange[1]); - sizeUniforms.uSizeT = util.clamp(t, 0, 1); +function evaluateSizeForZoom(sizeData: SizeData, currentZoom: number, property: Property>) { + if (sizeData.functionType === 'constant') { + return { + uSizeT: 0, + uSize: sizeData.layoutSize + }; + } else if (sizeData.functionType === 'source') { + return { + uSizeT: 0, + uSize: 0 + }; } else if (sizeData.functionType === 'camera') { + const {propertyValue, zoomRange, sizeRange} = sizeData; + const expression = ((normalizePropertyExpression(propertyValue, property.specification): any): CameraExpression); + // Even though we could get the exact value of the camera function // at z = tr.zoom, we intentionally do not: instead, we interpolate // between the camera function values at a pair of zoom stops covering // [tileZoom, tileZoom + 1] in order to be consistent with this // restriction on composite functions - const declaration = layer.getLayoutDeclaration( - isText ? 'text-size' : 'icon-size'); - const t = declaration.interpolationFactor( - tr.zoom, - sizeData.coveringZoomRange[0], - sizeData.coveringZoomRange[1]); + const t = util.clamp( + expression.interpolationFactor(currentZoom, zoomRange.min, zoomRange.max), + 0, 1); - const lowerValue = sizeData.coveringStopValues[0]; - const upperValue = sizeData.coveringStopValues[1]; - sizeUniforms.uSize = lowerValue + (upperValue - lowerValue) * util.clamp(t, 0, 1); - } else if (sizeData.functionType === 'constant') { - sizeUniforms.uSize = sizeData.layoutSize; + return { + uSizeT: 0, + uSize: sizeRange.min + t * (sizeRange.max - sizeRange.min) + }; + } else { + const {propertyValue, zoomRange} = sizeData; + const expression = ((normalizePropertyExpression(propertyValue, property.specification): any): CompositeExpression); + + return { + uSizeT: util.clamp(expression.interpolationFactor(currentZoom, zoomRange.min, zoomRange.max), 0, 1), + uSize: 0 + }; } - return sizeUniforms; } diff --git a/src/symbol/transform_text.js b/src/symbol/transform_text.js index 274992b79a3..ac9b352e1d6 100644 --- a/src/symbol/transform_text.js +++ b/src/symbol/transform_text.js @@ -2,11 +2,11 @@ const rtlTextPlugin = require('../source/rtl_text_plugin'); -import type StyleLayer from '../style/style_layer'; +import type SymbolStyleLayer from '../style/style_layer/symbol_style_layer'; import type {Feature} from '../style-spec/expression'; -module.exports = function(text: string, layer: StyleLayer, globalProperties: Object, feature: Feature) { - const transform = layer.getLayoutValue('text-transform', globalProperties, feature); +module.exports = function(text: string, layer: SymbolStyleLayer, feature: Feature) { + const transform = layer.layout.get('text-transform').evaluate(feature); if (transform === 'uppercase') { text = text.toLocaleUpperCase(); } else if (transform === 'lowercase') { diff --git a/src/ui/map.js b/src/ui/map.js index a1df632a229..4430d590ebd 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -8,7 +8,6 @@ const DOM = require('../util/dom'); const ajax = require('../util/ajax'); const Style = require('../style/style'); -const AnimationLoop = require('../style/animation_loop'); const Painter = require('../render/painter'); const Transform = require('../geo/transform'); @@ -216,7 +215,6 @@ const defaultOptions = { class Map extends Camera { style: Style; painter: Painter; - animationLoop: AnimationLoop; _container: HTMLElement; _missingCSSContainer: HTMLElement; @@ -286,8 +284,6 @@ class Map extends Camera { this._container = options.container; } - this.animationLoop = new AnimationLoop(); - if (options.maxBounds) { this.setMaxBounds(options.maxBounds); } @@ -1451,6 +1447,9 @@ class Map extends Camera { this._styleDirty = false; this.style.update(); this.style._recalculate(this.transform.zoom); + if (this.style.hasTransitions()) { + this._styleDirty = true; + } } // If we are in _render for any reason other than an in-progress paint @@ -1479,16 +1478,8 @@ class Map extends Camera { this.fire('load'); } - // We should set _styleDirty for ongoing animations before firing 'render', - // but the test suite currently assumes that it can read still images while animations are - // still ongoing. See https://github.com/mapbox/mapbox-gl-js/issues/3966 - if (!this.animationLoop.stopped()) { - this._styleDirty = true; - } - this._frameId = null; - // Schedule another render frame if it's needed. // // Even though `_styleDirty` and `_sourcesDirty` are reset in this diff --git a/src/util/util.js b/src/util/util.js index 2236d92255f..4fda72fbb52 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -413,10 +413,7 @@ exports.isClosedPolygon = function(points: Array): boolean { * @return cartesian coordinates in [x, y, z] */ -exports.sphericalToCartesian = function(spherical: Array): Array { - const r = spherical[0]; - let azimuthal = spherical[1], - polar = spherical[2]; +exports.sphericalToCartesian = function([r, azimuthal, polar]: [number, number, number]): {x: number, y: number, z: number} { // We abstract "north"/"up" (compass-wise) to be 0° when really this is 90° (π/2): // correct for that here azimuthal += 90; @@ -425,12 +422,11 @@ exports.sphericalToCartesian = function(spherical: Array): Array azimuthal *= Math.PI / 180; polar *= Math.PI / 180; - // spherical to cartesian (x, y, z) - return [ - r * Math.cos(azimuthal) * Math.sin(polar), - r * Math.sin(azimuthal) * Math.sin(polar), - r * Math.cos(polar) - ]; + return { + x: r * Math.cos(azimuthal) * Math.sin(polar), + y: r * Math.sin(azimuthal) * Math.sin(polar), + z: r * Math.cos(polar) + }; }; /** diff --git a/test/integration/render-tests/regressions/mapbox-gl-js#2533/style.json b/test/integration/render-tests/regressions/mapbox-gl-js#2533/style.json index 8a218fcc6c0..bbf9b40c2bd 100644 --- a/test/integration/render-tests/regressions/mapbox-gl-js#2533/style.json +++ b/test/integration/render-tests/regressions/mapbox-gl-js#2533/style.json @@ -17,6 +17,9 @@ ] } }, + "transition": { + "duration": 0 + }, "sprite": "local://sprites/emerald", "layers": [ { diff --git a/test/integration/render-tests/regressions/mapbox-gl-js#2534/style.json b/test/integration/render-tests/regressions/mapbox-gl-js#2534/style.json index 03e25a3c7a3..7528f016e33 100644 --- a/test/integration/render-tests/regressions/mapbox-gl-js#2534/style.json +++ b/test/integration/render-tests/regressions/mapbox-gl-js#2534/style.json @@ -18,6 +18,9 @@ ] } }, + "transition": { + "duration": 0 + }, "sprite": "local://sprites/emerald", "layers": [ { diff --git a/test/unit/data/fill_bucket.test.js b/test/unit/data/fill_bucket.test.js index 7c7dd867eb9..d7c17464656 100644 --- a/test/unit/data/fill_bucket.test.js +++ b/test/unit/data/fill_bucket.test.js @@ -8,7 +8,7 @@ const VectorTile = require('@mapbox/vector-tile').VectorTile; const Point = require('@mapbox/point-geometry'); const segment = require('../../../src/data/segment'); const FillBucket = require('../../../src/data/bucket/fill_bucket'); -const StyleLayer = require('../../../src/style/style_layer'); +const FillStyleLayer = require('../../../src/style/style_layer/fill_style_layer'); // Load a fill feature from fixture tile. const vt = new VectorTile(new Protobuf(fs.readFileSync(path.join(__dirname, '/../../fixtures/mbsv5-6-18-23.vector.pbf')))); @@ -23,7 +23,9 @@ function createPolygon(numPoints) { } test('FillBucket', (t) => { - const layer = new StyleLayer({ id: 'test', type: 'fill', layout: {} }); + const layer = new FillStyleLayer({ id: 'test', type: 'fill', layout: {} }); + layer.recalculate({zoom: 0, zoomHistory: {}}); + const bucket = new FillBucket({ layers: [layer] }); bucket.addFeature({}, [[ @@ -47,7 +49,7 @@ test('FillBucket segmentation', (t) => { // breaking across array groups without tests taking a _long_ time. t.stub(segment, 'MAX_VERTEX_ARRAY_LENGTH').value(256); - const layer = new StyleLayer({ + const layer = new FillStyleLayer({ id: 'test', type: 'fill', layout: {}, @@ -55,10 +57,7 @@ test('FillBucket segmentation', (t) => { 'fill-color': ['to-color', ['get', 'foo'], '#000'] } }); - - // this, plus the style function, sets things up so that - // populatePaintArrays iterates through each vertex - layer.updatePaintTransition('fill-color', [], {}); + layer.recalculate({zoom: 0, zoomHistory: {}}); const bucket = new FillBucket({ layers: [layer] }); diff --git a/test/unit/data/line_bucket.test.js b/test/unit/data/line_bucket.test.js index de87d772573..574bdacdf4f 100644 --- a/test/unit/data/line_bucket.test.js +++ b/test/unit/data/line_bucket.test.js @@ -8,7 +8,7 @@ const VectorTile = require('@mapbox/vector-tile').VectorTile; const Point = require('@mapbox/point-geometry'); const segment = require('../../../src/data/segment'); const LineBucket = require('../../../src/data/bucket/line_bucket'); -const StyleLayer = require('../../../src/style/style_layer'); +const LineStyleLayer = require('../../../src/style/style_layer/line_style_layer'); // Load a line feature from fixture tile. const vt = new VectorTile(new Protobuf(fs.readFileSync(path.join(__dirname, '/../../fixtures/mbsv5-6-18-23.vector.pbf')))); @@ -23,7 +23,9 @@ function createLine(numPoints) { } test('LineBucket', (t) => { - const layer = new StyleLayer({ id: 'test', type: 'line' }); + const layer = new LineStyleLayer({ id: 'test', type: 'line' }); + layer.recalculate({zoom: 0, zoomHistory: {}}); + const bucket = new LineBucket({ layers: [layer] }); const line = { @@ -102,7 +104,9 @@ test('LineBucket segmentation', (t) => { // breaking across array groups without tests taking a _long_ time. t.stub(segment, 'MAX_VERTEX_ARRAY_LENGTH').value(256); - const layer = new StyleLayer({ id: 'test', type: 'line' }); + const layer = new LineStyleLayer({ id: 'test', type: 'line' }); + layer.recalculate({zoom: 0, zoomHistory: {}}); + const bucket = new LineBucket({ layers: [layer] }); // first add an initial, small feature to make sure the next one starts at diff --git a/test/unit/data/symbol_bucket.test.js b/test/unit/data/symbol_bucket.test.js index 4c5c3f353e8..2f1d55f2402 100644 --- a/test/unit/data/symbol_bucket.test.js +++ b/test/unit/data/symbol_bucket.test.js @@ -48,6 +48,7 @@ function bucketSetup() { layout: { 'text-font': ['Test'], 'text-field': 'abcde' }, filter: featureFilter() }); + layer.recalculate({zoom: 0, zoomHistory: {}}); return new SymbolBucket({ overscaling: 1, diff --git a/test/unit/source/canvas_source.test.js b/test/unit/source/canvas_source.test.js index 1a89e26e4f0..fd6670e016e 100644 --- a/test/unit/source/canvas_source.test.js +++ b/test/unit/source/canvas_source.test.js @@ -3,7 +3,6 @@ const test = require('mapbox-gl-js-test').test; const CanvasSource = require('../../../src/source/canvas_source'); const Transform = require('../../../src/geo/transform'); -const AnimationLoop = require('../../../src/style/animation_loop'); const Evented = require('../../../src/util/evented'); const util = require('../../../src/util/util'); const window = require('../../../src/util/window'); @@ -30,7 +29,7 @@ class StubMap extends Evented { constructor() { super(); this.transform = new Transform(); - this.style = { animationLoop: new AnimationLoop() }; + this.style = {}; } _rerender() { @@ -100,15 +99,15 @@ test('CanvasSource', (t) => { source.onAdd(map); - t.equal(map.style.animationLoop.stopped(), false, 'should animate initally'); + t.equal(source.hasTransition(), true, 'should animate initally'); source.onRemove(); - t.equal(map.style.animationLoop.stopped(), true, 'should stop animating'); + t.equal(source.hasTransition(), false, 'should stop animating'); source.onAdd(map); - t.equal(map.style.animationLoop.stopped(), false, 'should animate when added again'); + t.equal(source.hasTransition(), true, 'should animate when added again'); t.end(); }); @@ -119,15 +118,15 @@ test('CanvasSource', (t) => { source.onAdd(map); - t.equal(map.style.animationLoop.stopped(), false, 'initially animating'); + t.equal(source.hasTransition(), true, 'initially animating'); source.pause(); - t.equal(map.style.animationLoop.stopped(), true, 'can be paused'); + t.equal(source.hasTransition(), false, 'can be paused'); source.play(); - t.equal(map.style.animationLoop.stopped(), false, 'can be played'); + t.equal(source.hasTransition(), true, 'can be played'); t.end(); }); diff --git a/test/unit/source/source_cache.test.js b/test/unit/source/source_cache.test.js index 07682fbfc16..f5a21870449 100644 --- a/test/unit/source/source_cache.test.js +++ b/test/unit/source/source_cache.test.js @@ -2,7 +2,6 @@ const test = require('mapbox-gl-js-test').test; const SourceCache = require('../../../src/source/source_cache'); -const AnimationLoop = require('../../../src/style/animation_loop'); const Source = require('../../../src/source/source'); const Tile = require('../../../src/source/tile'); const TileCoord = require('../../../src/source/tile_coord'); @@ -508,13 +507,12 @@ test('SourceCache#update', (t) => { const transform = new Transform(); transform.resize(511, 511); transform.zoom = 2; - const animationLoop = new AnimationLoop(); const sourceCache = createSourceCache({ loadTile: function(tile, callback) { tile.timeAdded = Infinity; tile.state = 'loaded'; - tile.registerFadeDuration(animationLoop, 100); + tile.registerFadeDuration(100); callback(); } }); @@ -546,13 +544,11 @@ test('SourceCache#update', (t) => { transform.resize(511, 511); transform.zoom = 0; - const animationLoop = new AnimationLoop(); - const sourceCache = createSourceCache({ loadTile: function(tile, callback) { tile.timeAdded = Infinity; tile.state = 'loaded'; - tile.registerFadeDuration(animationLoop, 100); + tile.registerFadeDuration(100); callback(); } }); diff --git a/test/unit/style/animation_loop.test.js b/test/unit/style/animation_loop.test.js deleted file mode 100644 index 8fbb1e20418..00000000000 --- a/test/unit/style/animation_loop.test.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -const test = require('mapbox-gl-js-test').test; -const AnimationLoop = require('../../../src/style/animation_loop'); - -test('animationloop', (t) => { - const loop = new AnimationLoop(); - t.equal(loop.stopped(), true, 'starts stopped'); - t.equal(loop.n, 0, 'starts with zero animations'); - t.equal(loop.set(1000), 0, 'returns an id for cancelling animations'); - t.equal(loop.stopped(), false, 'and then is not'); - loop.cancel(0); - t.deepEqual(loop.times, [], 'can cancel an animation'); - t.equal(loop.stopped(), true, 'and then is stopped'); - - t.end(); -}); diff --git a/test/unit/style/light.test.js b/test/unit/style/light.test.js index 1e0efc45706..3015ecc4ca8 100644 --- a/test/unit/style/light.test.js +++ b/test/unit/style/light.test.js @@ -4,124 +4,66 @@ const test = require('mapbox-gl-js-test').test; const Light = require('../../../src/style/light'); const spec = require('../../../src/style-spec/reference/latest').light; const Color = require('../../../src/style-spec/util/color'); +const {sphericalToCartesian} = require('../../../src/util/util'); -test('Light', (t) => { - t.test('creates default light with no options', (t) => { - const light = new Light({}); - - for (const key in spec) { - t.deepEqual(light.getLightProperty(key), spec[key].default); - } - - t.end(); - }); - - t.test('instantiates light correctly with options', (t) => { - const light = new Light({ - anchor: 'map', - position: [2, 30, 30], - intensity: 1 - }); - - t.equal(light.getLightProperty('anchor'), 'map'); - t.deepEqual(light.getLightProperty('position'), [2, 30, 30]); - t.equal(light.getLightProperty('intensity'), 1); - t.equal(light.getLightProperty('color'), '#ffffff'); - - t.end(); - }); - - t.end(); -}); - -test('Light#set', (t) => { +test('Light with defaults', (t) => { const light = new Light({}); + light.recalculate({zoom: 0, zoomHistory: {}}); - t.equal(light.getLightProperty('color'), '#ffffff'); - - light.set({ color: 'blue' }); - - t.equal(light.getLightProperty('color'), 'blue'); + t.deepEqual(light.properties.get('anchor'), spec.anchor.default); + t.deepEqual(light.properties.get('position'), sphericalToCartesian(spec.position.default)); + t.deepEqual(light.properties.get('intensity'), spec.intensity.default); + t.deepEqual(light.properties.get('color'), Color.parse(spec.color.default)); t.end(); }); -test('Light#getLight', (t) => { - const light = new Light({}); +test('Light with options', (t) => { + const light = new Light({ + anchor: 'map', + position: [2, 30, 30], + intensity: 1 + }); + light.recalculate({zoom: 0, zoomHistory: {}}); - const defaults = {}; - for (const key in spec) { - defaults[key] = spec[key].default; - } + t.deepEqual(light.properties.get('anchor'), 'map'); + t.deepEqual(light.properties.get('position'), sphericalToCartesian([2, 30, 30])); + t.deepEqual(light.properties.get('intensity'), 1); + t.deepEqual(light.properties.get('color'), Color.parse(spec.color.default)); - t.deepEqual(light.getLight(), defaults); t.end(); }); -test('Light#getLightProperty', (t) => { +test('Light with stops function', (t) => { const light = new Light({ intensity: { stops: [[16, 0.2], [17, 0.8]] - }, - color: 'red' + } }); - light.updateLightTransitions({ transition: true }, null, createAnimationLoop()); + light.recalculate({zoom: 16.5, zoomHistory: {}}); - t.deepEqual(light.getLightProperty('intensity'), { stops: [[16, 0.2], [17, 0.8]] }); - t.equal(light.getLightProperty('color'), 'red'); - t.deepEqual(light.getLightProperty('position', { zoom: 16.5 }), [1.15, 210, 30]); + t.deepEqual(light.properties.get('intensity'), 0.5); t.end(); }); -test('Light#getLightValue', (t) => { - const light = new Light({ - intensity: { - stops: [[16, 0.2], [17, 0.8]] - }, - color: 'red' - }); - light.updateLightTransitions({ transition: true }, null, createAnimationLoop()); - - t.equal(light.getLightValue('intensity', { zoom: 16.5 }), 0.5); - t.deepEqual(light.getLightValue('color', { zoom: 16.5 }), new Color(1, 0, 0, 1)); - t.deepEqual(light.getLightValue('position', { zoom: 16.5 }), { x: 0.2875, y: -0.4979646071760521, z: 0.9959292143521045 }); +test('Light#getLight', (t) => { + const defaults = {}; + for (const key in spec) { + defaults[key] = spec[key].default; + } + t.deepEqual(new Light(defaults).getLight(), defaults); t.end(); }); test('Light#setLight', (t) => { const light = new Light({}); light.setLight({ color: 'red', "color-transition": { duration: 3000 }}); - light.updateLightTransitions({ transition: true }, null, createAnimationLoop()); + light.updateTransitions({ transition: true }, {}); + light.recalculate({zoom: 16, zoomHistory: {}, now: 1500}); - t.deepEqual(light.getLightValue('color', { zoom: 16 }), new Color(1, 0, 0, 1)); + t.deepEqual(light.properties.get('color'), new Color(1, 0.5, 0.5, 1)); t.end(); }); - -test('Light#recalculate', (t) => { - const light = new Light({ - intensity: { - stops: [[16, 0.2], [17, 0.8]] - } - }); - light.updateLightTransitions({ transition: true }, null, createAnimationLoop()); - - light.recalculate(16, null); - - t.equal(light.calculated.intensity, 0.2); - - light.recalculate(17, null); - - t.equal(light.calculated.intensity, 0.8); - - t.end(); -}); - -function createAnimationLoop() { - return { - set: function() {}, - cancel: function() {} - }; -} diff --git a/test/unit/style/style.test.js b/test/unit/style/style.test.js index 1c9b043e77d..3858172a560 100644 --- a/test/unit/style/style.test.js +++ b/test/unit/style/style.test.js @@ -12,7 +12,6 @@ const window = require('../../../src/util/window'); const rtlTextPlugin = require('../../../src/source/rtl_text_plugin'); const ajax = require('../../../src/util/ajax'); const browser = require('../../../src/util/browser'); -const Color = require('../../../src/style-spec/util/color'); function createStyleJSON(properties) { return util.extend({ @@ -1441,20 +1440,20 @@ test('Style#queryRenderedFeatures', (t) => { const features = { 'land': [{ type: 'Feature', - layer: style._layers.land, + layer: style._layers.land.serialize(), geometry: { type: 'Polygon' } }, { type: 'Feature', - layer: style._layers.land, + layer: style._layers.land.serialize(), geometry: { type: 'Point' } }], 'landref': [{ type: 'Feature', - layer: style._layers.landref, + layer: style._layers.landref.serialize(), geometry: { type: 'Line' } @@ -1559,7 +1558,7 @@ test('Style#queryRenderedFeatures', (t) => { t.test('includes paint properties', (t) => { const results = style.queryRenderedFeatures([{column: 1, row: 1, zoom: 1}], {}, 0, 0); - t.deepEqual(results[2].layer.paint['line-color'], new Color(1, 0, 0, 1)); + t.deepEqual(results[2].layer.paint['line-color'], 'red'); t.end(); }); diff --git a/test/unit/style/style_declaration.test.js b/test/unit/style/style_declaration.test.js deleted file mode 100644 index 84cc309b01e..00000000000 --- a/test/unit/style/style_declaration.test.js +++ /dev/null @@ -1,86 +0,0 @@ -'use strict'; - -const test = require('mapbox-gl-js-test').test; -const StyleDeclaration = require('../../../src/style/style_declaration'); -const Color = require('../../../src/style-spec/util/color'); - -test('StyleDeclaration', (t) => { - t.test('with minimum value', (t) => { - t.equal((new StyleDeclaration({type: "number", minimum: -2}, -5)).calculate({zoom: 0}), -2); - t.equal((new StyleDeclaration({type: "number", minimum: -2}, 5)).calculate({zoom: 0}), 5); - t.end(); - }); - - t.test('number constant', (t) => { - const d = new StyleDeclaration({type: 'number'}, 1); - - t.ok(d.isZoomConstant()); - t.ok(d.isFeatureConstant()); - - t.equal(d.calculate({zoom: 0}), 1); - t.equal(d.calculate({zoom: 1}), 1); - t.equal(d.calculate({zoom: 2}), 1); - - t.end(); - }); - - t.test('string constant', (t) => { - const d = new StyleDeclaration({type: 'string'}, 'mapbox'); - - t.ok(d.isZoomConstant()); - t.ok(d.isFeatureConstant()); - - t.equal(d.calculate({zoom: 0}), 'mapbox'); - t.equal(d.calculate({zoom: 1}), 'mapbox'); - t.equal(d.calculate({zoom: 2}), 'mapbox'); - - t.end(); - }); - - t.test('color constant', (t) => { - const d = new StyleDeclaration({type: 'color'}, 'red'); - - t.ok(d.isZoomConstant()); - t.ok(d.isFeatureConstant()); - - t.deepEqual(d.calculate({zoom: 0}), new Color(1, 0, 0, 1)); - t.deepEqual(d.calculate({zoom: 1}), new Color(1, 0, 0, 1)); - t.deepEqual(d.calculate({zoom: 2}), new Color(1, 0, 0, 1)); - - t.end(); - }); - - t.test('array constant', (t) => { - const d = new StyleDeclaration({type: 'array'}, [1]); - - t.ok(d.isZoomConstant()); - t.ok(d.isFeatureConstant()); - - t.deepEqual(d.calculate({zoom: 0}), [1]); - t.deepEqual(d.calculate({zoom: 1}), [1]); - t.deepEqual(d.calculate({zoom: 2}), [1]); - - t.end(); - }); - - t.test('interpolated functions', (t) => { - const reference = {type: "number", function: "interpolated"}; - t.equal((new StyleDeclaration(reference, { stops: [[0, 1]] })).calculate({zoom: 0}), 1); - t.equal((new StyleDeclaration(reference, { stops: [[2, 2], [5, 10]] })).calculate({zoom: 0}), 2); - t.equal((new StyleDeclaration(reference, { stops: [[0, 0], [5, 10]] })).calculate({zoom: 12}), 10); - t.equal((new StyleDeclaration(reference, { stops: [[0, 0], [5, 10]] })).calculate({zoom: 6}), 10); - t.equal(Math.round((new StyleDeclaration(reference, { stops: [[0, 0], [5, 10]], base: 1.01 })).calculate({zoom: 2.5})), 5); - t.equal((new StyleDeclaration(reference, { stops: [[0, 0], [1, 10], [2, 20]] })).calculate({zoom: 2}), 20); - t.equal((new StyleDeclaration(reference, { stops: [[0, 0], [1, 10], [2, 20]] })).calculate({zoom: 1}), 10); - t.equal((new StyleDeclaration(reference, { stops: [[0, 0]] })).calculate({zoom: 6}), 0); - t.end(); - }); - - t.test('piecewise-constant function', (t) => { - const decl = new StyleDeclaration({type: "array", function: "piecewise-constant"}, {stops: [[0, [0, 10, 5]]]}); - t.deepEqual(decl.calculate({zoom: 0}), [0, 10, 5]); - t.end(); - }); - - t.end(); -}); diff --git a/test/unit/style/style_layer.test.js b/test/unit/style/style_layer.test.js index 302eda68a11..326a7388fcb 100644 --- a/test/unit/style/style_layer.test.js +++ b/test/unit/style/style_layer.test.js @@ -17,24 +17,6 @@ test('StyleLayer', (t) => { t.end(); }); -test('StyleLayer#updatePaintTransition', (t) => { - - t.test('updates paint transition', (t) => { - const layer = StyleLayer.create({ - "id": "background", - "type": "background", - "paint": { - "background-color": "red" - } - }); - layer.updatePaintTransition('background-color', [], {}); - t.deepEqual(layer.getPaintValue('background-color'), new Color(1, 0, 0, 1)); - t.end(); - }); - - t.end(); -}); - test('StyleLayer#setPaintProperty', (t) => { t.test('sets new property value', (t) => { const layer = StyleLayer.create({ @@ -72,13 +54,14 @@ test('StyleLayer#setPaintProperty', (t) => { "background-opacity": 1 } }); - layer.updatePaintTransitions([], {transition: false}, null, createAnimationLoop()); + layer.setPaintProperty('background-color', null); - layer.updatePaintTransitions([], {transition: false}, null, createAnimationLoop()); + layer.updatePaintTransitions({transition: false}); + layer.recalculate({zoom: 0, zoomHistory: {}}); - t.deepEqual(layer.getPaintValue('background-color'), new Color(0, 0, 0, 1)); + t.deepEqual(layer.paint.get('background-color'), new Color(0, 0, 0, 1)); t.equal(layer.getPaintProperty('background-color'), undefined); - t.equal(layer.getPaintValue('background-opacity'), 1); + t.equal(layer.paint.get('background-opacity'), 1); t.equal(layer.getPaintProperty('background-opacity'), 1); t.end(); @@ -125,7 +108,6 @@ test('StyleLayer#setPaintProperty', (t) => { layer.on('error', () => { t.equal(layer.getPaintProperty('background-opacity'), undefined); - t.equal(layer.getPaintValue('background-opacity'), 1); t.end(); }); @@ -158,11 +140,14 @@ test('StyleLayer#setPaintProperty', (t) => { }); layer.setPaintProperty('fill-outline-color', '#f00'); - layer.updatePaintTransitions([], {transition: false}, null, createAnimationLoop()); - t.deepEqual(layer.getPaintValue('fill-outline-color'), new Color(1, 0, 0, 1)); + layer.updatePaintTransitions({transition: false}); + layer.recalculate({zoom: 0, zoomHistory: {}}); + t.deepEqual(layer.paint.get('fill-outline-color').value, {kind: 'constant', value: new Color(1, 0, 0, 1)}); + layer.setPaintProperty('fill-outline-color', undefined); - layer.updatePaintTransitions([], {transition: false}, null, createAnimationLoop()); - t.deepEqual(layer.getPaintValue('fill-outline-color'), new Color(0, 0, 1, 1)); + layer.updatePaintTransitions({transition: false}); + layer.recalculate({zoom: 0, zoomHistory: {}}); + t.deepEqual(layer.paint.get('fill-outline-color').value, {kind: 'constant', value: new Color(0, 0, 1, 1)}); t.end(); }); @@ -177,22 +162,24 @@ test('StyleLayer#setPaintProperty', (t) => { } }); - const animationLoop = createAnimationLoop(); - // setup: set and then unset fill-outline-color so that, when we then try // to re-set it, StyleTransition#calculate() attempts interpolation layer.setPaintProperty('fill-outline-color', '#f00'); - layer.updatePaintTransitions([], {transition: true}, null, animationLoop); + layer.updatePaintTransitions({transition: true}); + layer.recalculate({zoom: 0, zoomHistory: {}}); + layer.setPaintProperty('fill-outline-color', undefined); - layer.updatePaintTransitions([], {transition: true}, null, animationLoop); + layer.updatePaintTransitions({transition: true}); + layer.recalculate({zoom: 0, zoomHistory: {}}); // re-set fill-outline-color and get its value, triggering the attempt // to interpolate between undefined and #f00 layer.setPaintProperty('fill-outline-color', '#f00'); - layer.updatePaintTransitions([], {transition: true}, null, animationLoop); - t.doesNotThrow(() => { - layer.getPaintValue('fill-outline-color'); - }); + layer.updatePaintTransitions({transition: true}); + layer.recalculate({zoom: 0, zoomHistory: {}}); + + layer.paint.get('fill-outline-color'); + t.end(); }); @@ -208,28 +195,6 @@ test('StyleLayer#setPaintProperty', (t) => { t.end(); }); - test('StyleLayer#isPaintValueZoomConstant', (t) => { - t.test('is paint value zoom constant undefined', (t) => { - const layer = StyleLayer.create({ - "id": "background", - "type": "fill", - "paint.blue": { - "fill-color": "#8ccbf7", - "fill-opacity": 1 - }, - "paint": { - "fill-opacity": 0 - } - }); - - t.equal(layer.isPaintValueZoomConstant('background-color'), true); - - t.end(); - }); - - t.end(); - }); - t.end(); }); @@ -284,8 +249,9 @@ test('StyleLayer#setLayoutProperty', (t) => { }); layer.setLayoutProperty('text-transform', null); + layer.recalculate({zoom: 0, zoomHistory: {}}); - t.equal(layer.getLayoutValue('text-transform'), 'none'); + t.deepEqual(layer.layout.get('text-transform').value, {kind: 'constant', value: 'none'}); t.equal(layer.getLayoutProperty('text-transform'), undefined); t.end(); }); @@ -414,167 +380,3 @@ test('StyleLayer#serialize', (t) => { t.end(); }); - -test('StyleLayer#getPaintValue', (t) => { - t.test('returns property default if the value is data-driven and no feature is provided', (t) => { - const layer = StyleLayer.create({ - "type": "circle", - "paint": { - "circle-opacity": ["get", "opacity"] - } - }); - layer.updatePaintTransitions({}); - t.deepEqual(layer.getPaintValue("circle-opacity"), 1); - t.end(); - }); - - t.end(); -}); - -test('StyleLayer#getLayoutValue', (t) => { - t.test('returns property default if the value is data-driven and no feature is provided', (t) => { - const layer = StyleLayer.create({ - "type": "symbol", - "layout": { - "icon-rotate": ["get", "rotate"] - } - }); - t.deepEqual(layer.getLayoutValue("icon-rotate"), 0); - t.end(); - }); - - t.end(); -}); - -test('StyleLayer#getLayoutValue (default exceptions)', (t) => { - t.test('symbol-placement:point => *-rotation-alignment:viewport', (t) => { - const layer = StyleLayer.create({ - "type": "symbol", - "layout": { - "symbol-placement": "point" - } - }); - t.equal(layer.getLayoutValue('text-rotation-alignment'), 'viewport'); - t.equal(layer.getLayoutValue('icon-rotation-alignment'), 'viewport'); - t.end(); - }); - - t.test('symbol-placement:line => *-rotation-alignment:map', (t) => { - const layer = StyleLayer.create({ - "type": "symbol", - "layout": { - "symbol-placement": "line" - } - }); - t.equal(layer.getLayoutValue('text-rotation-alignment'), 'map'); - t.equal(layer.getLayoutValue('icon-rotation-alignment'), 'map'); - t.end(); - }); - - t.test('text-rotation-alignment:map => text-pitch-alignment:map', (t) => { - const layer = StyleLayer.create({ - "type": "symbol", - "layout": { - "text-rotation-alignment": "map" - } - }); - t.equal(layer.getLayoutValue('text-rotation-alignment'), 'map'); - t.equal(layer.getLayoutValue('text-pitch-alignment'), 'map'); - t.end(); - }); - - t.test('text-rotation-alignment:viewport => text-pitch-alignment:viewport', (t) => { - const layer = StyleLayer.create({ - "type": "symbol", - "layout": { - "text-rotation-alignment": "viewport" - } - }); - t.equal(layer.getLayoutValue('text-rotation-alignment'), 'viewport'); - t.equal(layer.getLayoutValue('text-pitch-alignment'), 'viewport'); - t.end(); - }); - - t.test('text-pitch-alignment:auto defaults to text-rotation-alignment', (t) => { - const layer = StyleLayer.create({ - "type": "symbol", - "layout": { - "text-rotation-alignment": "map", - "text-pitch-alignment": "auto" - } - }); - t.equal(layer.getLayoutValue('text-rotation-alignment'), 'map'); - t.equal(layer.getLayoutValue('text-pitch-alignment'), 'map'); - t.end(); - }); - - t.test('text-pitch-alignment respected when set', (t) => { - const layer = StyleLayer.create({ - "type": "symbol", - "layout": { - "text-rotation-alignment": "viewport", - "text-pitch-alignment": "map" - } - }); - t.equal(layer.getLayoutValue('text-rotation-alignment'), 'viewport'); - t.equal(layer.getLayoutValue('text-pitch-alignment'), 'map'); - t.end(); - }); - - t.test('symbol-placement:point and text-rotation-alignment:auto => text-rotation-alignment:viewport ', (t) => { - const layer = StyleLayer.create({ - "type": "symbol", - "layout": { - "symbol-placement": "point", - "text-rotation-alignment": "auto" - } - }); - t.equal(layer.getLayoutValue('text-rotation-alignment'), 'viewport'); - t.end(); - }); - - t.test('symbol-placement:line and text-rotation-alignment:auto => text-rotation-alignment:map ', (t) => { - const layer = StyleLayer.create({ - "type": "symbol", - "layout": { - "symbol-placement": "line", - "text-rotation-alignment": "auto" - } - }); - t.equal(layer.getLayoutValue('text-rotation-alignment'), 'map'); - t.end(); - }); - - t.test('symbol-placement:point and icon-rotation-alignment:auto => icon-rotation-alignment:viewport ', (t) => { - const layer = StyleLayer.create({ - "type": "symbol", - "layout": { - "symbol-placement": "point", - "icon-rotation-alignment": "auto" - } - }); - t.equal(layer.getLayoutValue('icon-rotation-alignment'), 'viewport'); - t.end(); - }); - - t.test('symbol-placement:line and icon-rotation-alignment:auto => icon-rotation-alignment:map ', (t) => { - const layer = StyleLayer.create({ - "type": "symbol", - "layout": { - "symbol-placement": "line", - "icon-rotation-alignment": "auto" - } - }); - t.equal(layer.getLayoutValue('icon-rotation-alignment'), 'map'); - t.end(); - }); - - t.end(); -}); - -function createAnimationLoop() { - return { - set: function() {}, - cancel: function() {} - }; -} diff --git a/test/unit/style/style_transition.test.js b/test/unit/style/style_transition.test.js deleted file mode 100644 index f09a69abd3a..00000000000 --- a/test/unit/style/style_transition.test.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; - -const test = require('mapbox-gl-js-test').test; -const StyleDeclaration = require('../../../src/style/style_declaration'); -const StyleTransition = require('../../../src/style/style_transition'); - -test('StyleTransition', (t) => { - - t.test('interpolated piecewise-constant function', (t) => { - const reference = {type: "image", function: "piecewise-constant", transition: true}; - const options = {duration: 300, delay: 0}; - - const constant = new StyleTransition(reference, new StyleDeclaration(reference, 'a.png'), null, options); - t.deepEqual( - constant.calculate({zoom: 0}), - { to: 'a.png', toScale: 1, from: 'a.png', fromScale: 0.5, t: 1 } - ); - - const variable = new StyleTransition(reference, - new StyleDeclaration(reference, {stops: [[0, 'a.png'], [1, 'b.png']]}), null, options); - t.deepEqual( - variable.calculate({zoom: 1}), - { to: 'b.png', toScale: 1, from: 'a.png', fromScale: 2, t: 1 } - ); - - const unset = new StyleTransition(reference, new StyleDeclaration(reference, undefined), null, options); - t.deepEqual( - unset.calculate({zoom: 1}), - undefined - ); - - t.end(); - }); - - t.end(); -}); diff --git a/test/unit/symbol/quads.test.js b/test/unit/symbol/quads.test.js index dda26fb3a21..403b8987339 100644 --- a/test/unit/symbol/quads.test.js +++ b/test/unit/symbol/quads.test.js @@ -4,14 +4,12 @@ const test = require('mapbox-gl-js-test').test; const getIconQuads = require('../../../src/symbol/quads').getIconQuads; const Anchor = require('../../../src/symbol/anchor'); +const SymbolStyleLayer = require('../../../src/style/style_layer/symbol_style_layer'); function createLayer(layer) { - return { - layout: layer.layout, - getLayoutValue: function(key) { - return layer.layout[key]; - } - }; + const result = new SymbolStyleLayer(layer); + result.recalculate({zoom: 0, zoomHistory: {}}); + return result; } function createShapedIcon() { diff --git a/yarn.lock b/yarn.lock index 30603286fb2..c0630adb293 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3290,6 +3290,10 @@ ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" +ejs@^2.5.7: + version "2.5.7" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.7.tgz#cc872c168880ae3c7189762fd5ffc00896c9518a" + electron-to-chromium@^1.2.7: version "1.3.15" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.15.tgz#08397934891cbcfaebbd18b82a95b5a481138369"