diff --git a/src/style-spec/expression/compound_expression.js b/src/style-spec/expression/compound_expression.js index 8052e34dd18..e5417f1c6bf 100644 --- a/src/style-spec/expression/compound_expression.js +++ b/src/style-spec/expression/compound_expression.js @@ -43,6 +43,10 @@ class CompoundExpression implements Expression { return [undefined]; } + serialize() { + return [this.name].concat(this.args.map(arg => arg.serialize())); + } + static parse(args: Array, context: ParsingContext): ?Expression { const op: string = (args[0]: any); const definition = CompoundExpression.definitions[op]; diff --git a/src/style-spec/expression/definitions/array.js b/src/style-spec/expression/definitions/array.js index 2d18b4b7f3f..e95853dabe3 100644 --- a/src/style-spec/expression/definitions/array.js +++ b/src/style-spec/expression/definitions/array.js @@ -75,6 +75,22 @@ class ArrayAssertion implements Expression { possibleOutputs() { return this.input.possibleOutputs(); } + + serialize() { + const serialized = ["array"]; + const itemType = this.type.itemType; + if (itemType.kind === 'string' || + itemType.kind === 'number' || + itemType.kind === 'boolean') { + serialized.push(itemType.kind); + const N = this.type.N; + if (typeof N === 'number') { + serialized.push(N); + } + } + serialized.push(this.input.serialize()); + return serialized; + } } export default ArrayAssertion; diff --git a/src/style-spec/expression/definitions/assertion.js b/src/style-spec/expression/definitions/assertion.js index 40fa85bbc70..d76337dd067 100644 --- a/src/style-spec/expression/definitions/assertion.js +++ b/src/style-spec/expression/definitions/assertion.js @@ -76,6 +76,10 @@ class Assertion implements Expression { possibleOutputs() { return [].concat(...this.args.map((arg) => arg.possibleOutputs())); } + + serialize() { + return [this.type.kind].concat(this.args.map(arg => arg.serialize())); + } } export default Assertion; diff --git a/src/style-spec/expression/definitions/at.js b/src/style-spec/expression/definitions/at.js index 3dbbb2cf857..bc615470361 100644 --- a/src/style-spec/expression/definitions/at.js +++ b/src/style-spec/expression/definitions/at.js @@ -61,6 +61,10 @@ class At implements Expression { possibleOutputs() { return [undefined]; } + + serialize() { + return ["at", this.index.serialize(), this.input.serialize()]; + } } export default At; diff --git a/src/style-spec/expression/definitions/case.js b/src/style-spec/expression/definitions/case.js index 5929578e303..1d565949ebc 100644 --- a/src/style-spec/expression/definitions/case.js +++ b/src/style-spec/expression/definitions/case.js @@ -76,6 +76,12 @@ class Case implements Expression { .concat(...this.branches.map(([_, out]) => out.possibleOutputs())) .concat(this.otherwise.possibleOutputs()); } + + serialize() { + const serialized = ["case"]; + this.eachChild(child => { serialized.push(child.serialize()); }); + return serialized; + } } export default Case; diff --git a/src/style-spec/expression/definitions/coalesce.js b/src/style-spec/expression/definitions/coalesce.js index af764bbdd91..b9c759581f6 100644 --- a/src/style-spec/expression/definitions/coalesce.js +++ b/src/style-spec/expression/definitions/coalesce.js @@ -66,6 +66,12 @@ class Coalesce implements Expression { possibleOutputs() { return [].concat(...this.args.map((arg) => arg.possibleOutputs())); } + + serialize() { + const serialized = ["coalesce"]; + this.eachChild(child => { serialized.push(child.serialize()); }); + return serialized; + } } export default Coalesce; diff --git a/src/style-spec/expression/definitions/coercion.js b/src/style-spec/expression/definitions/coercion.js index c4c05be756e..ba50f63aee4 100644 --- a/src/style-spec/expression/definitions/coercion.js +++ b/src/style-spec/expression/definitions/coercion.js @@ -93,6 +93,12 @@ class Coercion implements Expression { possibleOutputs() { return [].concat(...this.args.map((arg) => arg.possibleOutputs())); } + + serialize() { + const serialized = [`to-${this.type.kind}`]; + this.eachChild(child => { serialized.push(child.serialize()); }); + return serialized; + } } export default Coercion; diff --git a/src/style-spec/expression/definitions/equals.js b/src/style-spec/expression/definitions/equals.js index dd08dc92355..93c37e9b239 100644 --- a/src/style-spec/expression/definitions/equals.js +++ b/src/style-spec/expression/definitions/equals.js @@ -6,6 +6,7 @@ import type { Expression } from '../expression'; import type EvaluationContext from '../evaluation_context'; import type ParsingContext from '../parsing_context'; import type { Type } from '../types'; +import type { Value } from '../values'; function isComparableType(type: Type) { return type.kind === 'string' || @@ -28,7 +29,7 @@ function isComparableType(type: Type) { * * @private */ -function makeComparison(compare) { +function makeComparison(op: string, compare: (Value, Value) => boolean) { return class Comparison implements Expression { type: Type; lhs: Expression; @@ -72,8 +73,12 @@ function makeComparison(compare) { possibleOutputs() { return [true, false]; } + + serialize() { + return [op, this.lhs.serialize(), this.rhs.serialize()]; + } }; } -export const Equals = makeComparison((lhs, rhs) => lhs === rhs); -export const NotEquals = makeComparison((lhs, rhs) => lhs !== rhs); +export const Equals = makeComparison('==', (lhs, rhs) => lhs === rhs); +export const NotEquals = makeComparison('!=', (lhs, rhs) => lhs !== rhs); diff --git a/src/style-spec/expression/definitions/interpolate.js b/src/style-spec/expression/definitions/interpolate.js index 779475bdc27..03f1980ba69 100644 --- a/src/style-spec/expression/definitions/interpolate.js +++ b/src/style-spec/expression/definitions/interpolate.js @@ -178,6 +178,31 @@ class Interpolate implements Expression { possibleOutputs() { return [].concat(...this.outputs.map((output) => output.possibleOutputs())); } + + serialize() { + let interpolation; + if (this.interpolation.name === 'linear') { + interpolation = ["linear"]; + } else if (this.interpolation.name === 'exponential') { + if (this.interpolation.base === 1) { + interpolation = ["linear"]; + } else { + interpolation = ["exponential", this.interpolation.base]; + } + } else { + interpolation = ["cubic-bezier" ].concat(this.interpolation.controlPoints); + } + + const serialized = ["interpolate", interpolation, this.input.serialize()]; + + for (let i = 0; i < this.labels.length; i++) { + serialized.push( + this.labels[i], + this.outputs[i].serialize() + ); + } + return serialized; + } } /** diff --git a/src/style-spec/expression/definitions/length.js b/src/style-spec/expression/definitions/length.js index 3361a19e304..52bc4eb75b2 100644 --- a/src/style-spec/expression/definitions/length.js +++ b/src/style-spec/expression/definitions/length.js @@ -50,6 +50,12 @@ class Length implements Expression { possibleOutputs() { return [undefined]; } + + serialize() { + const serialized = ["length"]; + this.eachChild(child => { serialized.push(child.serialize()); }); + return serialized; + } } export default Length; diff --git a/src/style-spec/expression/definitions/let.js b/src/style-spec/expression/definitions/let.js index 769c6ee6348..5eca64bff9a 100644 --- a/src/style-spec/expression/definitions/let.js +++ b/src/style-spec/expression/definitions/let.js @@ -58,6 +58,15 @@ class Let implements Expression { possibleOutputs() { return this.result.possibleOutputs(); } + + serialize() { + const serialized = ["let"]; + for (const [name, expr] of this.bindings) { + serialized.push(name, expr.serialize()); + } + serialized.push(this.result.serialize()); + return serialized; + } } export default Let; diff --git a/src/style-spec/expression/definitions/literal.js b/src/style-spec/expression/definitions/literal.js index fbb8564dcfc..48bdde334fe 100644 --- a/src/style-spec/expression/definitions/literal.js +++ b/src/style-spec/expression/definitions/literal.js @@ -1,6 +1,8 @@ // @flow +import assert from 'assert'; import { isValue, typeOf } from '../values'; +import Color from '../../util/color'; import type { Type } from '../types'; import type { Value } from '../values'; @@ -50,6 +52,20 @@ class Literal implements Expression { possibleOutputs() { return [this.value]; } + + serialize() { + if (this.type.kind === 'array' || this.type.kind === 'object') { + return ["literal", this.value]; + } else if (this.value instanceof Color) { + return ["rgba"].concat(this.value.toArray()); + } else { + assert(this.value === null || + typeof this.value === 'string' || + typeof this.value === 'number' || + typeof this.value === 'boolean'); + return (this.value: any); + } + } } export default Literal; diff --git a/src/style-spec/expression/definitions/match.js b/src/style-spec/expression/definitions/match.js index 1d01dbb0918..d4f015f5328 100644 --- a/src/style-spec/expression/definitions/match.js +++ b/src/style-spec/expression/definitions/match.js @@ -110,6 +110,45 @@ class Match implements Expression { .concat(...this.outputs.map((out) => out.possibleOutputs())) .concat(this.otherwise.possibleOutputs()); } + + serialize() { + const serialized = ["match", this.input.serialize()]; + + // Sort so serialization has an arbitrary defined order, even though + // branch order doesn't affect evaluation + const sortedLabels = Object.keys(this.cases).sort(); + + // Group branches by unique match expression to support condensed + // serializations of the form [case1, case2, ...] -> matchExpression + const groupedByOutput: Array<[number, Array]> = []; + const outputLookup: {[index: number]: number} = {}; // lookup index into groupedByOutput for a given output expression + for (const label of sortedLabels) { + const outputIndex = outputLookup[this.cases[label]]; + if (outputIndex === undefined) { + // First time seeing this output, add it to the end of the grouped list + outputLookup[this.cases[label]] = groupedByOutput.length; + groupedByOutput.push([this.cases[label], [label]]); + } else { + // We've seen this expression before, add the label to that output's group + groupedByOutput[outputIndex][1].push(label); + } + } + + const coerceLabel = (label) => this.input.type.kind === 'number' ? Number(label) : label; + + for (const [outputIndex, labels] of groupedByOutput) { + if (labels.length === 1) { + // Only a single label matches this output expression + serialized.push(coerceLabel(labels[0])); + } else { + // Array of literal labels pointing to this output expression + serialized.push(labels.map(coerceLabel)); + } + serialized.push(this.outputs[outputIndex].serialize()); + } + serialized.push(this.otherwise.serialize()); + return serialized; + } } export default Match; diff --git a/src/style-spec/expression/definitions/step.js b/src/style-spec/expression/definitions/step.js index 9b45b4bb125..0f8e9a45704 100644 --- a/src/style-spec/expression/definitions/step.js +++ b/src/style-spec/expression/definitions/step.js @@ -108,6 +108,17 @@ class Step implements Expression { possibleOutputs() { return [].concat(...this.outputs.map((output) => output.possibleOutputs())); } + + serialize() { + const serialized = ["step", this.input.serialize()]; + for (let i = 0; i < this.labels.length; i++) { + if (i > 0) { + serialized.push(this.labels[i]); + } + serialized.push(this.outputs[i].serialize()); + } + return serialized; + } } export default Step; diff --git a/src/style-spec/expression/definitions/var.js b/src/style-spec/expression/definitions/var.js index e4bb41a3622..43acf599e03 100644 --- a/src/style-spec/expression/definitions/var.js +++ b/src/style-spec/expression/definitions/var.js @@ -37,6 +37,10 @@ class Var implements Expression { possibleOutputs() { return [undefined]; } + + serialize() { + return ["var", this.name]; + } } export default Var; diff --git a/src/style-spec/expression/expression.js b/src/style-spec/expression/expression.js index 8384b02b4c7..b36a7d2daf0 100644 --- a/src/style-spec/expression/expression.js +++ b/src/style-spec/expression/expression.js @@ -5,6 +5,8 @@ import type {Value} from './values'; import type ParsingContext from './parsing_context'; import type EvaluationContext from './evaluation_context'; +type SerializedExpression = Array | string | number | boolean | null; + export interface Expression { +type: Type; @@ -18,6 +20,8 @@ export interface Expression { * complete set of outputs is statically undecidable. */ possibleOutputs(): Array; + + serialize(): SerializedExpression; } export type ExpressionParser = (args: Array, context: ParsingContext) => ?Expression;