From b4fd6ee030e33d7ffc612c668a61b85171fa042b Mon Sep 17 00:00:00 2001 From: Alexander Shalamov Date: Wed, 14 Aug 2019 01:30:30 +0300 Subject: [PATCH] Allow overriding SymbolLayer's paint properties using format expression options (#8068) * Add text color option to format expression * Update text-shaping and format expression's unit tests * Impement FormatSectionOverride expression and use it for SymbolStyleLayer * Unskip format expression's "text-color" render tests * Update documentation * Add unit test for SymbolStyleLayer::has/setPaintOverrides * Add unit test for FormatSectionOverride expression * fix paint property overrides in expressions The previous way of checking for paint overrides didn't properly handle the case where there might be multiple cases with some not having overrides. * move setPaintOverrides to recalculate This seems more robust because it changes the calculated values consistently after they are calculated instead of just in some cases. This makes it a bit easier to understand what state the style layer is in. --- build/generate-style-code.js | 16 ++- src/data/bucket/symbol_bucket.js | 45 ++++++- src/data/program_configuration.js | 21 ++-- .../expression/definitions/coercion.js | 2 +- .../expression/definitions/format.js | 20 +++- .../definitions/format_section_override.js | 55 +++++++++ .../expression/evaluation_context.js | 4 +- src/style-spec/expression/index.js | 41 ++++--- src/style-spec/expression/types/formatted.js | 11 +- src/style-spec/reference/v8.json | 3 +- src/style-spec/style-spec.js | 3 +- src/style/properties.js | 9 +- src/style/style_layer.js | 14 ++- src/style/style_layer/layer_properties.js.ejs | 9 ++ src/style/style_layer/symbol_style_layer.js | 110 +++++++++++++++++- .../symbol_style_layer_properties.js | 6 +- src/symbol/quads.js | 7 +- src/symbol/shaping.js | 12 +- test/expected/text-shaping-default.json | 15 ++- test/expected/text-shaping-linebreak.json | 30 +++-- test/expected/text-shaping-newline.json | 30 +++-- .../text-shaping-newlines-in-middle.json | 30 +++-- test/expected/text-shaping-null.json | 6 +- test/expected/text-shaping-spacing.json | 15 ++- .../text-shaping-zero-width-space.json | 42 ++++--- test/ignores.json | 8 -- .../expression-tests/format/basic/test.json | 31 ++++- .../format/coercion/test.json | 9 +- .../format/data-driven-font/test.json | 6 +- .../format/implicit-coerce/test.json | 9 +- .../format/implicit-omit/test.json | 9 +- .../format/implicit/test.json | 9 +- .../style.json | 1 + test/unit/style-spec/spec.test.js | 3 +- .../symbol/format_section_override.test.js | 66 +++++++++++ test/unit/symbol/quads.test.js | 6 +- test/unit/symbol/symbol_style_layer.test.js | 104 +++++++++++++++++ 37 files changed, 674 insertions(+), 143 deletions(-) create mode 100644 src/style-spec/expression/definitions/format_section_override.js create mode 100644 test/unit/symbol/format_section_override.test.js create mode 100644 test/unit/symbol/symbol_style_layer.test.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 12c13c2afdc..d7c2f5792c2 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -45,7 +45,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'; @@ -297,6 +297,7 @@ class SymbolBucket implements Bucket { symbolInstanceIndexes: Array; writingModes: Array; allowVerticalPlacement: boolean; + hasPaintOverrides: boolean; constructor(options: BucketParameters) { this.collisionBoxArray = options.collisionBoxArray; @@ -308,6 +309,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; @@ -333,6 +335,9 @@ class SymbolBucket implements Bucket { } createArrays() { + const layout = this.layers[0].layout; + this.hasPaintOverrides = SymbolStyleLayer.hasPaintOverrides(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))); @@ -535,8 +540,7 @@ class SymbolBucket implements Bucket { const angle = (this.allowVerticalPlacement && writingMode === WritingMode.vertical) ? Math.PI / 2 : 0; - for (const symbol of quads) { - + const addSymbol = (symbol: SymbolQuad) => { const tl = symbol.tl, tr = symbol.tr, bl = symbol.bl, @@ -560,6 +564,39 @@ class SymbolBucket implements Bucket { segment.primitiveLength += 2; this.glyphOffsetArray.emplaceBack(symbol.glyphOffset[0]); + }; + + if (feature.text && feature.text.sections) { + const sections = feature.text.sections; + + if (this.hasPaintOverrides) { + let currentSectionIndex; + const populatePaintArrayForSection = (sectionIndex?: number, lastSection: boolean) => { + if (currentSectionIndex !== undefined && (currentSectionIndex !== sectionIndex || lastSection)) { + arrays.programConfigurations.populatePaintArrays(arrays.layoutVertexArray.length, feature, feature.index, {}, sections[currentSectionIndex]); + } + currentSectionIndex = sectionIndex; + }; + + for (const symbol of quads) { + populatePaintArrayForSection(symbol.sectionIndex, false); + addSymbol(symbol); + } + + // Populate paint arrays for the last section. + populatePaintArrayForSection(currentSectionIndex, true); + } else { + for (const symbol of quads) { + addSymbol(symbol); + } + arrays.programConfigurations.populatePaintArrays(arrays.layoutVertexArray.length, feature, feature.index, {}, sections[0]); + } + + } else { + for (const symbol of quads) { + addSymbol(symbol); + } + arrays.programConfigurations.populatePaintArrays(arrays.layoutVertexArray.length, feature, feature.index, {}); } arrays.placedSymbolArray.emplaceBack(labelAnchor.x, labelAnchor.y, @@ -573,8 +610,6 @@ class SymbolBucket implements Bucket { (false: any), // The crossTileID is only filled/used on the foreground for dynamic text anchors 0); - - arrays.programConfigurations.populatePaintArrays(arrays.layoutVertexArray.length, feature, feature.index, {}); } _addCollisionDebugVertex(layoutVertexArray: StructArray, collisionVertexArray: StructArray, point: Point, anchorX: number, anchorY: number, extrude: Point) { diff --git a/src/data/program_configuration.js b/src/data/program_configuration.js index 29b535ab5fb..dfa1cac5df5 100644 --- a/src/data/program_configuration.js +++ b/src/data/program_configuration.js @@ -32,6 +32,7 @@ import type { } from '../style-spec/expression'; import type {PossiblyEvaluated} from '../style/properties'; import type {FeatureStates} from '../source/source_state'; +import type {FormattedSection} from '../style-spec/expression/types/formatted'; export type BinderUniform = { name: string, @@ -78,7 +79,7 @@ interface Binder { maxValue: number; uniformNames: Array; - populatePaintArray(length: number, feature: Feature, imagePositions: {[string]: ImagePosition}): void; + populatePaintArray(length: number, feature: Feature, imagePositions: {[string]: ImagePosition}, formattedSection?: FormattedSection): void; updatePaintArray(start: number, length: number, feature: Feature, featureState: FeatureState, imagePositions: {[string]: ImagePosition}): void; upload(Context): void; destroy(): void; @@ -215,13 +216,13 @@ class SourceExpressionBinder implements Binder { setConstantPatternPositions() {} - populatePaintArray(newLength: number, feature: Feature) { + populatePaintArray(newLength: number, feature: Feature, imagePositions: {[string]: ImagePosition}, formattedSection?: FormattedSection) { const paintArray = this.paintVertexArray; const start = paintArray.length; paintArray.reserve(newLength); - const value = this.expression.evaluate(new EvaluationParameters(0), feature, {}); + const value = this.expression.evaluate(new EvaluationParameters(0), feature, {}, formattedSection); if (this.type === 'color') { const color = packColor(value); @@ -319,14 +320,14 @@ class CompositeExpressionBinder implements Binder { setConstantPatternPositions() {} - populatePaintArray(newLength: number, feature: Feature) { + populatePaintArray(newLength: number, feature: Feature, imagePositions: {[string]: ImagePosition}, formattedSection?: FormattedSection) { const paintArray = this.paintVertexArray; const start = paintArray.length; paintArray.reserve(newLength); - const min = this.expression.evaluate(new EvaluationParameters(this.zoom), feature, {}); - const max = this.expression.evaluate(new EvaluationParameters(this.zoom + 1), feature, {}); + const min = this.expression.evaluate(new EvaluationParameters(this.zoom), feature, {}, formattedSection); + const max = this.expression.evaluate(new EvaluationParameters(this.zoom + 1), feature, {}, formattedSection); if (this.type === 'color') { const minColor = packColor(min); @@ -611,10 +612,10 @@ export default class ProgramConfiguration { return self; } - populatePaintArrays(newLength: number, feature: Feature, index: number, imagePositions: {[string]: ImagePosition}) { + populatePaintArrays(newLength: number, feature: Feature, index: number, imagePositions: {[string]: ImagePosition}, formattedSection?: FormattedSection) { for (const property in this.binders) { const binder = this.binders[property]; - binder.populatePaintArray(newLength, feature, imagePositions); + binder.populatePaintArray(newLength, feature, imagePositions, formattedSection); } if (feature.id !== undefined) { this._featureMap.add(+feature.id, index, this._bufferOffset, newLength); @@ -743,9 +744,9 @@ export class ProgramConfigurationSet { this.needsUpload = false; } - populatePaintArrays(length: number, feature: Feature, index: number, imagePositions: {[string]: ImagePosition}) { + populatePaintArrays(length: number, feature: Feature, index: number, imagePositions: {[string]: ImagePosition}, formattedSection?: FormattedSection) { for (const key in this.programConfigurations) { - this.programConfigurations[key].populatePaintArrays(length, feature, index, imagePositions); + this.programConfigurations[key].populatePaintArrays(length, feature, index, imagePositions, formattedSection); } this.needsUpload = true; } diff --git a/src/style-spec/expression/definitions/coercion.js b/src/style-spec/expression/definitions/coercion.js index cfe4dae6a2e..6065194900d 100644 --- a/src/style-spec/expression/definitions/coercion.js +++ b/src/style-spec/expression/definitions/coercion.js @@ -114,7 +114,7 @@ class Coercion implements Expression { serialize() { if (this.type.kind === 'formatted') { - return new FormatExpression([{text: this.args[0], scale: null, font: null}]).serialize(); + return new FormatExpression([{text: this.args[0], scale: null, font: null, textColor: null}]).serialize(); } const serialized = [`to-${this.type.kind}`]; this.eachChild(child => { serialized.push(child.serialize()); }); diff --git a/src/style-spec/expression/definitions/format.js b/src/style-spec/expression/definitions/format.js index d37794d0e4e..4feece22ac7 100644 --- a/src/style-spec/expression/definitions/format.js +++ b/src/style-spec/expression/definitions/format.js @@ -1,6 +1,6 @@ // @flow -import { NumberType, ValueType, FormattedType, array, StringType } from '../types'; +import { NumberType, ValueType, FormattedType, array, StringType, ColorType } from '../types'; import Formatted, { FormattedSection } from '../types/formatted'; import { toString } from '../values'; @@ -13,6 +13,7 @@ type FormattedSectionExpression = { text: Expression, scale: Expression | null; font: Expression | null; + textColor: Expression | null; } export default class FormatExpression implements Expression { @@ -56,7 +57,13 @@ export default class FormatExpression implements Expression { font = context.parse(options['text-font'], 1, array(StringType)); if (!font) return null; } - sections.push({text, scale, font}); + + let textColor = null; + if (options['text-color']) { + textColor = context.parse(options['text-color'], 1, ColorType); + if (!textColor) return null; + } + sections.push({text, scale, font, textColor}); } return new FormatExpression(sections); @@ -68,7 +75,8 @@ export default class FormatExpression implements Expression { new FormattedSection( toString(section.text.evaluate(ctx)), section.scale ? section.scale.evaluate(ctx) : null, - section.font ? section.font.evaluate(ctx).join(',') : null + section.font ? section.font.evaluate(ctx).join(',') : null, + section.textColor ? section.textColor.evaluate(ctx) : null ) ) ); @@ -83,6 +91,9 @@ export default class FormatExpression implements Expression { if (section.font) { fn(section.font); } + if (section.textColor) { + fn(section.textColor); + } } } @@ -103,6 +114,9 @@ export default class FormatExpression implements Expression { if (section.font) { options['text-font'] = section.font.serialize(); } + if (section.textColor) { + options['text-color'] = section.textColor.serialize(); + } serialized.push(options); } return serialized; 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..3efd155d2fe --- /dev/null +++ b/src/style-spec/expression/definitions/format_section_override.js @@ -0,0 +1,55 @@ +// @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 this.defaultValue.property.specification.default; + } + + 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/evaluation_context.js b/src/style-spec/expression/evaluation_context.js index 6b301d61007..4b1bfc39a86 100644 --- a/src/style-spec/expression/evaluation_context.js +++ b/src/style-spec/expression/evaluation_context.js @@ -1,7 +1,7 @@ // @flow import { Color } from './values'; - +import type { FormattedSection } from './types/formatted'; import type { GlobalProperties, Feature, FeatureState } from './index'; const geometryTypes = ['Unknown', 'Point', 'LineString', 'Polygon']; @@ -10,6 +10,7 @@ class EvaluationContext { globals: GlobalProperties; feature: ?Feature; featureState: ?FeatureState; + formattedSection: ?FormattedSection; _parseColorCache: {[string]: ?Color}; @@ -17,6 +18,7 @@ class EvaluationContext { this.globals = (null: any); this.feature = null; this.featureState = null; + this.formattedSection = null; this._parseColorCache = {}; } diff --git a/src/style-spec/expression/index.js b/src/style-spec/expression/index.js index b974629ec32..591e62f8d72 100644 --- a/src/style-spec/expression/index.js +++ b/src/style-spec/expression/index.js @@ -24,6 +24,7 @@ import type {StylePropertySpecification} from '../style-spec'; import type {Result} from '../util/result'; import type {InterpolationType} from './definitions/interpolate'; import type {PropertyValueSpecification} from '../types'; +import type {FormattedSection} from './types/formatted'; export type Feature = { +type: 1 | 2 | 3 | 'Unknown' | 'Point' | 'MultiPoint' | 'LineString' | 'MultiLineString' | 'Polygon' | 'MultiPolygon', @@ -58,18 +59,20 @@ export class StyleExpression { this._enumValues = propertySpec && propertySpec.type === 'enum' ? propertySpec.values : null; } - evaluateWithoutErrorHandling(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState): any { + evaluateWithoutErrorHandling(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, formattedSection?: FormattedSection): any { this._evaluator.globals = globals; this._evaluator.feature = feature; this._evaluator.featureState = featureState; + this._evaluator.formattedSection = formattedSection; return this.expression.evaluate(this._evaluator); } - evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState): any { + evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, formattedSection?: FormattedSection): any { this._evaluator.globals = globals; this._evaluator.feature = feature || null; this._evaluator.featureState = featureState || null; + this._evaluator.formattedSection = formattedSection || null; try { const val = this.expression.evaluate(this._evaluator); @@ -132,12 +135,12 @@ export class ZoomConstantExpression { this.isStateDependent = kind !== ('constant': EvaluationKind) && !isConstant.isStateConstant(expression.expression); } - evaluateWithoutErrorHandling(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState): any { - return this._styleExpression.evaluateWithoutErrorHandling(globals, feature, featureState); + evaluateWithoutErrorHandling(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, formattedSection?: FormattedSection): any { + return this._styleExpression.evaluateWithoutErrorHandling(globals, feature, featureState, formattedSection); } - evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState): any { - return this._styleExpression.evaluate(globals, feature, featureState); + evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, formattedSection?: FormattedSection): any { + return this._styleExpression.evaluate(globals, feature, featureState, formattedSection); } } @@ -149,22 +152,20 @@ 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): any { - return this._styleExpression.evaluateWithoutErrorHandling(globals, feature, featureState); + evaluateWithoutErrorHandling(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, formattedSection?: FormattedSection): any { + return this._styleExpression.evaluateWithoutErrorHandling(globals, feature, featureState, formattedSection); } - evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState): any { - return this._styleExpression.evaluate(globals, feature, featureState); + evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, formattedSection?: FormattedSection): any { + return this._styleExpression.evaluate(globals, feature, featureState, formattedSection); } interpolationFactor(input: number, lower: number, upper: number): number { @@ -184,7 +185,7 @@ export type ConstantExpression = { export type SourceExpression = { kind: 'source', isStateDependent: boolean, - +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState) => any, + +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, formattedSection?: FormattedSection) => any, }; export type CameraExpression = { @@ -198,7 +199,7 @@ export type CameraExpression = { export type CompositeExpression = { kind: 'composite', isStateDependent: boolean, - +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState) => any, + +evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, formattedSection?: FormattedSection) => any, +interpolationFactor: (input: number, lower: number, upper: number) => number, zoomStops: Array, interpolationType: ?InterpolationType @@ -243,9 +244,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/expression/types/formatted.js b/src/style-spec/expression/types/formatted.js index 67e177127b4..6cd74986f9c 100644 --- a/src/style-spec/expression/types/formatted.js +++ b/src/style-spec/expression/types/formatted.js @@ -1,14 +1,18 @@ // @flow +import type Color from '../../util/color'; + export class FormattedSection { text: string; scale: number | null; fontStack: string | null; + textColor: Color | null; - constructor(text: string, scale: number | null, fontStack: string | null) { + constructor(text: string, scale: number | null, fontStack: string | null, textColor: Color | null) { this.text = text; this.scale = scale; this.fontStack = fontStack; + this.textColor = textColor; } } @@ -20,7 +24,7 @@ export default class Formatted { } static fromString(unformatted: string): Formatted { - return new Formatted([new FormattedSection(unformatted, null, null)]); + return new Formatted([new FormattedSection(unformatted, null, null, null)]); } toString(): string { @@ -38,6 +42,9 @@ export default class Formatted { if (section.scale) { options["font-scale"] = section.scale; } + if (section.textColor) { + options["text-color"] = ["literal", section.textColor]; + } serialized.push(options); } return serialized; diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json index 0d65c34cc32..a1bfdd8caca 100644 --- a/src/style-spec/reference/v8.json +++ b/src/style-spec/reference/v8.json @@ -2796,7 +2796,7 @@ } }, "format": { - "doc": "Returns `formatted` text containing annotations for use in mixed-format `text-field` entries. If set, the `text-font` argument overrides the font specified by the root layout properties. If set, the `font-scale` argument specifies a scaling factor relative to the `text-size` specified in the root layout properties.", + "doc": "Returns `formatted` text containing annotations for use in mixed-format `text-field` entries. If set, the `text-font` argument overrides the font specified by the root layout properties. If set, the `font-scale` argument specifies a scaling factor relative to the `text-size` specified in the root layout properties. If set, the `text-color` argument overrides the color specified by the paint properties for this layer.", "group": "Types", "sdk-support": { "basic functionality": { @@ -5141,6 +5141,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 967a224b03f..8dfa885b869 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 22013fbfa42..483b5ab2e9b 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 { @@ -715,6 +717,7 @@ export class Properties { defaultTransitionablePropertyValues: TransitionablePropertyValues; defaultTransitioningPropertyValues: TransitioningPropertyValues; defaultPossiblyEvaluatedValues: PossiblyEvaluatedPropertyValues; + overridableProperties: Array; constructor(properties: Props) { this.properties = properties; @@ -722,9 +725,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 5f60525e0d4..bfdf0f65253 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 { @@ -154,16 +154,18 @@ class StyleLayer extends Evented { const transitionable = this._transitionablePaint._values[name]; const isCrossFadedProperty = transitionable.property.specification["property-type"] === 'cross-faded-data-driven'; const wasDataDriven = transitionable.value.isDataDriven(); + const oldValue = transitionable.value; this._transitionablePaint.setValue(name, value); this._handleSpecialPaintPropertyUpdate(name); - const isDataDriven = this._transitionablePaint._values[name].value.isDataDriven(); + const newValue = this._transitionablePaint._values[name].value; + const isDataDriven = newValue.isDataDriven(); // if a cross-faded value is changed, we need to make sure the new icons get added to each tile's iconAtlas // so a call to _updateLayer is necessary, and we return true from this function so it gets called in // Style#setPaintProperty - return isDataDriven || wasDataDriven || isCrossFadedProperty; + return isDataDriven || wasDataDriven || isCrossFadedProperty || this._handleOverridablePaintPropertyUpdate(name, oldValue, newValue); } } @@ -171,6 +173,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 4392741825f..c1abf747a60 100644 --- a/src/style/style_layer/symbol_style_layer.js +++ b/src/style/style_layer/symbol_style_layer.js @@ -2,18 +2,39 @@ import StyleLayer from '../style_layer'; +import assert from 'assert'; import SymbolBucket from '../../data/bucket/symbol_bucket'; import resolveTokens from '../../util/resolve_tokens'; -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 type {Expression} from '../../style-spec/expression/expression'; +import {FormattedType} from '../../style-spec/expression/types'; +import {typeOf} from '../../style-spec/expression/values'; +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'; +import Literal from '../../style-spec/expression/definitions/literal'; class SymbolStyleLayer extends StyleLayer { _unevaluatedLayout: Layout; @@ -67,6 +88,8 @@ class SymbolStyleLayer extends StyleLayer { this.layout._values['text-writing-mode'] = ['horizontal']; } } + + this._setPaintOverrides(); } getValueAndResolveTokens(name: *, feature: Feature) { @@ -91,6 +114,85 @@ class SymbolStyleLayer extends StyleLayer { assert(false); // Should take a different path in FeatureIndex return false; } + + _setPaintOverrides() { + for (const overridable of properties.paint.overridableProperties) { + if (!SymbolStyleLayer.hasPaintOverride(this.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'); + const property = properties.paint.properties[propertyName]; + let hasOverrides = false; + + const checkSections = (sections) => { + for (const section of sections) { + if (property.overrides && property.overrides.hasOverride(section)) { + hasOverrides = true; + return; + } + } + }; + + if (textField.value.kind === 'constant' && textField.value.value instanceof Formatted) { + checkSections(textField.value.value.sections); + } else if (textField.value.kind === 'source') { + + const checkExpression = (expression: Expression) => { + if (hasOverrides) return; + + if (expression instanceof Literal && typeOf(expression.value) === FormattedType) { + const formatted: Formatted = ((expression.value): any); + checkSections(formatted.sections); + } else if (expression instanceof FormatExpression) { + checkSections(expression.sections); + } else { + expression.eachChild(checkExpression); + } + }; + + const expr: ZoomConstantExpression<'source'> = ((textField.value): any); + if (expr._styleExpression) { + checkExpression(expr._styleExpression.expression); + } + } + + return hasOverrides; + } + + 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 b92b2d212c4..858410c7fe4 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, @@ -131,7 +135,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/src/symbol/quads.js b/src/symbol/quads.js index 7ac39d50218..62040852174 100644 --- a/src/symbol/quads.js +++ b/src/symbol/quads.js @@ -36,7 +36,8 @@ export type SymbolQuad = { h: number }, writingMode: any | void, - glyphOffset: [number, number] + glyphOffset: [number, number], + sectionIndex: number }; /** @@ -108,7 +109,7 @@ export function getIconQuads(anchor: Anchor, } // Icon quad is padded, so texture coordinates also need to be padded. - return [{tl, tr, bl, br, tex: image.paddedRect, writingMode: undefined, glyphOffset: [0, 0]}]; + return [{tl, tr, bl, br, tex: image.paddedRect, writingMode: undefined, glyphOffset: [0, 0], sectionIndex: 0}]; } /** @@ -207,7 +208,7 @@ export function getGlyphQuads(anchor: Anchor, br._matMult(matrix); } - quads.push({tl, tr, bl, br, tex: rect, writingMode: shaping.writingMode, glyphOffset}); + quads.push({tl, tr, bl, br, tex: rect, writingMode: shaping.writingMode, glyphOffset, sectionIndex: positionedGlyph.sectionIndex}); } return quads; diff --git a/src/symbol/shaping.js b/src/symbol/shaping.js index a7c60a99a1e..71269981578 100644 --- a/src/symbol/shaping.js +++ b/src/symbol/shaping.js @@ -28,7 +28,8 @@ export type PositionedGlyph = { y: number, vertical: boolean, scale: number, - fontStack: string + fontStack: string, + sectionIndex: number }; // A collection of positioned glyphs and some metadata @@ -82,6 +83,10 @@ class TaggedString { return this.sections[this.sectionIndex[index]]; } + getSectionIndex(index: number): number { + return this.sectionIndex[index]; + } + getCharCode(index: number): number { return this.text.charCodeAt(index); } @@ -469,6 +474,7 @@ function shapeLines(shaping: Shaping, const lineStartIndex = positionedGlyphs.length; for (let i = 0; i < line.length(); i++) { const section = line.getSection(i); + const sectionIndex = line.getSectionIndex(i); const codePoint = line.getCharCode(i); // We don't know the baseline, but since we're laying out // at 24 points, we can calculate how much it will move when @@ -485,10 +491,10 @@ function shapeLines(shaping: Shaping, // If vertical placement is ebabled, don't verticalize glyphs that // are from complex text layout script, or whitespaces. (allowVerticalPlacement && (whitespace[codePoint] || charInComplexShapingScript(codePoint)))) { - positionedGlyphs.push({glyph: codePoint, x, y: y + baselineOffset, vertical: false, scale: section.scale, fontStack: section.fontStack}); + positionedGlyphs.push({glyph: codePoint, x, y: y + baselineOffset, vertical: false, scale: section.scale, fontStack: section.fontStack, sectionIndex}); x += glyph.metrics.advance * section.scale + spacing; } else { - positionedGlyphs.push({glyph: codePoint, x, y: y + baselineOffset, vertical: true, scale: section.scale, fontStack: section.fontStack}); + positionedGlyphs.push({glyph: codePoint, x, y: y + baselineOffset, vertical: true, scale: section.scale, fontStack: section.fontStack, sectionIndex}); x += ONE_EM * section.scale + spacing; } } diff --git a/test/expected/text-shaping-default.json b/test/expected/text-shaping-default.json index f103d4aa98c..bf75ee0d5bb 100644 --- a/test/expected/text-shaping-default.json +++ b/test/expected/text-shaping-default.json @@ -6,7 +6,8 @@ "y": -17, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 98, @@ -14,7 +15,8 @@ "y": -17, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 99, @@ -22,7 +24,8 @@ "y": -17, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 100, @@ -30,7 +33,8 @@ "y": -17, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 101, @@ -38,7 +42,8 @@ "y": -17, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 } ], "text": "abcde", diff --git a/test/expected/text-shaping-linebreak.json b/test/expected/text-shaping-linebreak.json index 9ba4b8298e3..f0f9b4430b4 100644 --- a/test/expected/text-shaping-linebreak.json +++ b/test/expected/text-shaping-linebreak.json @@ -6,7 +6,8 @@ "y": -29, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 98, @@ -14,7 +15,8 @@ "y": -29, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 99, @@ -22,7 +24,8 @@ "y": -29, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 100, @@ -30,7 +33,8 @@ "y": -29, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 101, @@ -38,7 +42,8 @@ "y": -29, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 97, @@ -46,7 +51,8 @@ "y": -5, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 98, @@ -54,7 +60,8 @@ "y": -5, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 99, @@ -62,7 +69,8 @@ "y": -5, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 100, @@ -70,7 +78,8 @@ "y": -5, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 101, @@ -78,7 +87,8 @@ "y": -5, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 } ], "text": "abcde abcde", diff --git a/test/expected/text-shaping-newline.json b/test/expected/text-shaping-newline.json index 77fa41532d8..a02ed316308 100644 --- a/test/expected/text-shaping-newline.json +++ b/test/expected/text-shaping-newline.json @@ -6,7 +6,8 @@ "y": -29, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 98, @@ -14,7 +15,8 @@ "y": -29, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 99, @@ -22,7 +24,8 @@ "y": -29, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 100, @@ -30,7 +33,8 @@ "y": -29, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 101, @@ -38,7 +42,8 @@ "y": -29, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 97, @@ -46,7 +51,8 @@ "y": -5, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 98, @@ -54,7 +60,8 @@ "y": -5, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 99, @@ -62,7 +69,8 @@ "y": -5, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 100, @@ -70,7 +78,8 @@ "y": -5, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 101, @@ -78,7 +87,8 @@ "y": -5, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 } ], "text": "abcde\nabcde", diff --git a/test/expected/text-shaping-newlines-in-middle.json b/test/expected/text-shaping-newlines-in-middle.json index e532bcb53a0..27f1ea28f35 100644 --- a/test/expected/text-shaping-newlines-in-middle.json +++ b/test/expected/text-shaping-newlines-in-middle.json @@ -6,7 +6,8 @@ "y": -41, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 98, @@ -14,7 +15,8 @@ "y": -41, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 99, @@ -22,7 +24,8 @@ "y": -41, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 100, @@ -30,7 +33,8 @@ "y": -41, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 101, @@ -38,7 +42,8 @@ "y": -41, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 97, @@ -46,7 +51,8 @@ "y": 7, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 98, @@ -54,7 +60,8 @@ "y": 7, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 99, @@ -62,7 +69,8 @@ "y": 7, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 100, @@ -70,7 +78,8 @@ "y": 7, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 101, @@ -78,7 +87,8 @@ "y": 7, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 } ], "text": "abcde\n\nabcde", diff --git a/test/expected/text-shaping-null.json b/test/expected/text-shaping-null.json index a9c08690b57..febc8a01e0b 100644 --- a/test/expected/text-shaping-null.json +++ b/test/expected/text-shaping-null.json @@ -6,7 +6,8 @@ "y": -17, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 105, @@ -14,7 +15,8 @@ "y": -17, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 } ], "text": "hi\u0000", diff --git a/test/expected/text-shaping-spacing.json b/test/expected/text-shaping-spacing.json index 9c1f2a704b2..960771413aa 100644 --- a/test/expected/text-shaping-spacing.json +++ b/test/expected/text-shaping-spacing.json @@ -6,7 +6,8 @@ "y": -17, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 98, @@ -14,7 +15,8 @@ "y": -17, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 99, @@ -22,7 +24,8 @@ "y": -17, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 100, @@ -30,7 +33,8 @@ "y": -17, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 101, @@ -38,7 +42,8 @@ "y": -17, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 } ], "text": "abcde", diff --git a/test/expected/text-shaping-zero-width-space.json b/test/expected/text-shaping-zero-width-space.json index 15fe489357c..85959cc53db 100644 --- a/test/expected/text-shaping-zero-width-space.json +++ b/test/expected/text-shaping-zero-width-space.json @@ -6,7 +6,8 @@ "y": -41, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 19977, @@ -14,7 +15,8 @@ "y": -41, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 19977, @@ -22,7 +24,8 @@ "y": -41, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 19977, @@ -30,7 +33,8 @@ "y": -41, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 19977, @@ -38,7 +42,8 @@ "y": -41, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 19977, @@ -46,7 +51,8 @@ "y": -41, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 19977, @@ -54,7 +60,8 @@ "y": -17, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 19977, @@ -62,7 +69,8 @@ "y": -17, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 19977, @@ -70,7 +78,8 @@ "y": -17, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 19977, @@ -78,7 +87,8 @@ "y": -17, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 19977, @@ -86,7 +96,8 @@ "y": -17, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 19977, @@ -94,7 +105,8 @@ "y": -17, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 19977, @@ -102,7 +114,8 @@ "y": 7, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 }, { "glyph": 19977, @@ -110,7 +123,8 @@ "y": 7, "vertical": false, "scale": 1, - "fontStack": "Test" + "fontStack": "Test", + "sectionIndex": 0 } ], "text": "三三​三三​三三​三三三三三三​三三", diff --git a/test/ignores.json b/test/ignores.json index 8e90170db52..f2be122166e 100644 --- a/test/ignores.json +++ b/test/ignores.json @@ -12,13 +12,5 @@ "render-tests/runtime-styling/image-update-pattern": "skip - https://github.com/mapbox/mapbox-gl-js/issues/4804", "render-tests/mixed-zoom/z10-z11": "current behavior conflicts with https://github.com/mapbox/mapbox-gl-js/pull/6803. can be fixed when https://github.com/mapbox/api-maps/issues/1480 is done", "render-tests/fill-extrusion-pattern/tile-buffer": "https://github.com/mapbox/mapbox-gl-js/issues/4403", - "render-tests/runtime-styling/paint-property-overriden-default-to-expression": "skip - https://github.com/mapbox/mapbox-gl-js/issues/8045", - "render-tests/runtime-styling/paint-property-overriden-default-to-literal": "skip - https://github.com/mapbox/mapbox-gl-js/issues/8045", - "render-tests/runtime-styling/paint-property-overriden-expression-to-literal": "skip - https://github.com/mapbox/mapbox-gl-js/issues/8045", - "render-tests/text-field/formatted-text-color-overrides": "skip - https://github.com/mapbox/mapbox-gl-js/issues/8045", - "render-tests/text-field/formatted-text-color": "skip - https://github.com/mapbox/mapbox-gl-js/issues/8045", - "render-tests/runtime-styling/layout-property-override-paint-property-expression": "skip - https://github.com/mapbox/mapbox-gl-js/issues/8045", - "render-tests/runtime-styling/layout-property-override-paint-property-literal": "skip - https://github.com/mapbox/mapbox-gl-js/issues/8045", - "render-tests/text-field/formatted-text-color-overrides-nested-expression": "skip - https://github.com/mapbox/mapbox-gl-js/issues/8045", "render-tests/symbol-sort-key/text-ignore-placement": "skip - text drawn over icons" } diff --git a/test/integration/expression-tests/format/basic/test.json b/test/integration/expression-tests/format/basic/test.json index e4420844045..2e9e51d62f5 100644 --- a/test/integration/expression-tests/format/basic/test.json +++ b/test/integration/expression-tests/format/basic/test.json @@ -16,6 +16,10 @@ "b" ] ] + }, + "d", + { + "text-color": "rgb(0, 255, 0)" } ], "inputs": [ @@ -37,17 +41,26 @@ { "text": "a", "scale": null, - "fontStack": null + "fontStack": null, + "textColor": null }, { "text": "b", "scale": 2, - "fontStack": null + "fontStack": null, + "textColor": null }, { "text": "c", "scale": null, - "fontStack": "a,b" + "fontStack": "a,b", + "textColor": null + }, + { + "text": "d", + "scale": null, + "fontStack": null, + "textColor": {"r":0,"g":1,"b":0,"a":1} } ] } @@ -69,6 +82,18 @@ "b" ] ] + }, + "d", + { + "text-color": [ + "literal", + { + "a": 1, + "b": 0, + "g": 1, + "r": 0 + } + ] } ] } diff --git a/test/integration/expression-tests/format/coercion/test.json b/test/integration/expression-tests/format/coercion/test.json index e7570b146ab..60980b55672 100644 --- a/test/integration/expression-tests/format/coercion/test.json +++ b/test/integration/expression-tests/format/coercion/test.json @@ -22,17 +22,20 @@ { "text": "a", "scale": null, - "fontStack": null + "fontStack": null, + "textColor": null }, { "text": "1", "scale": null, - "fontStack": null + "fontStack": null, + "textColor": null }, { "text": "true", "scale": null, - "fontStack": null + "fontStack": null, + "textColor": null } ] } diff --git a/test/integration/expression-tests/format/data-driven-font/test.json b/test/integration/expression-tests/format/data-driven-font/test.json index 0d731a3d3d1..dc0793d3169 100644 --- a/test/integration/expression-tests/format/data-driven-font/test.json +++ b/test/integration/expression-tests/format/data-driven-font/test.json @@ -23,7 +23,8 @@ { "text": "a", "scale": 1.5, - "fontStack": null + "fontStack": null, + "textColor": null } ] }, @@ -32,7 +33,8 @@ { "text": "a", "scale": 0.5, - "fontStack": null + "fontStack": null, + "textColor": null } ] } diff --git a/test/integration/expression-tests/format/implicit-coerce/test.json b/test/integration/expression-tests/format/implicit-coerce/test.json index dabce92bb5f..24533043cdd 100644 --- a/test/integration/expression-tests/format/implicit-coerce/test.json +++ b/test/integration/expression-tests/format/implicit-coerce/test.json @@ -21,7 +21,8 @@ { "text": "", "scale": null, - "fontStack": null + "fontStack": null, + "textColor": null } ] }, @@ -30,7 +31,8 @@ { "text": "0", "scale": null, - "fontStack": null + "fontStack": null, + "textColor": null } ] }, @@ -39,7 +41,8 @@ { "text": "a", "scale": null, - "fontStack": null + "fontStack": null, + "textColor": null } ] } diff --git a/test/integration/expression-tests/format/implicit-omit/test.json b/test/integration/expression-tests/format/implicit-omit/test.json index 44405f7fdd4..01d9f9e9d6f 100644 --- a/test/integration/expression-tests/format/implicit-omit/test.json +++ b/test/integration/expression-tests/format/implicit-omit/test.json @@ -21,7 +21,8 @@ { "text": "", "scale": null, - "fontStack": null + "fontStack": null, + "textColor": null } ] }, @@ -30,7 +31,8 @@ { "text": "0", "scale": null, - "fontStack": null + "fontStack": null, + "textColor": null } ] }, @@ -39,7 +41,8 @@ { "text": "a", "scale": null, - "fontStack": null + "fontStack": null, + "textColor": null } ] } diff --git a/test/integration/expression-tests/format/implicit/test.json b/test/integration/expression-tests/format/implicit/test.json index 19485f2d658..f9e2a151cce 100644 --- a/test/integration/expression-tests/format/implicit/test.json +++ b/test/integration/expression-tests/format/implicit/test.json @@ -21,7 +21,8 @@ { "text": "", "scale": null, - "fontStack": null + "fontStack": null, + "textColor": null } ] }, @@ -30,7 +31,8 @@ { "text": "0", "scale": null, - "fontStack": null + "fontStack": null, + "textColor": null } ] }, @@ -39,7 +41,8 @@ { "text": "a", "scale": null, - "fontStack": null + "fontStack": null, + "textColor": null } ] } diff --git a/test/integration/render-tests/text-field/formatted-text-color-overrides-nested-expression/style.json b/test/integration/render-tests/text-field/formatted-text-color-overrides-nested-expression/style.json index 19d1f05bdb7..abec25c59c6 100644 --- a/test/integration/render-tests/text-field/formatted-text-color-overrides-nested-expression/style.json +++ b/test/integration/render-tests/text-field/formatted-text-color-overrides-nested-expression/style.json @@ -37,6 +37,7 @@ "layout": { "text-field": [ "match", ["get", "case"], + "false", "error", "one", ["format", "Green", { "text-color": "green" }, "\n", {}, "Turquoise", {}], "default" ], diff --git a/test/unit/style-spec/spec.test.js b/test/unit/style-spec/spec.test.js index 79137837f54..ea30f7a02bd 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. diff --git a/test/unit/symbol/format_section_override.test.js b/test/unit/symbol/format_section_override.test.js new file mode 100644 index 00000000000..be428f96b6e --- /dev/null +++ b/test/unit/symbol/format_section_override.test.js @@ -0,0 +1,66 @@ +import { test } from '../../util/test'; +import { createExpression, ZoomConstantExpression } from '../../../src/style-spec/expression'; +import EvaluationContext from '../../../src/style-spec/expression/evaluation_context'; +import properties from '../../../src/style/style_layer/symbol_style_layer_properties'; +import {PossiblyEvaluatedPropertyValue} from '../../../src/style/properties'; +import FormatSectionOverride from '../../../src/style-spec/expression/definitions/format_section_override'; + +test('evaluate', (t) => { + + t.test('override constant', (t) => { + const defaultColor = { "r": 0, "g": 1, "b": 0, "a": 1 }; + const overridenColor = { "r": 1, "g": 0, "b": 0, "a": 1 }; + const overriden = new PossiblyEvaluatedPropertyValue( + properties.paint.properties['text-color'], + { kind: 'constant', value: defaultColor }, + {zoom: 0, zoomHistory: {}} + ); + + const override = new FormatSectionOverride(overriden); + const ctx = new EvaluationContext(); + ctx.feature = {}; + ctx.featureState = {}; + t.deepEqual(override.evaluate(ctx), defaultColor); + + ctx.formattedSection = {textColor: overridenColor}; + t.deepEqual(override.evaluate(ctx), overridenColor); + + t.end(); + }); + + t.test('override expression', (t) => { + const warn = console.warn; + console.warn = (_) => {}; + const defaultColor = { "r": 0, "g": 0, "b": 0, "a": 1 }; + const propertyColor = { "r": 1, "g": 0, "b": 0, "a": 1 }; + const overridenColor = { "r": 0, "g": 0, "b": 1, "a": 1 }; + const styleExpr = createExpression( + ["get", "color"], + properties.paint.properties['text-color'].specification); + + const sourceExpr = new ZoomConstantExpression('source', styleExpr.value); + const overriden = new PossiblyEvaluatedPropertyValue( + properties.paint.properties['text-color'], + sourceExpr, + {zoom: 0, zoomHistory: {}} + ); + + const override = new FormatSectionOverride(overriden); + const ctx = new EvaluationContext(); + ctx.feature = { properties: {}}; + ctx.featureState = {}; + + t.deepEqual(override.evaluate(ctx), defaultColor); + + ctx.feature.properties.color = "red"; + t.deepEqual(override.evaluate(ctx), propertyColor); + + ctx.formattedSection = {textColor: overridenColor}; + t.deepEqual(override.evaluate(ctx), overridenColor); + + console.warn = warn; + t.end(); + }); + + t.end(); +}); diff --git a/test/unit/symbol/quads.test.js b/test/unit/symbol/quads.test.js index 08a6ec8ecec..fd5d7846992 100644 --- a/test/unit/symbol/quads.test.js +++ b/test/unit/symbol/quads.test.js @@ -37,7 +37,8 @@ test('getIconQuads', (t) => { br: { x: 9, y: 7 }, tex: { x: 0, y: 0, w: 17, h: 13 }, writingMode: null, - glyphOffset: [0, 0] + glyphOffset: [0, 0], + sectionIndex: 0 }]); t.end(); }); @@ -55,7 +56,8 @@ test('getIconQuads', (t) => { br: { x: 9, y: 7 }, tex: { x: 0, y: 0, w: 17, h: 13 }, writingMode: null, - glyphOffset: [0, 0] + glyphOffset: [0, 0], + sectionIndex: 0 }]); t.end(); }); diff --git a/test/unit/symbol/symbol_style_layer.test.js b/test/unit/symbol/symbol_style_layer.test.js new file mode 100644 index 00000000000..4696d41a397 --- /dev/null +++ b/test/unit/symbol/symbol_style_layer.test.js @@ -0,0 +1,104 @@ +import { test } from '../../util/test'; +import SymbolStyleLayer from '../../../src/style/style_layer/symbol_style_layer'; +import FormatSectionOverride from '../../../src/style-spec/expression/definitions/format_section_override'; +import properties from '../../../src/style/style_layer/symbol_style_layer_properties'; + +function createSymbolLayer(layerProperties) { + const layer = new SymbolStyleLayer(layerProperties); + layer.recalculate({zoom: 0, zoomHistory: {}}); + return layer; +} + +function isOverriden(paintProperty) { + if (paintProperty.value.kind === 'source' || paintProperty.value.kind === 'composite') { + return paintProperty.value._styleExpression.expression instanceof FormatSectionOverride; + } + return false; +} + +test('setPaintOverrides', (t) => { + t.test('setPaintOverrides, no overrides', (t) => { + const layer = createSymbolLayer({}); + layer._setPaintOverrides(); + for (const overridable of properties.paint.overridableProperties) { + t.equal(isOverriden(layer.paint.get(overridable)), false); + } + t.end(); + }); + + t.test('setPaintOverrides, format expression, overriden text-color', (t) => { + const props = { layout: {'text-field': ["format", "text", {"text-color": "yellow"}]} }; + const layer = createSymbolLayer(props); + layer._setPaintOverrides(); + t.equal(isOverriden(layer.paint.get('text-color')), true); + t.end(); + }); + + t.test('setPaintOverrides, format expression, no overrides', (t) => { + const props = { layout: {'text-field': ["format", "text", {}]} }; + const layer = createSymbolLayer(props); + layer._setPaintOverrides(); + t.equal(isOverriden(layer.paint.get('text-color')), false); + t.end(); + }); + + t.end(); +}); + +test('hasPaintOverrides', (t) => { + t.test('undefined', (t) => { + const layer = createSymbolLayer({}); + t.equal(SymbolStyleLayer.hasPaintOverrides(layer.layout), false); + t.end(); + }); + + t.test('constant, Formatted type, overriden text-color', (t) => { + const props = { layout: {'text-field': ["format", "text", {"text-color": "red"}]} }; + const layer = createSymbolLayer(props); + t.equal(SymbolStyleLayer.hasPaintOverrides(layer.layout), true); + t.end(); + }); + + t.test('constant, Formatted type, no overrides', (t) => { + const props = { layout: {'text-field': ["format", "text", {"font-scale": 0.8}]} }; + const layer = createSymbolLayer(props); + t.equal(SymbolStyleLayer.hasPaintOverrides(layer.layout), false); + t.end(); + }); + + t.test('format expression, overriden text-color', (t) => { + const props = { layout: {'text-field': ["format", ["get", "name"], {"text-color":"red"}]} }; + const layer = createSymbolLayer(props); + t.equal(SymbolStyleLayer.hasPaintOverrides(layer.layout), true); + t.end(); + }); + + t.test('format expression, no overrides', (t) => { + const props = { layout: {'text-field': ["format", ["get", "name"], {}]} }; + const layer = createSymbolLayer(props); + t.equal(SymbolStyleLayer.hasPaintOverrides(layer.layout), false); + t.end(); + }); + + t.test('nested expression, overriden text-color', (t) => { + const matchExpr = ["match", ["get", "case"], + "one", ["format", "color", {"text-color": "blue"}], + "default"]; + const props = { layout: {'text-field': matchExpr} }; + const layer = createSymbolLayer(props); + t.equal(SymbolStyleLayer.hasPaintOverrides(layer.layout), true); + t.end(); + }); + + t.test('nested expression, no overrides', (t) => { + const matchExpr = ["match", ["get", "case"], + "one", ["format", "b&w", {}], + "default"]; + const props = { layout: {'text-field': matchExpr} }; + const layer = createSymbolLayer(props); + t.equal(SymbolStyleLayer.hasPaintOverrides(layer.layout), false); + t.end(); + }); + + t.end(); +});