Skip to content

Commit

Permalink
Make Expression classes serializable
Browse files Browse the repository at this point in the history
  • Loading branch information
Anand Thakker committed Nov 28, 2017
1 parent 355bf6b commit ce87f20
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 63 deletions.
214 changes: 155 additions & 59 deletions src/style-spec/expression/index.js
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -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',
Expand All @@ -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 &&
Expand All @@ -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: 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: Kind;
zoomStops: Array<number>;

_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<number>
};

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<number>
};

Expand All @@ -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']) {
Expand All @@ -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<T> {
_parameters: PropertyValueSpecification<T>;
_specification: StylePropertySpecification;

kind: 'constant' | 'source' | 'camera' | 'composite';
evaluate: (globals: GlobalProperties, feature?: Feature) => any;
interpolationFactor: ?(input: number, lower: number, upper: number) => number;
zoomStops: ?Array<number>;

constructor(parameters: PropertyValueSpecification<T>, specification: StylePropertySpecification) {
this._parameters = parameters;
this._specification = specification;
extend(this, createFunction(this._parameters, this._specification));
}

static deserialize(serialized: {_parameters: PropertyValueSpecification<T>, _specification: StylePropertySpecification}) {
return new StylePropertyFunction(serialized._parameters, serialized._specification);
}

static serialize(input: StylePropertyFunction<T>) {
return {
_parameters: input._parameters,
_specification: input._specification
};
}
}

function normalizePropertyExpression<T>(value: PropertyValueSpecification<T>, 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);
Expand All @@ -206,10 +297,15 @@ function normalizePropertyExpression<T>(value: PropertyValueSpecification<T>, 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"
Expand Down
2 changes: 1 addition & 1 deletion src/style-spec/feature_filter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/style-spec/validate/validate_expression.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.')];
}

Expand Down
23 changes: 23 additions & 0 deletions src/util/web_worker_transfer.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -59,6 +69,19 @@ function register<T: any>(klass: Class<T>, options: RegisterOptions<T> = {}) {

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.
*
Expand Down
6 changes: 4 additions & 2 deletions test/expression.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,16 @@ expressionSuite.run('js', { ignores, tests }, (fixture) => {

expression = expression.value;

const type = expression._styleExpression.expression.type; // :scream:

const outputs = [];
const result = {
outputs,
compiled: {
result: 'success',
isZoomConstant: expression.kind === 'constant' || expression.kind === 'source',
isFeatureConstant: expression.kind === 'constant' || expression.kind === 'camera',
type: toString(expression.parsed.type)
type: toString(type)
}
};

Expand All @@ -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);
Expand Down

0 comments on commit ce87f20

Please sign in to comment.