From ce87f20abb7847832f9a13fc9408be652f6320b3 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Tue, 21 Nov 2017 16:49:00 -0500 Subject: [PATCH] Make Expression classes serializable --- src/style-spec/expression/index.js | 214 +++++++++++++----- src/style-spec/feature_filter/index.js | 2 +- .../validate/validate_expression.js | 2 +- src/util/web_worker_transfer.js | 23 ++ test/expression.test.js | 6 +- 5 files changed, 184 insertions(+), 63 deletions(-) diff --git a/src/style-spec/expression/index.js b/src/style-spec/expression/index.js index 446c072692c..bed2799223a 100644 --- a/src/style-spec/expression/index.js +++ b/src/style-spec/expression/index.js @@ -1,6 +1,7 @@ // @flow const assert = require('assert'); +const extend = require('../util/extend'); const ParsingError = require('./parsing_error'); const ParsingContext = require('./parsing_context'); const EvaluationContext = require('./evaluation_context'); @@ -19,6 +20,7 @@ import type {Value} from './values'; import type {Expression} from './expression'; import type {StylePropertySpecification} from '../style-spec'; import type {Result} from '../util/result'; +import type {InterpolationType} from './definitions/interpolate'; export type Feature = { +type: 1 | 2 | 3 | 'Unknown' | 'Point' | 'MultiPoint' | 'LineString' | 'MultiLineString' | 'Polygon' | 'MultiPolygon', @@ -31,10 +33,70 @@ export type GlobalProperties = { heatmapDensity?: number }; -export type StyleExpression = { - evaluate: (globals: GlobalProperties, feature?: Feature) => any, - parsed: Expression -}; +class StyleExpression { + expression: Expression; + + _evaluator: EvaluationContext; + + constructor(expression: Expression) { + this.expression = expression; + } + + evaluate(globals: GlobalProperties, feature?: Feature): any { + if (!this._evaluator) { + this._evaluator = new EvaluationContext(); + } + + this._evaluator.globals = globals; + this._evaluator.feature = feature; + return this.expression.evaluate(this._evaluator); + } +} + +class StyleExpressionWithErrorHandling extends StyleExpression { + _defaultValue: Value; + _warningHistory: {[key: string]: boolean}; + _enumValues: {[string]: any}; + + _evaluator: EvaluationContext; + + constructor(expression: Expression, propertySpec: StylePropertySpecification) { + super(expression); + this._warningHistory = {}; + this._defaultValue = getDefaultValue(propertySpec); + if (propertySpec.type === 'enum') { + this._enumValues = propertySpec.values; + } + } + + evaluate(globals: GlobalProperties, feature?: Feature) { + if (!this._evaluator) { + this._evaluator = new EvaluationContext(); + } + + this._evaluator.globals = globals; + this._evaluator.feature = feature; + + try { + const val = this.expression.evaluate(this._evaluator); + if (val === null || val === undefined) { + return this._defaultValue; + } + if (this._enumValues && !(val in this._enumValues)) { + throw new RuntimeError(`Expected value to be one of ${Object.keys(this._enumValues).map(v => JSON.stringify(v)).join(', ')}, but found ${JSON.stringify(val)} instead.`); + } + return val; + } catch (e) { + if (!this._warningHistory[e.message]) { + this._warningHistory[e.message] = true; + if (typeof console !== 'undefined') { + console.warn(e.message); + } + } + return this._defaultValue; + } + } +} function isExpression(expression: mixed) { return Array.isArray(expression) && expression.length > 0 && @@ -60,70 +122,75 @@ function createExpression(expression: mixed, return error(parser.errors); } - const evaluator = new EvaluationContext(); - - let evaluate; if (options.handleErrors === false) { - evaluate = function (globals, feature) { - evaluator.globals = globals; - evaluator.feature = feature; - return parsed.evaluate(evaluator); - }; + return success(new StyleExpression(parsed)); } else { - const warningHistory: {[key: string]: boolean} = {}; - const defaultValue = getDefaultValue(propertySpec); - let enumValues; - if (propertySpec.type === 'enum') { - enumValues = propertySpec.values; + return success(new StyleExpressionWithErrorHandling(parsed, propertySpec)); + } +} + +class ZoomConstantExpression { + kind: Kind; + _styleExpression: StyleExpression; + constructor(kind: Kind, expression: StyleExpression) { + this.kind = kind; + this._styleExpression = expression; + } + evaluate(globals: GlobalProperties, feature?: Feature): any { + return this._styleExpression.evaluate(globals, feature); + } +} + +class ZoomDependentExpression { + kind: Kind; + zoomStops: Array; + + _styleExpression: StyleExpression; + _interpolationType: ?InterpolationType; + + constructor(kind: Kind, expression: StyleExpression, zoomCurve: Step | Interpolate) { + this.kind = kind; + this.zoomStops = zoomCurve.labels; + this._styleExpression = expression; + if (zoomCurve instanceof Interpolate) { + this._interpolationType = zoomCurve.interpolation; } - evaluate = function (globals, feature) { - evaluator.globals = globals; - evaluator.feature = feature; - try { - const val = parsed.evaluate(evaluator); - if (val === null || val === undefined) { - return defaultValue; - } - if (enumValues && !(val in enumValues)) { - throw new RuntimeError(`Expected value to be one of ${Object.keys(enumValues).map(v => JSON.stringify(v)).join(', ')}, but found ${JSON.stringify(val)} instead.`); - } - return val; - } catch (e) { - if (!warningHistory[e.message]) { - warningHistory[e.message] = true; - if (typeof console !== 'undefined') { - console.warn(e.message); - } - } - return defaultValue; - } - }; } - return success({ evaluate, parsed }); + evaluate(globals: GlobalProperties, feature?: Feature): any { + return this._styleExpression.evaluate(globals, feature); + } + + interpolationFactor(input: number, lower: number, upper: number): number { + if (this._interpolationType) { + return Interpolate.interpolationFactor(this._interpolationType, input, lower, upper); + } else { + return 0; + } + } } export type ConstantExpression = { kind: 'constant', - evaluate: (globals: GlobalProperties, feature?: Feature) => any -}; + +evaluate: (globals: GlobalProperties, feature?: Feature) => any, +} export type SourceExpression = { kind: 'source', - evaluate: (globals: GlobalProperties, feature?: Feature) => any, + +evaluate: (globals: GlobalProperties, feature?: Feature) => any, }; export type CameraExpression = { kind: 'camera', - evaluate: (globals: GlobalProperties, feature?: Feature) => any, - interpolationFactor: (input: number, lower: number, upper: number) => number, + +evaluate: (globals: GlobalProperties, feature?: Feature) => any, + +interpolationFactor: (input: number, lower: number, upper: number) => number, zoomStops: Array }; export type CompositeExpression = { kind: 'composite', - evaluate: (globals: GlobalProperties, feature?: Feature) => any, - interpolationFactor: (input: number, lower: number, upper: number) => number, + +evaluate: (globals: GlobalProperties, feature?: Feature) => any, + +interpolationFactor: (input: number, lower: number, upper: number) => number, zoomStops: Array }; @@ -141,7 +208,7 @@ function createPropertyExpression(expression: mixed, return expression; } - const {evaluate, parsed} = expression.value; + const parsed = expression.value.expression; const isFeatureConstant = isConstant.isFeatureConstant(parsed); if (!isFeatureConstant && !propertySpec['property-function']) { @@ -164,26 +231,50 @@ function createPropertyExpression(expression: mixed, if (!zoomCurve) { return success(isFeatureConstant ? - { kind: 'constant', parsed, evaluate } : - { kind: 'source', parsed, evaluate }); + (new ZoomConstantExpression('constant', expression.value): ConstantExpression) : + (new ZoomConstantExpression('source', expression.value): SourceExpression)); } - const interpolationFactor = zoomCurve instanceof Interpolate ? - Interpolate.interpolationFactor.bind(undefined, zoomCurve.interpolation) : - () => 0; - const zoomStops = zoomCurve.labels; - return success(isFeatureConstant ? - { kind: 'camera', parsed, evaluate, interpolationFactor, zoomStops } : - { kind: 'composite', parsed, evaluate, interpolationFactor, zoomStops }); + (new ZoomDependentExpression('camera', expression.value, zoomCurve): CameraExpression) : + (new ZoomDependentExpression('composite', expression.value, zoomCurve): CompositeExpression)); } const {isFunction, createFunction} = require('../function'); const {Color} = require('./values'); +// serialization wrapper for old-style stop functions normalized to the +// expression interface +class StylePropertyFunction { + _parameters: PropertyValueSpecification; + _specification: StylePropertySpecification; + + kind: 'constant' | 'source' | 'camera' | 'composite'; + evaluate: (globals: GlobalProperties, feature?: Feature) => any; + interpolationFactor: ?(input: number, lower: number, upper: number) => number; + zoomStops: ?Array; + + constructor(parameters: PropertyValueSpecification, specification: StylePropertySpecification) { + this._parameters = parameters; + this._specification = specification; + extend(this, createFunction(this._parameters, this._specification)); + } + + static deserialize(serialized: {_parameters: PropertyValueSpecification, _specification: StylePropertySpecification}) { + return new StylePropertyFunction(serialized._parameters, serialized._specification); + } + + static serialize(input: StylePropertyFunction) { + return { + _parameters: input._parameters, + _specification: input._specification + }; + } +} + function normalizePropertyExpression(value: PropertyValueSpecification, specification: StylePropertySpecification): StylePropertyExpression { if (isFunction(value)) { - return createFunction(value, specification); + return (new StylePropertyFunction(value, specification): any); } else if (isExpression(value)) { const expression = createPropertyExpression(value, specification); @@ -206,10 +297,15 @@ function normalizePropertyExpression(value: PropertyValueSpecification, sp } module.exports = { + StyleExpression, + StyleExpressionWithErrorHandling, isExpression, createExpression, createPropertyExpression, - normalizePropertyExpression + normalizePropertyExpression, + ZoomConstantExpression, + ZoomDependentExpression, + StylePropertyFunction }; // Zoom-dependent expressions may only use ["zoom"] as the input to a top-level "step" or "interpolate" diff --git a/src/style-spec/feature_filter/index.js b/src/style-spec/feature_filter/index.js index 7d9bbea6aa0..4f93d353f6d 100644 --- a/src/style-spec/feature_filter/index.js +++ b/src/style-spec/feature_filter/index.js @@ -76,7 +76,7 @@ function createFilter(filter: any): FeatureFilter { if (compiled.result === 'error') { throw new Error(compiled.value.map(err => `${err.key}: ${err.message}`).join(', ')); } else { - return compiled.value.evaluate; + return (globalProperties: GlobalProperties, feature: VectorTileFeature) => compiled.value.evaluate(globalProperties, feature); } } diff --git a/src/style-spec/validate/validate_expression.js b/src/style-spec/validate/validate_expression.js index 37a766bfbae..7c94fd54a0b 100644 --- a/src/style-spec/validate/validate_expression.js +++ b/src/style-spec/validate/validate_expression.js @@ -13,7 +13,7 @@ module.exports = function validateExpression(options: any) { } if (options.expressionContext === 'property' && options.propertyKey === 'text-font' && - (expression.value: any).parsed.possibleOutputs().indexOf(undefined) !== -1) { + (expression.value: any)._styleExpression.expression.possibleOutputs().indexOf(undefined) !== -1) { return [new ValidationError(options.key, options.value, 'Invalid data expression for "text-font". Output values must be contained as literals within the expression.')]; } diff --git a/src/util/web_worker_transfer.js b/src/util/web_worker_transfer.js index 46ffee834e3..d861b858ce0 100644 --- a/src/util/web_worker_transfer.js +++ b/src/util/web_worker_transfer.js @@ -1,7 +1,17 @@ // @flow const assert = require('assert'); + const Color = require('../style-spec/util/color'); +const { + StylePropertyFunction, + StyleExpression, + StyleExpressionWithErrorHandling, + ZoomDependentExpression, + ZoomConstantExpression +} = require('../style-spec/expression'); +const {CompoundExpression} = require('../style-spec/expression/compound_expression'); +const expressions = require('../style-spec/expression/definitions'); import type {Transferable} from '../types/transferable'; @@ -59,6 +69,19 @@ function register(klass: Class, options: RegisterOptions = {}) { register(Object); register(Color); + +register(StylePropertyFunction); +register(StyleExpression, {omit: ['_evaluator']}); +register(StyleExpressionWithErrorHandling, {omit: ['_evaluator']}); +register(ZoomDependentExpression); +register(ZoomConstantExpression); +register(CompoundExpression, {omit: ['_evaluate']}); +for (const name in expressions) { + const Expression = expressions[name]; + if (registry[Expression.name]) continue; + register(expressions[name]); +} + /** * Serialize the given object for transfer to or from a web worker. * diff --git a/test/expression.test.js b/test/expression.test.js index 9024c2ec3cb..2fdb9e706ab 100644 --- a/test/expression.test.js +++ b/test/expression.test.js @@ -32,6 +32,8 @@ expressionSuite.run('js', { ignores, tests }, (fixture) => { expression = expression.value; + const type = expression._styleExpression.expression.type; // :scream: + const outputs = []; const result = { outputs, @@ -39,7 +41,7 @@ expressionSuite.run('js', { ignores, tests }, (fixture) => { result: 'success', isZoomConstant: expression.kind === 'constant' || expression.kind === 'source', isFeatureConstant: expression.kind === 'constant' || expression.kind === 'camera', - type: toString(expression.parsed.type) + type: toString(type) } }; @@ -53,7 +55,7 @@ expressionSuite.run('js', { ignores, tests }, (fixture) => { feature.type = input[1].geometry.type; } let value = expression.evaluate(input[0], feature); - if (expression.parsed.type.kind === 'color') { + if (type.kind === 'color') { value = [value.r, value.g, value.b, value.a]; } outputs.push(value);