From 65bb6c9bea846bb84e5a785c98ec85db2545d3d5 Mon Sep 17 00:00:00 2001 From: Alexander Shalamov Date: Tue, 19 Mar 2019 17:32:44 +0200 Subject: [PATCH] Impement FormatSectionOverride expression and use it for SymbolStyleLayer --- build/generate-style-code.js | 16 +++- src/data/bucket/symbol_bucket.js | 15 +++- .../definitions/format_section_override.js | 54 ++++++++++++ src/style-spec/expression/index.js | 14 +-- src/style-spec/reference/v8.json | 1 + src/style-spec/style-spec.js | 3 +- src/style/properties.js | 9 +- src/style/style_layer.js | 16 +++- src/style/style_layer/layer_properties.js.ejs | 9 ++ src/style/style_layer/symbol_style_layer.js | 85 ++++++++++++++++++- .../symbol_style_layer_properties.js | 6 +- test/unit/style-spec/spec.test.js | 3 +- 12 files changed, 208 insertions(+), 23 deletions(-) create mode 100644 src/style-spec/expression/definitions/format_section_override.js diff --git a/build/generate-style-code.js b/build/generate-style-code.js index ba5a1d6afd2..1cad05c9048 100644 --- a/build/generate-style-code.js +++ b/build/generate-style-code.js @@ -12,6 +12,12 @@ global.camelize = function (str) { }); }; +global.camelizeWithLeadingLowercase = function (str) { + return str.replace(/-(.)/g, function (_, x) { + return x.toUpperCase(); + }); +}; + global.flowType = function (property) { switch (property.type) { case 'boolean': @@ -96,10 +102,18 @@ global.defaultValue = function (property) { } }; +global.overrides = function (property) { + return `{ runtimeType: ${runtimeType(property)}, getOverride: (o) => o.${camelizeWithLeadingLowercase(property.name)}, hasOverride: (o) => !!o.${camelizeWithLeadingLowercase(property.name)} }`; +} + global.propertyValue = function (property, type) { switch (property['property-type']) { case 'data-driven': - return `new DataDrivenProperty(styleSpec["${type}_${property.layerType}"]["${property.name}"])`; + if (property.overridable) { + return `new DataDrivenProperty(styleSpec["${type}_${property.layerType}"]["${property.name}"], ${overrides(property)})`; + } else { + return `new DataDrivenProperty(styleSpec["${type}_${property.layerType}"]["${property.name}"])`; + } case 'cross-faded': return `new CrossFadedProperty(styleSpec["${type}_${property.layerType}"]["${property.name}"])`; case 'cross-faded-data-driven': diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index c0dfd1ac956..c96b0e491b4 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -36,7 +36,6 @@ import { register } from '../../util/web_worker_transfer'; import EvaluationParameters from '../../style/evaluation_parameters'; import Formatted from '../../style-spec/expression/types/formatted'; - import type { Bucket, BucketParameters, @@ -45,7 +44,7 @@ import type { } from '../bucket'; import type {CollisionBoxArray, CollisionBox, SymbolInstance} from '../array_types'; import type { StructArray, StructArrayMember } from '../../util/struct_array'; -import type SymbolStyleLayer from '../../style/style_layer/symbol_style_layer'; +import SymbolStyleLayer from '../../style/style_layer/symbol_style_layer'; import type Context from '../../gl/context'; import type IndexBuffer from '../../gl/index_buffer'; import type VertexBuffer from '../../gl/vertex_buffer'; @@ -292,6 +291,7 @@ class SymbolBucket implements Bucket { uploaded: boolean; sourceLayerIndex: number; sourceID: string; + hasPaintOverrides: boolean; constructor(options: BucketParameters) { this.collisionBoxArray = options.collisionBoxArray; @@ -303,6 +303,7 @@ class SymbolBucket implements Bucket { this.pixelRatio = options.pixelRatio; this.sourceLayerIndex = options.sourceLayerIndex; this.hasPattern = false; + this.hasPaintOverrides = false; const layer = this.layers[0]; const unevaluatedLayoutValues = layer._unevaluatedLayout._values; @@ -324,6 +325,14 @@ class SymbolBucket implements Bucket { } createArrays() { + const layout = this.layers[0].layout; + this.hasPaintOverrides = SymbolStyleLayer.hasPaintOverrides(layout); + if (this.hasPaintOverrides) { + for (const layer of this.layers) { + layer.setPaintOverrides(layout); + } + } + this.text = new SymbolBuffers(new ProgramConfigurationSet(symbolLayoutAttributes.members, this.layers, this.zoom, property => /^text/.test(property))); this.icon = new SymbolBuffers(new ProgramConfigurationSet(symbolLayoutAttributes.members, this.layers, this.zoom, property => /^icon/.test(property))); @@ -552,7 +561,7 @@ class SymbolBucket implements Bucket { if (feature.text && feature.text.sections) { const sections = feature.text.sections; - if (sections[0].textColor) { + if (this.hasPaintOverrides) { let currentSectionIndex; const populatePaintArrayForSection = (sectionIndex?: number, lastSection: boolean) => { if (currentSectionIndex !== undefined && (currentSectionIndex !== sectionIndex || lastSection)) { diff --git a/src/style-spec/expression/definitions/format_section_override.js b/src/style-spec/expression/definitions/format_section_override.js new file mode 100644 index 00000000000..ebcaaea44a9 --- /dev/null +++ b/src/style-spec/expression/definitions/format_section_override.js @@ -0,0 +1,54 @@ +// @flow + +import assert from 'assert'; +import type { Expression } from '../expression'; +import type EvaluationContext from '../evaluation_context'; +import type { Value } from '../values'; +import type { Type } from '../types'; +import type { ZoomConstantExpression } from '../../expression'; +import { NullType } from '../types'; +import { PossiblyEvaluatedPropertyValue } from '../../../style/properties'; +import { register } from '../../../util/web_worker_transfer'; + +export default class FormatSectionOverride implements Expression { + type: Type; + defaultValue: PossiblyEvaluatedPropertyValue; + + constructor(defaultValue: PossiblyEvaluatedPropertyValue) { + assert(defaultValue.property.overrides !== undefined); + this.type = defaultValue.property.overrides ? defaultValue.property.overrides.runtimeType : NullType; + this.defaultValue = defaultValue; + } + + evaluate(ctx: EvaluationContext) { + if (ctx.formattedSection) { + const overrides = this.defaultValue.property.overrides; + if (overrides && overrides.hasOverride(ctx.formattedSection)) { + return overrides.getOverride(ctx.formattedSection); + } + } + + if (ctx.feature && ctx.featureState) { + return this.defaultValue.evaluate(ctx.feature, ctx.featureState); + } + return null; + } + + eachChild(fn: (Expression) => void) { + if (!this.defaultValue.isConstant()) { + const expr: ZoomConstantExpression<'source'> = ((this.defaultValue.value): any); + fn(expr._styleExpression.expression); + } + } + + // Cannot be statically evaluated, as the output depends on the evaluation context. + possibleOutputs(): Array { + return [undefined]; + } + + serialize() { + return null; + } +} + +register('FormatSectionOverride', FormatSectionOverride, {omit: ['defaultValue']}); diff --git a/src/style-spec/expression/index.js b/src/style-spec/expression/index.js index 7d756cd125a..91513e34b7d 100644 --- a/src/style-spec/expression/index.js +++ b/src/style-spec/expression/index.js @@ -152,14 +152,12 @@ export class ZoomDependentExpression { _styleExpression: StyleExpression; _interpolationType: ?InterpolationType; - constructor(kind: Kind, expression: StyleExpression, zoomCurve: Step | Interpolate) { + constructor(kind: Kind, expression: StyleExpression, zoomStops: Array, interpolationType?: InterpolationType) { this.kind = kind; - this.zoomStops = zoomCurve.labels; + this.zoomStops = zoomStops; this._styleExpression = expression; this.isStateDependent = kind !== ('camera': EvaluationKind) && !isConstant.isStateConstant(expression.expression); - if (zoomCurve instanceof Interpolate) { - this._interpolationType = zoomCurve.interpolation; - } + this._interpolationType = interpolationType; } evaluateWithoutErrorHandling(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, formattedSection?: FormattedSection): any { @@ -244,9 +242,11 @@ export function createPropertyExpression(expression: mixed, propertySpec: StyleP (new ZoomConstantExpression('source', expression.value): SourceExpression)); } + const interpolationType = zoomCurve instanceof Interpolate ? zoomCurve.interpolation : undefined; + return success(isFeatureConstant ? - (new ZoomDependentExpression('camera', expression.value, zoomCurve): CameraExpression) : - (new ZoomDependentExpression('composite', expression.value, zoomCurve): CompositeExpression)); + (new ZoomDependentExpression('camera', expression.value, zoomCurve.labels, interpolationType): CameraExpression) : + (new ZoomDependentExpression('composite', expression.value, zoomCurve.labels, interpolationType): CompositeExpression)); } import { isFunction, createFunction } from '../function'; diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json index cdd03c76393..3ce2a30d99c 100644 --- a/src/style-spec/reference/v8.json +++ b/src/style-spec/reference/v8.json @@ -5033,6 +5033,7 @@ "doc": "The color with which the text will be drawn.", "default": "#000000", "transition": true, + "overridable": true, "requires": [ "text-field" ], diff --git a/src/style-spec/style-spec.js b/src/style-spec/style-spec.js index 68b3182d763..a02368e9f6c 100644 --- a/src/style-spec/style-spec.js +++ b/src/style-spec/style-spec.js @@ -39,7 +39,8 @@ export type StylePropertySpecification = { 'property-type': ExpressionType, expression?: ExpressionSpecification, transition: boolean, - default?: string + default?: string, + overridable: boolean } | { type: 'array', value: 'number', diff --git a/src/style/properties.js b/src/style/properties.js index 694ab066e98..113a7b9c6f2 100644 --- a/src/style/properties.js +++ b/src/style/properties.js @@ -535,9 +535,11 @@ export class DataConstantProperty implements Property { */ export class DataDrivenProperty implements Property> { specification: StylePropertySpecification; + overrides: ?Object; - constructor(specification: StylePropertySpecification) { + constructor(specification: StylePropertySpecification, overrides?: Object) { this.specification = specification; + this.overrides = overrides; } possiblyEvaluate(value: PropertyValue>, parameters: EvaluationParameters): PossiblyEvaluatedPropertyValue { @@ -716,6 +718,7 @@ export class Properties { defaultTransitionablePropertyValues: TransitionablePropertyValues; defaultTransitioningPropertyValues: TransitioningPropertyValues; defaultPossiblyEvaluatedValues: PossiblyEvaluatedPropertyValues; + overridableProperties: Array; constructor(properties: Props) { this.properties = properties; @@ -723,9 +726,13 @@ export class Properties { this.defaultTransitionablePropertyValues = ({}: any); this.defaultTransitioningPropertyValues = ({}: any); this.defaultPossiblyEvaluatedValues = ({}: any); + this.overridableProperties = ([]: any); for (const property in properties) { const prop = properties[property]; + if (prop.specification.overridable) { + this.overridableProperties.push(property); + } const defaultPropertyValue = this.defaultPropertyValues[property] = new PropertyValue(prop, undefined); const defaultTransitionablePropertyValue = this.defaultTransitionablePropertyValues[property] = diff --git a/src/style/style_layer.js b/src/style/style_layer.js index 9cfd91bd62f..557d2da24dd 100644 --- a/src/style/style_layer.js +++ b/src/style/style_layer.js @@ -17,7 +17,7 @@ import type { FeatureState } from '../style-spec/expression'; import type {Bucket} from '../data/bucket'; import type Point from '@mapbox/point-geometry'; import type {FeatureFilter} from '../style-spec/feature_filter'; -import type {TransitionParameters} from './properties'; +import type {TransitionParameters, PropertyValue} from './properties'; import type EvaluationParameters, {CrossfadeParameters} from './evaluation_parameters'; import type Transform from '../geo/transform'; import type { @@ -157,11 +157,13 @@ class StyleLayer extends Evented { const prop = this._transitionablePaint._values[name]; const newCrossFadedValue = prop.property.specification["property-type"] === 'cross-faded-data-driven' && !prop.value.value && value; - const wasDataDriven = this._transitionablePaint._values[name].value.isDataDriven(); + const oldValue = this._transitionablePaint._values[name].value; + const wasDataDriven = oldValue.isDataDriven(); this._transitionablePaint.setValue(name, value); - const isDataDriven = this._transitionablePaint._values[name].value.isDataDriven(); + const newValue = this._transitionablePaint._values[name].value; + const isDataDriven = newValue.isDataDriven(); this._handleSpecialPaintPropertyUpdate(name); - return isDataDriven || wasDataDriven || newCrossFadedValue; + return isDataDriven || wasDataDriven || newCrossFadedValue || this._handleOverridablePaintPropertyUpdate(name, oldValue, newValue); } } @@ -169,6 +171,12 @@ class StyleLayer extends Evented { // No-op; can be overridden by derived classes. } + // eslint-disable-next-line no-unused-vars + _handleOverridablePaintPropertyUpdate(name: string, oldValue: PropertyValue, newValue: PropertyValue): boolean { + // No-op; can be overridden by derived classes. + return false; + } + isHidden(zoom: number) { if (this.minzoom && zoom < this.minzoom) return true; if (this.maxzoom && zoom >= this.maxzoom) return true; diff --git a/src/style/style_layer/layer_properties.js.ejs b/src/style/style_layer/layer_properties.js.ejs index 573538879dc..9fabb9505f5 100644 --- a/src/style/style_layer/layer_properties.js.ejs +++ b/src/style/style_layer/layer_properties.js.ejs @@ -21,6 +21,15 @@ import { import type Color from '../../style-spec/util/color'; import type Formatted from '../../style-spec/expression/types/formatted'; +<% +const overridables = paintProperties.filter(p => p.overridable) +if (overridables.length) { -%> + +import { + <%= overridables.reduce((imports, prop) => { imports.push(runtimeType(prop)); return imports; }, []).join(',\n\t'); -%> + +} from '../../style-spec/expression/types'; +<% } -%> <% if (layoutProperties.length) { -%> export type LayoutProps = {| diff --git a/src/style/style_layer/symbol_style_layer.js b/src/style/style_layer/symbol_style_layer.js index 4d4d51e0e1b..1e746c70d8a 100644 --- a/src/style/style_layer/symbol_style_layer.js +++ b/src/style/style_layer/symbol_style_layer.js @@ -2,18 +2,35 @@ import StyleLayer from '../style_layer'; +import assert from 'assert'; import SymbolBucket from '../../data/bucket/symbol_bucket'; import resolveTokens from '../../util/token'; -import { isExpression } from '../../style-spec/expression'; -import assert from 'assert'; import properties from './symbol_style_layer_properties'; -import { Transitionable, Transitioning, Layout, PossiblyEvaluated } from '../properties'; + +import { + Transitionable, + Transitioning, + Layout, + PossiblyEvaluated, + PossiblyEvaluatedPropertyValue, + PropertyValue +} from '../properties'; + +import { + isExpression, + StyleExpression, + ZoomConstantExpression, + ZoomDependentExpression +} 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 '../evaluation_parameters'; import type {LayerSpecification} from '../../style-spec/types'; +import type { Feature, SourceExpression, CompositeExpression } from '../../style-spec/expression'; +import Formatted from '../../style-spec/expression/types/formatted'; +import FormatSectionOverride from '../../style-spec/expression/definitions/format_section_override'; +import FormatExpression from '../../style-spec/expression/definitions/format'; class SymbolStyleLayer extends StyleLayer { _unevaluatedLayout: Layout; @@ -77,6 +94,66 @@ class SymbolStyleLayer extends StyleLayer { assert(false); // Should take a different path in FeatureIndex return false; } + + setPaintOverrides(layout: PossiblyEvaluated) { + for (const overridable of properties.paint.overridableProperties) { + if (!SymbolStyleLayer.hasPaintOverride(layout, overridable)) { + continue; + } + const overriden = this.paint.get(overridable); + const override = new FormatSectionOverride(overriden); + const styleExpression = new StyleExpression(override, overriden.property.specification); + let expression = null; + if (overriden.value.kind === 'constant' || overriden.value.kind === 'source') { + expression = (new ZoomConstantExpression('source', styleExpression): SourceExpression); + } else { + expression = (new ZoomDependentExpression('composite', + styleExpression, + overriden.value.zoomStops, + overriden.value._interpolationType): CompositeExpression); + } + this.paint._values[overridable] = new PossiblyEvaluatedPropertyValue(overriden.property, + expression, + overriden.parameters); + } + } + + _handleOverridablePaintPropertyUpdate(name: string, oldValue: PropertyValue, newValue: PropertyValue): boolean { + if (!this.layout || oldValue.isDataDriven() || newValue.isDataDriven()) { + return false; + } + return SymbolStyleLayer.hasPaintOverride(this.layout, name); + } + + static hasPaintOverride(layout: PossiblyEvaluated, propertyName: string): boolean { + const textField = layout.get('text-field'); + let sections: any = []; + if (textField.value.kind === 'constant' && textField.value.value instanceof Formatted) { + sections = textField.value.value.sections; + } else if (textField.value.kind === 'source') { + const expr: ZoomConstantExpression<'source'> = ((textField.value): any); + if (expr._styleExpression && expr._styleExpression.expression instanceof FormatExpression) { + sections = expr._styleExpression.expression.sections; + } + } + + const property = properties.paint.properties[propertyName]; + for (const section of sections) { + if (property.overrides && property.overrides.hasOverride(section)) { + return true; + } + } + return false; + } + + static hasPaintOverrides(layout: PossiblyEvaluated): boolean { + for (const overridable of properties.paint.overridableProperties) { + if (SymbolStyleLayer.hasPaintOverride(layout, overridable)) { + return true; + } + } + return false; + } } export default SymbolStyleLayer; diff --git a/src/style/style_layer/symbol_style_layer_properties.js b/src/style/style_layer/symbol_style_layer_properties.js index d5cdeb55f4b..9e95bd4bd2b 100644 --- a/src/style/style_layer/symbol_style_layer_properties.js +++ b/src/style/style_layer/symbol_style_layer_properties.js @@ -17,6 +17,10 @@ import type Color from '../../style-spec/util/color'; import type Formatted from '../../style-spec/expression/types/formatted'; +import { + ColorType +} from '../../style-spec/expression/types'; + export type LayoutProps = {| "symbol-placement": DataConstantProperty<"point" | "line" | "line-center">, "symbol-spacing": DataConstantProperty, @@ -129,7 +133,7 @@ const paint: Properties = new Properties({ "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-color": new DataDrivenProperty(styleSpec["paint_symbol"]["text-color"], { runtimeType: ColorType, getOverride: (o) => o.textColor, hasOverride: (o) => !!o.textColor }), "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"]), diff --git a/test/unit/style-spec/spec.test.js b/test/unit/style-spec/spec.test.js index 0f29e0d10a1..7a3846293bd 100644 --- a/test/unit/style-spec/spec.test.js +++ b/test/unit/style-spec/spec.test.js @@ -61,7 +61,8 @@ function validSchema(k, t, obj, ref, version, kind) { 'minimum', 'period', 'requires', - 'sdk-support' + 'sdk-support', + 'overridable' ]; // Schema object.