ol
Version:
OpenLayers mapping library
651 lines (618 loc) • 18.8 kB
JavaScript
/**
* @module ol/expr/cpu
*/
import {
fromString,
lchaToRgba,
normalize,
rgbaToLcha,
toString,
withAlpha,
} from '../color.js';
import {ColorType, LiteralExpression, Ops, parse} from './expression.js';
/**
* @fileoverview This module includes functions to build expressions for evaluation on the CPU.
* Building is composed of two steps: parsing and compiling. The parsing step takes an encoded
* expression and returns an instance of one of the expression classes. The compiling step takes
* the expression instance and returns a function that can be evaluated in to return a literal
* value. The evaluator function should do as little allocation and work as possible.
*/
/**
* @typedef {Object} EvaluationContext
* @property {Object} properties The values for properties used in 'get' expressions.
* @property {Object} variables The values for variables used in 'var' expressions.
* @property {number} resolution The map resolution.
* @property {string|number|null} featureId The feature id.
* @property {string} geometryType Geometry type of the current object.
*/
/**
* @return {EvaluationContext} A new evaluation context.
*/
export function newEvaluationContext() {
return {
variables: {},
properties: {},
resolution: NaN,
featureId: null,
geometryType: '',
};
}
/**
* @typedef {function(EvaluationContext):import("./expression.js").LiteralValue} ExpressionEvaluator
*/
/**
* @typedef {function(EvaluationContext):boolean} BooleanEvaluator
*/
/**
* @typedef {function(EvaluationContext):number} NumberEvaluator
*/
/**
* @typedef {function(EvaluationContext):string} StringEvaluator
*/
/**
* @typedef {function(EvaluationContext):(Array<number>|string)} ColorLikeEvaluator
*/
/**
* @typedef {function(EvaluationContext):Array<number>} NumberArrayEvaluator
*/
/**
* @typedef {function(EvaluationContext):Array<number>} CoordinateEvaluator
*/
/**
* @typedef {function(EvaluationContext):(Array<number>)} SizeEvaluator
*/
/**
* @typedef {function(EvaluationContext):(Array<number>|number)} SizeLikeEvaluator
*/
/**
* @param {import('./expression.js').EncodedExpression} encoded The encoded expression.
* @param {number} type The expected type.
* @param {import('./expression.js').ParsingContext} context The parsing context.
* @return {ExpressionEvaluator} The expression evaluator.
*/
export function buildExpression(encoded, type, context) {
const expression = parse(encoded, type, context);
return compileExpression(expression, context);
}
/**
* @param {import("./expression.js").Expression} expression The expression.
* @param {import('./expression.js').ParsingContext} context The parsing context.
* @return {ExpressionEvaluator} The evaluator function.
*/
function compileExpression(expression, context) {
if (expression instanceof LiteralExpression) {
// convert colors to array if possible
if (expression.type === ColorType && typeof expression.value === 'string') {
const colorValue = fromString(expression.value);
return function () {
return colorValue;
};
}
return function () {
return expression.value;
};
}
const operator = expression.operator;
switch (operator) {
case Ops.Number:
case Ops.String:
case Ops.Coalesce: {
return compileAssertionExpression(expression, context);
}
case Ops.Get:
case Ops.Var:
case Ops.Has: {
return compileAccessorExpression(expression, context);
}
case Ops.Id: {
return (context) => context.featureId;
}
case Ops.GeometryType: {
return (context) => context.geometryType;
}
case Ops.Concat: {
const args = expression.args.map((e) => compileExpression(e, context));
return (context) =>
''.concat(...args.map((arg) => arg(context).toString()));
}
case Ops.Resolution: {
return (context) => context.resolution;
}
case Ops.Any:
case Ops.All:
case Ops.Between:
case Ops.In:
case Ops.Not: {
return compileLogicalExpression(expression, context);
}
case Ops.Equal:
case Ops.NotEqual:
case Ops.LessThan:
case Ops.LessThanOrEqualTo:
case Ops.GreaterThan:
case Ops.GreaterThanOrEqualTo: {
return compileComparisonExpression(expression, context);
}
case Ops.Multiply:
case Ops.Divide:
case Ops.Add:
case Ops.Subtract:
case Ops.Clamp:
case Ops.Mod:
case Ops.Pow:
case Ops.Abs:
case Ops.Floor:
case Ops.Ceil:
case Ops.Round:
case Ops.Sin:
case Ops.Cos:
case Ops.Atan:
case Ops.Sqrt: {
return compileNumericExpression(expression, context);
}
case Ops.Case: {
return compileCaseExpression(expression, context);
}
case Ops.Match: {
return compileMatchExpression(expression, context);
}
case Ops.Interpolate: {
return compileInterpolateExpression(expression, context);
}
case Ops.ToString: {
return compileConvertExpression(expression, context);
}
default: {
throw new Error(`Unsupported operator ${operator}`);
}
// TODO: unimplemented
// Ops.Zoom
// Ops.Time
// Ops.Array
// Ops.Color
// Ops.Band
// Ops.Palette
}
}
/**
* @param {import('./expression.js').CallExpression} expression The call expression.
* @param {import('./expression.js').ParsingContext} context The parsing context.
* @return {ExpressionEvaluator} The evaluator function.
*/
function compileAssertionExpression(expression, context) {
const type = expression.operator;
const length = expression.args.length;
const args = new Array(length);
for (let i = 0; i < length; ++i) {
args[i] = compileExpression(expression.args[i], context);
}
switch (type) {
case Ops.Coalesce: {
return (context) => {
for (let i = 0; i < length; ++i) {
const value = args[i](context);
if (typeof value !== 'undefined' && value !== null) {
return value;
}
}
throw new Error('Expected one of the values to be non-null');
};
}
case Ops.Number:
case Ops.String: {
return (context) => {
for (let i = 0; i < length; ++i) {
const value = args[i](context);
if (typeof value === type) {
return value;
}
}
throw new Error(`Expected one of the values to be a ${type}`);
};
}
default: {
throw new Error(`Unsupported assertion operator ${type}`);
}
}
}
/**
* @param {import('./expression.js').CallExpression} expression The call expression.
* @param {import('./expression.js').ParsingContext} context The parsing context.
* @return {ExpressionEvaluator} The evaluator function.
*/
function compileAccessorExpression(expression, context) {
const nameExpression = /** @type {LiteralExpression} */ (expression.args[0]);
const name = /** @type {string} */ (nameExpression.value);
switch (expression.operator) {
case Ops.Get: {
return (context) => {
const args = expression.args;
let value = context.properties[name];
for (let i = 1, ii = args.length; i < ii; ++i) {
const keyExpression = /** @type {LiteralExpression} */ (args[i]);
const key = /** @type {string|number} */ (keyExpression.value);
value = value[key];
}
return value;
};
}
case Ops.Var: {
return (context) => context.variables[name];
}
case Ops.Has: {
return (context) => {
const args = expression.args;
if (!(name in context.properties)) {
return false;
}
let value = context.properties[name];
for (let i = 1, ii = args.length; i < ii; ++i) {
const keyExpression = /** @type {LiteralExpression} */ (args[i]);
const key = /** @type {string|number} */ (keyExpression.value);
if (!value || !Object.hasOwn(value, key)) {
return false;
}
value = value[key];
}
return true;
};
}
default: {
throw new Error(`Unsupported accessor operator ${expression.operator}`);
}
}
}
/**
* @param {import('./expression.js').CallExpression} expression The call expression.
* @param {import('./expression.js').ParsingContext} context The parsing context.
* @return {BooleanEvaluator} The evaluator function.
*/
function compileComparisonExpression(expression, context) {
const op = expression.operator;
const left = compileExpression(expression.args[0], context);
const right = compileExpression(expression.args[1], context);
switch (op) {
case Ops.Equal: {
return (context) => left(context) === right(context);
}
case Ops.NotEqual: {
return (context) => left(context) !== right(context);
}
case Ops.LessThan: {
return (context) => left(context) < right(context);
}
case Ops.LessThanOrEqualTo: {
return (context) => left(context) <= right(context);
}
case Ops.GreaterThan: {
return (context) => left(context) > right(context);
}
case Ops.GreaterThanOrEqualTo: {
return (context) => left(context) >= right(context);
}
default: {
throw new Error(`Unsupported comparison operator ${op}`);
}
}
}
/**
* @param {import('./expression.js').CallExpression} expression The call expression.
* @param {import('./expression.js').ParsingContext} context The parsing context.
* @return {BooleanEvaluator} The evaluator function.
*/
function compileLogicalExpression(expression, context) {
const op = expression.operator;
const length = expression.args.length;
const args = new Array(length);
for (let i = 0; i < length; ++i) {
args[i] = compileExpression(expression.args[i], context);
}
switch (op) {
case Ops.Any: {
return (context) => {
for (let i = 0; i < length; ++i) {
if (args[i](context)) {
return true;
}
}
return false;
};
}
case Ops.All: {
return (context) => {
for (let i = 0; i < length; ++i) {
if (!args[i](context)) {
return false;
}
}
return true;
};
}
case Ops.Between: {
return (context) => {
const value = args[0](context);
const min = args[1](context);
const max = args[2](context);
return value >= min && value <= max;
};
}
case Ops.In: {
return (context) => {
const value = args[0](context);
for (let i = 1; i < length; ++i) {
if (value === args[i](context)) {
return true;
}
}
return false;
};
}
case Ops.Not: {
return (context) => !args[0](context);
}
default: {
throw new Error(`Unsupported logical operator ${op}`);
}
}
}
/**
* @param {import('./expression.js').CallExpression} expression The call expression.
* @param {import('./expression.js').ParsingContext} context The parsing context.
* @return {NumberEvaluator} The evaluator function.
*/
function compileNumericExpression(expression, context) {
const op = expression.operator;
const length = expression.args.length;
const args = new Array(length);
for (let i = 0; i < length; ++i) {
args[i] = compileExpression(expression.args[i], context);
}
switch (op) {
case Ops.Multiply: {
return (context) => {
let value = 1;
for (let i = 0; i < length; ++i) {
value *= args[i](context);
}
return value;
};
}
case Ops.Divide: {
return (context) => args[0](context) / args[1](context);
}
case Ops.Add: {
return (context) => {
let value = 0;
for (let i = 0; i < length; ++i) {
value += args[i](context);
}
return value;
};
}
case Ops.Subtract: {
return (context) => args[0](context) - args[1](context);
}
case Ops.Clamp: {
return (context) => {
const value = args[0](context);
const min = args[1](context);
if (value < min) {
return min;
}
const max = args[2](context);
if (value > max) {
return max;
}
return value;
};
}
case Ops.Mod: {
return (context) => args[0](context) % args[1](context);
}
case Ops.Pow: {
return (context) => Math.pow(args[0](context), args[1](context));
}
case Ops.Abs: {
return (context) => Math.abs(args[0](context));
}
case Ops.Floor: {
return (context) => Math.floor(args[0](context));
}
case Ops.Ceil: {
return (context) => Math.ceil(args[0](context));
}
case Ops.Round: {
return (context) => Math.round(args[0](context));
}
case Ops.Sin: {
return (context) => Math.sin(args[0](context));
}
case Ops.Cos: {
return (context) => Math.cos(args[0](context));
}
case Ops.Atan: {
if (length === 2) {
return (context) => Math.atan2(args[0](context), args[1](context));
}
return (context) => Math.atan(args[0](context));
}
case Ops.Sqrt: {
return (context) => Math.sqrt(args[0](context));
}
default: {
throw new Error(`Unsupported numeric operator ${op}`);
}
}
}
/**
* @param {import('./expression.js').CallExpression} expression The call expression.
* @param {import('./expression.js').ParsingContext} context The parsing context.
* @return {ExpressionEvaluator} The evaluator function.
*/
function compileCaseExpression(expression, context) {
const length = expression.args.length;
const args = new Array(length);
for (let i = 0; i < length; ++i) {
args[i] = compileExpression(expression.args[i], context);
}
return (context) => {
for (let i = 0; i < length - 1; i += 2) {
const condition = args[i](context);
if (condition) {
return args[i + 1](context);
}
}
return args[length - 1](context);
};
}
/**
* @param {import('./expression.js').CallExpression} expression The call expression.
* @param {import('./expression.js').ParsingContext} context The parsing context.
* @return {ExpressionEvaluator} The evaluator function.
*/
function compileMatchExpression(expression, context) {
const length = expression.args.length;
const args = new Array(length);
for (let i = 0; i < length; ++i) {
args[i] = compileExpression(expression.args[i], context);
}
return (context) => {
const value = args[0](context);
for (let i = 1; i < length; i += 2) {
if (value === args[i](context)) {
return args[i + 1](context);
}
}
return args[length - 1](context);
};
}
/**
* @param {import('./expression.js').CallExpression} expression The call expression.
* @param {import('./expression.js').ParsingContext} context The parsing context.
* @return {ExpressionEvaluator} The evaluator function.
*/
function compileInterpolateExpression(expression, context) {
const length = expression.args.length;
const args = new Array(length);
for (let i = 0; i < length; ++i) {
args[i] = compileExpression(expression.args[i], context);
}
return (context) => {
const base = args[0](context);
const value = args[1](context);
let previousInput;
let previousOutput;
for (let i = 2; i < length; i += 2) {
const input = args[i](context);
let output = args[i + 1](context);
const isColor = Array.isArray(output);
if (isColor) {
output = withAlpha(output);
}
if (input >= value) {
if (i === 2) {
return output;
}
if (isColor) {
return interpolateColor(
base,
value,
previousInput,
previousOutput,
input,
output,
);
}
return interpolateNumber(
base,
value,
previousInput,
previousOutput,
input,
output,
);
}
previousInput = input;
previousOutput = output;
}
return previousOutput;
};
}
/**
* @param {import('./expression.js').CallExpression} expression The call expression.
* @param {import('./expression.js').ParsingContext} context The parsing context.
* @return {ExpressionEvaluator} The evaluator function.
*/
function compileConvertExpression(expression, context) {
const op = expression.operator;
const length = expression.args.length;
const args = new Array(length);
for (let i = 0; i < length; ++i) {
args[i] = compileExpression(expression.args[i], context);
}
switch (op) {
case Ops.ToString: {
return (context) => {
const value = args[0](context);
if (expression.args[0].type === ColorType) {
return toString(value);
}
return value.toString();
};
}
default: {
throw new Error(`Unsupported convert operator ${op}`);
}
}
}
/**
* @param {number} base The base.
* @param {number} value The value.
* @param {number} input1 The first input value.
* @param {number} output1 The first output value.
* @param {number} input2 The second input value.
* @param {number} output2 The second output value.
* @return {number} The interpolated value.
*/
function interpolateNumber(base, value, input1, output1, input2, output2) {
const delta = input2 - input1;
if (delta === 0) {
return output1;
}
const along = value - input1;
const factor =
base === 1
? along / delta
: (Math.pow(base, along) - 1) / (Math.pow(base, delta) - 1);
return output1 + factor * (output2 - output1);
}
/**
* @param {number} base The base.
* @param {number} value The value.
* @param {number} input1 The first input value.
* @param {import('../color.js').Color} rgba1 The first output value.
* @param {number} input2 The second input value.
* @param {import('../color.js').Color} rgba2 The second output value.
* @return {import('../color.js').Color} The interpolated color.
*/
function interpolateColor(base, value, input1, rgba1, input2, rgba2) {
const delta = input2 - input1;
if (delta === 0) {
return rgba1;
}
const lcha1 = rgbaToLcha(rgba1);
const lcha2 = rgbaToLcha(rgba2);
let deltaHue = lcha2[2] - lcha1[2];
if (deltaHue > 180) {
deltaHue -= 360;
} else if (deltaHue < -180) {
deltaHue += 360;
}
const lcha = [
interpolateNumber(base, value, input1, lcha1[0], input2, lcha2[0]),
interpolateNumber(base, value, input1, lcha1[1], input2, lcha2[1]),
lcha1[2] + interpolateNumber(base, value, input1, 0, input2, deltaHue),
interpolateNumber(base, value, input1, rgba1[3], input2, rgba2[3]),
];
return normalize(lchaToRgba(lcha));
}