Skip to content

Commit

Permalink
Allow overriding SymbolLayer's paint properties using format expressi…
Browse files Browse the repository at this point in the history
…on 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.
  • Loading branch information
alexshalamov authored and ansis committed Aug 13, 2019
1 parent 4b6784f commit b4fd6ee
Show file tree
Hide file tree
Showing 37 changed files with 674 additions and 143 deletions.
16 changes: 15 additions & 1 deletion build/generate-style-code.js
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down Expand Up @@ -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':
Expand Down
45 changes: 40 additions & 5 deletions src/data/bucket/symbol_bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -297,6 +297,7 @@ class SymbolBucket implements Bucket {
symbolInstanceIndexes: Array<number>;
writingModes: Array<number>;
allowVerticalPlacement: boolean;
hasPaintOverrides: boolean;

constructor(options: BucketParameters<SymbolStyleLayer>) {
this.collisionBoxArray = options.collisionBoxArray;
Expand All @@ -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;
Expand All @@ -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)));

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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) {
Expand Down
21 changes: 11 additions & 10 deletions src/data/program_configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -78,7 +79,7 @@ interface Binder<T> {
maxValue: number;
uniformNames: Array<string>;

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;
Expand Down Expand Up @@ -215,13 +216,13 @@ class SourceExpressionBinder<T> implements Binder<T> {

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);
Expand Down Expand Up @@ -319,14 +320,14 @@ class CompositeExpressionBinder<T> implements Binder<T> {

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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -743,9 +744,9 @@ export class ProgramConfigurationSet<Layer: TypedStyleLayer> {
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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/style-spec/expression/definitions/coercion.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()); });
Expand Down
20 changes: 17 additions & 3 deletions src/style-spec/expression/definitions/format.js
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -13,6 +13,7 @@ type FormattedSectionExpression = {
text: Expression,
scale: Expression | null;
font: Expression | null;
textColor: Expression | null;
}

export default class FormatExpression implements Expression {
Expand Down Expand Up @@ -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);
Expand All @@ -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
)
)
);
Expand All @@ -83,6 +91,9 @@ export default class FormatExpression implements Expression {
if (section.font) {
fn(section.font);
}
if (section.textColor) {
fn(section.textColor);
}
}
}

Expand All @@ -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;
Expand Down
55 changes: 55 additions & 0 deletions src/style-spec/expression/definitions/format_section_override.js
Original file line number Diff line number Diff line change
@@ -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<T> implements Expression {
type: Type;
defaultValue: PossiblyEvaluatedPropertyValue<T>;

constructor(defaultValue: PossiblyEvaluatedPropertyValue<T>) {
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<Value | void> {
return [undefined];
}

serialize() {
return null;
}
}

register('FormatSectionOverride', FormatSectionOverride, {omit: ['defaultValue']});
4 changes: 3 additions & 1 deletion src/style-spec/expression/evaluation_context.js
Original file line number Diff line number Diff line change
@@ -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'];
Expand All @@ -10,13 +10,15 @@ class EvaluationContext {
globals: GlobalProperties;
feature: ?Feature;
featureState: ?FeatureState;
formattedSection: ?FormattedSection;

_parseColorCache: {[string]: ?Color};

constructor() {
this.globals = (null: any);
this.feature = null;
this.featureState = null;
this.formattedSection = null;
this._parseColorCache = {};
}

Expand Down
Loading

0 comments on commit b4fd6ee

Please sign in to comment.