diff --git a/package.json b/package.json index b81ddc948..6334e773f 100644 --- a/package.json +++ b/package.json @@ -62,4 +62,4 @@ "lint-staged": { "*.{js,ts}": "eslint --cache --fix" } -} \ No newline at end of file +} diff --git a/packages/common/src/Color.ts b/packages/common/src/Color.ts new file mode 100644 index 000000000..c4638210c --- /dev/null +++ b/packages/common/src/Color.ts @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2019-2022 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +export namespace Colors { + + export type RGBA = [number, number, number, number]; + + const HTML_COLOR_NAMES: { [color: string]: RGBA | string } = { + aliceblue: 'f0f8ff', + antiquewhite: 'faebd7', + aqua: '00ffff', + aquamarine: '7fffd4', + azure: 'f0ffff', + beige: 'f5f5dc', + bisque: 'ffe4c4', + black: '000000', + blanchedalmond: 'ffebcd', + blue: '0000ff', + blueviolet: '8a2be2', + brown: 'a52a2a', + burlywood: 'deb887', + cadetblue: '5f9ea0', + chartreuse: '7fff00', + chocolate: 'd2691e', + coral: 'ff7f50', + cornflowerblue: '6495ed', + cornsilk: 'fff8dc', + crimson: 'dc143c', + cyan: '00ffff', + darkblue: '00008b', + darkcyan: '008b8b', + darkgoldenrod: 'b8860b', + darkgray: 'a9a9a9', + darkgrey: 'a9a9a9', + darkgreen: '006400', + darkkhaki: 'bdb76b', + darkmagenta: '8b008b', + darkolivegreen: '556b2f', + darkorange: 'ff8c00', + darkorchid: '9932cc', + darkred: '8b0000', + darksalmon: 'e9967a', + darkseagreen: '8fbc8f', + darkslateblue: '483d8b', + darkslategray: '2f4f4f', + darkslategrey: '2f4f4f', + darkturquoise: '00ced1', + darkviolet: '9400d3', + deeppink: 'ff1493', + deepskyblue: '00bfff', + dimgray: '696969', + dimgrey: '696969', + dodgerblue: '1e90ff', + firebrick: 'b22222', + floralwhite: 'fffaf0', + forestgreen: '228b22', + fuchsia: 'ff00ff', + gainsboro: 'dcdcdc', + ghostwhite: 'f8f8ff', + gold: 'ffd700', + goldenrod: 'daa520', + gray: '808080', + grey: '808080', + green: '008000', + greenyellow: 'adff2f', + honeydew: 'f0fff0', + hotpink: 'ff69b4', + indianred: 'cd5c5c', + indigo: '4b0082', + ivory: 'fffff0', + khaki: 'f0e68c', + lavender: 'e6e6fa', + lavenderblush: 'fff0f5', + lawngreen: '7cfc00', + lemonchiffon: 'fffacd', + lightblue: 'add8e6', + lightcoral: 'f08080', + lightcyan: 'e0ffff', + lightgoldenrodyellow: 'fafad2', + lightgray: 'd3d3d3', + lightgrey: 'd3d3d3', + lightgreen: '90ee90', + lightpink: 'ffb6c1', + lightsalmon: 'ffa07a', + lightseagreen: '20b2aa', + lightskyblue: '87cefa', + lightslategray: '778899', + lightslategrey: '778899', + lightsteelblue: 'b0c4de', + lightyellow: 'ffffe0', + lime: '00ff00', + limegreen: '32cd32', + linen: 'faf0e6', + magenta: 'ff00ff', + maroon: '800000', + mediumaquamarine: '66cdaa', + mediumblue: '0000cd', + mediumorchid: 'ba55d3', + mediumpurple: '9370d8', + mediumseagreen: '3cb371', + mediumslateblue: '7b68ee', + mediumspringgreen: '00fa9a', + mediumturquoise: '48d1cc', + mediumvioletred: 'c71585', + midnightblue: '191970', + mintcream: 'f5fffa', + mistyrose: 'ffe4e1', + moccasin: 'ffe4b5', + navajowhite: 'ffdead', + navy: '000080', + oldlace: 'fdf5e6', + olive: '808000', + olivedrab: '6b8e23', + orange: 'ffa500', + orangered: 'ff4500', + orchid: 'da70d6', + palegoldenrod: 'eee8aa', + palegreen: '98fb98', + paleturquoise: 'afeeee', + palevioletred: 'd87093', + papayawhip: 'ffefd5', + peachpuff: 'ffdab9', + peru: 'cd853f', + pink: 'ffc0cb', + plum: 'dda0dd', + powderblue: 'b0e0e6', + purple: '800080', + red: 'ff0000', + rosybrown: 'bc8f8f', + royalblue: '4169e1', + saddlebrown: '8b4513', + salmon: 'fa8072', + sandybrown: 'f4a460', + seagreen: '2e8b57', + seashell: 'fff5ee', + sienna: 'a0522d', + silver: 'c0c0c0', + skyblue: '87ceeb', + slateblue: '6a5acd', + slategray: '708090', + slategrey: '708090', + snow: 'fffafa', + springgreen: '00ff7f', + steelblue: '4682b4', + tan: 'd2b48c', + teal: '008080', + thistle: 'd8bfd8', + tomato: 'ff6347', + turquoise: '40e0d0', + violet: 'ee82ee', + wheat: 'f5deb3', + white: 'ffffff', + whitesmoke: 'f5f5f5', + yellow: 'ffff00', + yellowgreen: '9acd32' + }; + + const hexStringToRGBA = (hexString: string): RGBA => { + const length = hexString.length; + if (length < 5) { + return [ + parseInt(hexString.charAt(0), 16) / 15, + parseInt(hexString.charAt(1), 16) / 15, + parseInt(hexString.charAt(2), 16) / 15, + length == 4 + ? parseInt(hexString.charAt(3), 16) / 15 + : 1 + ]; + } else { + return hexToRGBA(parseInt(hexString, 16), length == 8); + } + }; + + const hexToRGBA = (hex: number, alpha?: boolean): RGBA => { + return alpha ? [ + (hex >> 24 & 255) / 255, + (hex >> 16 & 255) / 255, + (hex >> 8 & 255) / 255, + (hex & 255) / 255 + ] : [ + (hex >> 16 & 255) / 255, + (hex >> 8 & 255) / 255, + (hex & 255) / 255, + 1 + ]; + }; + + for (let name in HTML_COLOR_NAMES) { + HTML_COLOR_NAMES[name] = hexStringToRGBA(HTML_COLOR_NAMES[name] as string); + } + + const parseRGBAString = (color: string): RGBA => { + const rgb = color.match(/^rgba?\s*\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d*(?:\.\d+)?))?\)$/); + + return rgb?.length > 3 && [ + rgb[1] / 255, + rgb[2] / 255, + rgb[3] / 255, + rgb[4] == undefined ? 1 : Number(rgb[4]) + ]; + }; + + export type Color = string | RGBA | number; + + export const toRGB = (color: Color, ignoreNumbers?: boolean): RGBA => { + let rgba; + if (color) { + if (Array.isArray(color)) { + rgba = color; + if (rgba.length == 3) { + rgba[3] = 1; + } + } else if (typeof color == 'number') { + if (!ignoreNumbers) { + rgba = hexToRGBA(color); + } + } else if (color[0] == '#') { + rgba = hexStringToRGBA(color.slice(1)); + } else { + if (/^([A-Fa-f\d]+)$/.test(color)) { + rgba = hexStringToRGBA(color); + } else if (color.startsWith('rgb')) { + rgba = parseRGBAString(color); + } else { + rgba = HTML_COLOR_NAMES[color]; + rgba = (rgba as RGBA) && [rgba[0], rgba[1], rgba[2], rgba[3]]; + } + } + } + return rgba || null; + }; + +} diff --git a/packages/common/src/Expressions/ArrayExpressions.ts b/packages/common/src/Expressions/ArrayExpressions.ts new file mode 100644 index 000000000..8253c730b --- /dev/null +++ b/packages/common/src/Expressions/ArrayExpressions.ts @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2019-2024 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import {Expression} from './Expression'; +export class SliceExpression extends Expression { + static operator = 'slice'; + + eval(context) { + const array = this.operand(1, context); + const start = this.operand(2, context); + const end = this.operand(3, context); + + return array.slice(start, end); + } +} + +export class AtArrayExpression extends Expression { + static operator = 'at'; + + eval(context) { + const index = this.operand(1, context); + const array = this.operand(2, context); + return array[index]; + } +} + +export class LengthExpression extends Expression { + static operator = 'length'; + + eval(context) { + const stringOrArray = this.operand(1, context); + return stringOrArray.length; + } +} diff --git a/packages/common/src/Expressions/ConditionalExpressions.ts b/packages/common/src/Expressions/ConditionalExpressions.ts new file mode 100644 index 000000000..e952adee5 --- /dev/null +++ b/packages/common/src/Expressions/ConditionalExpressions.ts @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2019-2024 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import {Expression, ExpressionMode} from './Expression'; + +class CaseExpressionError extends Error { + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, CaseExpressionError.prototype); + this.name = this.constructor.name; + } +} +export class CaseExpression extends Expression { + static operator = 'case'; + private runtime: number; + + constructor(json, env) { + if (json.length < 4) { + throw new CaseExpressionError('invalid arguments'); + } + if (json.length % 2) { + throw new CaseExpressionError('missing fallback'); + } + super(json, env); + } + + dynamic(): boolean { + this.supportsPartialEval = true; + for (let i = 1, {json} = this, len = json.length; i < len; i++) { + let exp = this.compileOperand(i); + if (Expression.isDynamicExpression(exp)) { + this.runtime = i; + const isCondition = Boolean(i%2); + this.supportsPartialEval = !isCondition; + return true; + } + } + this.runtime = Infinity; + return false; + } + + isOperandDynamic(index) { + let expr = this.compileOperand(index); + return Expression.isDynamicExpression(expr); + } + + + eval(context) { + const {json} = this; + let len = json.length - 1; + const requiresRuntimeExecution = this.env.getMode() == ExpressionMode.dynamic; + const initialRuntimeConditionIndex = requiresRuntimeExecution ? this.runtime : Infinity; + + for (let i = 1; i < len; i += 2) { + if (initialRuntimeConditionIndex===i) { + return this; + } + let condition = this.operand(i, context); + if (condition) { + return this.operand(i + 1, context); + } + } + // result or fallback + return this.operand(len, context); + } +} + +export class StepExpression extends Expression { + static operator = 'step'; + + dynamic(): boolean { + return Expression.isDynamicExpression(this.compileOperand(1)) || + Expression.isDynamicExpression(this.compileOperand(2)) || + Expression.isDynamicExpression(this.compileOperand(3)); + } + + eval(context) { + let input = this.operand(1, context); + let defaultValue = this.operand(2, context); + let step = this.operand(3, context); + + if (input < step) return defaultValue; + + const {json} = this; + for (let i = json.length - 2; i > 2; i -= 2) { + if (input >= json[i]) { + return json[i + 1]; + } + } + } +} + +export class MatchExpression extends Expression { + static operator = 'match'; + + dynamic(): boolean { + for (let i = 1, len = this.json.length - 2; i < len; i += 2) { + if (Expression.isDynamicExpression(this.compileOperand(i))) { + return true; + } + } + if (Expression.isDynamicExpression(this.compileOperand(this.json.length - 1))) { + return true; + } + return false; + } + + eval(context) { + const {json} = this; + const value = this.operand(1, context); + const len = json.length - 2; + + for (let i = 2; i < len; i += 2) { + let labels = json[i]; + if (!Array.isArray(labels)) labels = [labels]; + for (let label of labels) { + if (label == value) { + return this.operand(i + 1, context); + } + } + } + return this.operand(json.length - 1, context); + } +} diff --git a/packages/common/src/Expressions/Expression.ts b/packages/common/src/Expressions/Expression.ts new file mode 100644 index 000000000..324ae53b1 --- /dev/null +++ b/packages/common/src/Expressions/Expression.ts @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2019-2024 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ +import {ExpressionParser} from './ExpressionParser'; + +export enum ExpressionMode { + static, + dynamic +}; + +export interface IExpression { + json: any[]; + eval(context: any); +} + +export type JSONExpression = [string, ...any[]]; + + +let expId =0; +export abstract class Expression implements IExpression { + static operator: string; + id?: number; + static isExpression(exp) { + return exp instanceof Expression; + } + + static isDynamicExpression(exp: Expression) { + // return false; + // return this.isExpression(exp) && false; + return this.isExpression(exp) && exp.dynamic(); + } + + protected env: ExpressionParser; + + json: JSONExpression; + + supportsPartialEval: boolean = false; + constructor(json: JSONExpression, env: ExpressionParser) { + // this.id = expId++; + this.json = json; + this.env = env; + } + + // compute(context); + abstract eval(context); + + dynamic(): boolean { + for (let i = 1, {json} = this, len = json.length; i < len; i++) { + let exp = this.compileOperand(i); + if (Expression.isDynamicExpression(exp)) { + return true; + } + } + return false; + } + + protected compileOperand(index: number) { + return this.json[index] = this.env.parseJSON(this.json[index]); + } + + operand(index: number, context?) { + return this.env.evaluateParsed(this.compileOperand(index), context); + } + + toJSON() { + return this.json.map((v)=>Expression.isExpression(v) ? v.toJSON(): v); + } + + resolve(context=this.env.context, mode?: ExpressionMode) { + const {env} = this; + env.setMode(mode); + const result = env.evaluateParsed(this, context); + env.setMode(ExpressionMode.static); + return result; + } +} diff --git a/packages/common/src/Expressions/ExpressionParser.ts b/packages/common/src/Expressions/ExpressionParser.ts new file mode 100644 index 000000000..cc338b8f8 --- /dev/null +++ b/packages/common/src/Expressions/ExpressionParser.ts @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2019-2024 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import {Expression, ExpressionMode, IExpression, JSONExpression} from './Expression'; +import * as Expressions from './Expressions'; +import * as InterpolateExpressions from './InterpolateExpression'; +import {JSUtils} from '@here/xyz-maps-common'; + +type ResultCache = Map & { hits?: number }; + + +class DynamicExpressionInterrupt extends Error { + exp: Expression; + + constructor() { + super('DynamicExpressionInterrupt'); + Object.setPrototypeOf(this, DynamicExpressionInterrupt.prototype); + this.name = this.constructor.name; + } +} + +export class ExpressionParser { + static DYNAMIC_EXPRESSION_INTERRUPT: DynamicExpressionInterrupt = new DynamicExpressionInterrupt(); + static Mode = ExpressionMode; + static Expressions: { + [op: string]: new (e: JSONExpression, p: ExpressionParser) => Expression & { [K in keyof typeof Expression]: typeof Expression[K] } + }; + + private definitions: {}; + private cache = new Map(); + context: { [name: string]: any }; + private _cacheHits: number = 0; + private defaultResultCache: ResultCache = new Map(); + private resultCache: ResultCache; + private _mode: ExpressionMode = ExpressionMode.static; + private dynamicResultCache: ResultCache = new Map(); + + static { + let expressions = {}; + for (let Exp of ([...Object.values(Expressions), ...Object.values(InterpolateExpressions)] as (typeof Expression)[])) { + expressions[Exp.operator] = Exp; + } + this.Expressions = expressions; + } + + constructor(definitions = {}, context = {}) { + this.definitions = definitions; + this.context = context; + // console.time('clone definitions'); + // this.definitions = JSUtils.clone(definitions); + // console.timeEnd('clone definitions'); + + // this.cache.get = this.cache.set =()=>undefined; + // this.defaultResultCache.get = this.defaultResultCache.set =()=>undefined; + // this.dynamicResultCache.get = this.defaultResultCache.set =()=>undefined; + } + + init(def, mapContext) { + this.clearCache(); + this.setDefinitions(def); + this.context = mapContext; + } + + setDefinitions(def) { + this.definitions = def; + } + + clearCache() { + this._cacheHits = 0; + this.cache.clear(); + } + + evaluate(exp, context) { + exp = this.parseJSON(exp); + let result; + try { + result = this.evaluateParsed(exp, context); + } catch (e) { + if (e.message === 'DynamicExpressionInterrupt') { + return e.exp; + } else { + throw e; + } + } + return result; + } + + evaluateParsed(exp: Expression, context) { + if (exp instanceof Expression) { + if (this.getMode() == ExpressionParser.Mode.dynamic && exp.dynamic() && !exp.supportsPartialEval) { + const DYNAMIC_EXPRESSION_INTERRUPT = ExpressionParser.DYNAMIC_EXPRESSION_INTERRUPT; + DYNAMIC_EXPRESSION_INTERRUPT.exp = exp; + throw DYNAMIC_EXPRESSION_INTERRUPT; + } + // return exp.eval(context); + let result = this.resultCache.get(exp); + if (result !== undefined) { + this.resultCache.hits = (this.resultCache.hits || 0) + 1; + return result; + } + result = exp.eval(context); + + this.resultCache.set(exp, result); + return result; + } + return exp; + } + + + resolveReference(exp: JSONExpression, definitions = this.definitions) { + let key = exp?.[1]; + let value = definitions[key]; + if (value != null) { + if (value.value != null) { + value = value.value; + } + while (ExpressionParser.isJSONExp(value) && value[0] == 'ref') { + value = definitions[value[1]]; + if (value.value !== undefined) { + value = value.value; + } + } + } + return value; + } + + parseJSON(expression: Expression | JSONExpression, throwUnsupportedExpError?: boolean) { + const isJSONExp = ExpressionParser.isJSONExp(expression); + if (!isJSONExp) return expression; + + let operator = expression[0]; + const isReferenceExp = operator == 'ref'; + let cacheKey = isReferenceExp ? expression[1] : expression; + let exp = this.cache.get(cacheKey); + if (exp != undefined) { + this._cacheHits++; + return exp; + } + + if (isReferenceExp) { + exp = this.resolveReference(expression as JSONExpression); + if (!ExpressionParser.isJSONExp(exp)) { + this.cache.set(cacheKey, exp); + return exp; + } + expression = exp; + [operator] = exp; + } + + const Expression = ExpressionParser.Expressions[operator]; + + if (Expression) { + exp = new Expression(expression as JSONExpression, this); + } else if (throwUnsupportedExpError !== false) { + throw new Error(`Expression ${operator} unsupported`); + } else { + return; + } + + this.cache.set(cacheKey, exp); + return exp; + } + + createExpression(jsonExp: JSONExpression) { + const Expression = ExpressionParser.Expressions[jsonExp[0]]; + return Expression && new Expression(jsonExp, this); + } + + static isJSONExp(exp) { + return Array.isArray(exp) && typeof exp[0] == 'string'; + } + + clearResultCache() { + this.defaultResultCache.clear(); + this.dynamicResultCache.clear(); + } + + isSupported(exp: JSONExpression) { + return Boolean(ExpressionParser.Expressions[exp[0]]); + } + + setMode(mode: ExpressionMode) { + // if (mode != this._mode) { + this._mode = mode; + this.context.mode = mode; + this.resultCache = mode === ExpressionMode.static ? this.defaultResultCache : this.dynamicResultCache; + // } + } + + getMode() { + return this._mode; + } +} diff --git a/packages/common/src/Expressions/Expressions.ts b/packages/common/src/Expressions/Expressions.ts new file mode 100644 index 000000000..8ff703781 --- /dev/null +++ b/packages/common/src/Expressions/Expressions.ts @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2019-2024 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ +import {Expression, JSONExpression} from './Expression'; +import {ExpressionParser} from '@here/xyz-maps-common'; + +export * from './MathExpressions'; +export * from './LogicalExpressions'; +export * from './StringExpressions'; +export * from './ArrayExpressions'; +export * from './ConditionalExpressions'; +export * from './LookupExpression'; +export * from './TypeExpressions'; + +export class ReferenceExpression extends Expression { + static operator = 'ref'; + private refExp: Expression | any; + + eval(context) { + const refExp = this.refExp ||= this.env.resolveReference(this.json); + const result = this.env.evaluate(refExp, context); + return result; + } +} + + +class GetExpressionError extends Error { + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, GetExpressionError.prototype); + this.name = this.constructor.name; + } +} + +export class GetExpression extends Expression { + static operator = 'get'; + + constructor(json: JSONExpression, expressions: ExpressionParser) { + if (typeof json[1] != 'string') { + throw new GetExpressionError('Name parameter must be of type string'); + } + super(json, expressions); + } + + dynamic(): boolean { + return false; + } + + eval(context) { + let ctx = this.json[2]; + if (ctx) { + context = this.operand(2, context); + } + // const name = this.operand(1, context); + const name = this.json[1]; + let value = context?.[name]; + if (value === undefined) { + value = this.env.context[name]; + } + return value ?? null; + } +} diff --git a/packages/common/src/Expressions/InterpolateExpression.ts b/packages/common/src/Expressions/InterpolateExpression.ts new file mode 100644 index 000000000..1ddaf9624 --- /dev/null +++ b/packages/common/src/Expressions/InterpolateExpression.ts @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2019-2024 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import {Expression} from './Expression'; +import {Colors} from '../Color'; +import toRGB = Colors.toRGB; + +export class ZoomExpression extends Expression { + static operator = 'zoom'; + + dynamic() { + return true; + } + + eval(context) { + return this.env.context.zoom; + // return this.env.context.$zoom; + } +} + +const lerp = (x: number, y: number, a: number) => x + (y - x) * a; + +export class InterpolateExpression extends Expression { + static operator = 'interpolate'; + + static supported = {'linear': lerp, 'discrete': (x) => x, 'exponential': lerp}; + + dynamic(): boolean { + for (let i = 2, len = this.json.length - 1; i < len; i += 2) { + if (Expression.isDynamicExpression(this.compileOperand(i))) { + return true; + } + } + return false; + } + + eval(context) { + const {json} = this; + const type = json[1]?.[0]; + const interpolate = InterpolateExpression.supported[type]; + if (!interpolate) { + console.warn('unsupported interpolation expression:', type); + return; + } + + const value = this.operand(2, context); + let int0; + let int1; + let value0; + let value1; + let i = 3; + let len = json.length; + + for (; i < len; i += 2) { + value1 = this.operand(i, context); + if (value1 > value) { + int1 = this.operand(i + 1, context); + break; + } + value0 = value1; + } + + if (i == 3) { + // first step is already greater. we can simply return the first value. + return int1; + } else if (i == len) { + // last step is still smaller. we can simply return the last value. + return this.operand(len - 1, context); + } + let t; + + int0 = this.operand(i - 1, context); + + if (type == 'linear') { + t = (value - value0) / (value1 - value0); + } else if (type == 'exponential') { + const base = json[1][1]; + t = base == 1 + ? (value - value0) / (value1 - value0) + : (Math.pow(base, value - value0) - 1) / (Math.pow(base, value1 - value0) - 1); + } + let color = toRGB(int0, true); + if (color != null) { // is color? + if (typeof t == 'number') { + int1 = toRGB(int1, true); + return [ + interpolate(color[0], int1[0], t), + interpolate(color[1], int1[1], t), + interpolate(color[2], int1[2], t), + interpolate(color[3], int1[3], t) + ]; + } + return color; + } + return interpolate(int0, int1, t); + } +} + diff --git a/packages/common/src/Expressions/LogicalExpressions.ts b/packages/common/src/Expressions/LogicalExpressions.ts new file mode 100644 index 000000000..0d8fe2f92 --- /dev/null +++ b/packages/common/src/Expressions/LogicalExpressions.ts @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2019-2024 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import {Expression} from './Expression'; + +export class AllExpression extends Expression { + static operator = 'all'; + + eval(context) { + const {json} = this; + for (let i = 1, {length} = json; i < length; i++) { + if (!Boolean(this.operand(i, context))) return false; + } + return true; + } +} + +export class AnyExpression extends Expression { + static operator = 'any'; + + eval(context) { + const {json} = this; + for (let i = 1, {length} = json; i < length; i++) { + if (Boolean(this.operand(i, context))) return true; + } + return false; + } +} + +export class isFalseExpression extends Expression { + static operator = '!'; + eval(context) { + let val = this.operand(1, context); + return !val; + } +} + +export class HasNotExpression extends Expression { + static operator = '!has'; + + eval(context) { + const {json} = this; + const property = json[1]; + let object = this.operand(2, context); + return !(object ?? context)?.hasOwnProperty(property); + } +} + +export class NotInExpression extends HasNotExpression { + static operator = '!has'; +} + +export class HasExpression extends Expression { + static operator = 'has'; + + dynamic(): boolean { + return false; + } + + eval(context) { + const {json} = this; + const property = json[1]; + let object = this.operand(2, context); + return (object ?? context)?.hasOwnProperty(property) || false; + } +} + + +export class NoneExpression extends Expression { + static operator = 'none'; + + eval(context) { + const {json} = this; + for (let i = 1, {length} = json; i < length; i++) { + let val = this.operand(1, context); + if (val) return false; + } + return true; + } +} + + +class CompareExpression extends Expression { + dynamic(): boolean { + const a = this.compileOperand(1); + if (Expression.isDynamicExpression(a)) { + return true; + } + const b = this.compileOperand(2); + if (Expression.isDynamicExpression(b)) { + return true; + } + return false; + } + + eval(context) { + } +} + +export class EqualsExpression extends CompareExpression { + static operator = '=='; + + eval(context) { + const a = this.operand(1, context); + const b = this.operand(2, context); + return a == b; + } +} + +export class NotEqualsExpression extends CompareExpression { + static operator = '!='; + + eval(context) { + const a = this.operand(1, context); + const b = this.operand(2, context); + return a != b; + } +} + +export class GreaterExpression extends CompareExpression { + static operator = '>'; + + eval(context) { + const a = this.operand(1, context); + const b = this.operand(2, context); + return a > b; + } +} + +export class GreaterOrEqualExpression extends CompareExpression { + static operator = '>='; + + eval(context) { + const a = this.operand(1, context); + const b = this.operand(2, context); + return a >= b; + } +} + + +export class SmallerOrEqualExpression extends CompareExpression { + static operator = '<='; + + eval(context) { + const a = this.operand(1, context); + const b = this.operand(2, context); + return a <= b; + } +} + +export class SmallerExpression extends CompareExpression { + static operator = '<'; + + eval(context) { + const a = this.operand(1, context); + const b = this.operand(2, context); + return a < b; + } +} diff --git a/packages/common/src/Expressions/LookupExpression.ts b/packages/common/src/Expressions/LookupExpression.ts new file mode 100644 index 000000000..5aea42b70 --- /dev/null +++ b/packages/common/src/Expressions/LookupExpression.ts @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2019-2024 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import {Expression} from './Expression'; + +export class LiteralExpression extends Expression { + static operator = 'literal'; + + dynamic(): boolean { + return false; + } + + eval(context) { + return this.json[1]; + } +} + +class LookupExpressionError extends Error { + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, LookupExpressionError.prototype); + this.name = this.constructor.name; + } +} +export class LookupExpression extends Expression { + static operator = 'lookup'; + + static _lookupSearchKeyCache = new Map(); + static _lookupTableCache = new Map(); + + + private getLookupTable(table: any) { + let map = LookupExpression._lookupTableCache.get(table); + + if (map && !Array.isArray(map)) { + return map; + } + + map = new Map(); + + for (const item of table) { + if (!(item?.keys && item?.attributes)) throw new LookupExpressionError('invalid lookup table'); + const keys = Object.getOwnPropertyNames(item.keys).sort(); + for (let i = 0, length = keys.length; i < length; i++) { + const key = keys[i]; + keys[i] = `${key}=${item.keys[key]}`; + } + map.set(keys.join(';'), item.attributes); + } + + LookupExpression._lookupTableCache.set(table, map); + return map; + } + + + private getCombinations(arr: T[]): T[][] { + const n = arr.length; + const combinations: T[][] = []; + for (let i = 0; i < 1 << n; i++) { + const currentCombination: T[] = []; + for (let j = 0; j < n; j++) { + if (i & (1 << j)) { + currentCombination.push(arr[j]); + } + } + combinations.push(currentCombination); + } + return combinations; + } + + private getLookupMapSearchKeys(exp) { + let searchKeys: string[] = []; + + for (let i = 0, len = exp.length; i < len; i += 2) { + searchKeys.push(exp[i] + '=' + exp[i + 1]); + } + const id = searchKeys.join(';'); + + let combinations = LookupExpression._lookupSearchKeyCache.get(id); + + if (!combinations) { + searchKeys.sort(); + combinations = this.getCombinations(searchKeys).sort((a, b) => b.length - a.length); + for (let i = 0, len = combinations.length; i < len; i++) { + combinations[i] = combinations[i].join(';'); + } + LookupExpression._lookupSearchKeyCache.set(id, combinations); + } + + return combinations; + } + + eval(context) { + const {json} = this; + let table = this.operand(1, context); + const keyValues: string[] = []; + for (let i = 2; i < json.length; i++) { + keyValues.push(this.operand(i, context)); + } + + let map = this.getLookupTable(table); + let keys = this.getLookupMapSearchKeys(keyValues); + for (let key of keys) { + let val = map.get(key); + if (val) { + return val; + } + } + return null; + } +} diff --git a/packages/common/src/Expressions/MathExpressions.ts b/packages/common/src/Expressions/MathExpressions.ts new file mode 100644 index 000000000..95bf9d036 --- /dev/null +++ b/packages/common/src/Expressions/MathExpressions.ts @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2019-2024 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import {Expression} from './Expression'; + +class SimpleOperatorExpression extends Expression { + dynamic(): boolean { + return Expression.isDynamicExpression(this.compileOperand(1)) || + Expression.isDynamicExpression(this.compileOperand(2)); + } + + eval(context) { + } +} + +export class SumExpression extends SimpleOperatorExpression { + static operator = '+'; + eval(context) { + const {json} = this; + // this.env._expressionRequiresLiveMode ||= this; + let sum = 0; + // max 17.5ms + for (let i = 1, {length} = json; i < length; i++) { + sum += Number(this.operand(i, context)) || 0; + } + // this.env._expressionRequiresLiveMode = null; + return sum; + } +} + +export class SubtractExpression extends SimpleOperatorExpression { + static operator = '-'; + + eval(context) { + let a = this.operand(1, context); + let b = this.operand(2, context); + return Number(a) - Number(b); + } +} + +export class MultiplyExpression extends SimpleOperatorExpression { + static operator = '*'; + + eval(context) { + let a = this.operand(1, context); + let b = this.operand(2, context); + return Number(a) * Number(b); + } +} + +export class DevideExpression extends SimpleOperatorExpression { + static operator = '/'; + + eval(context) { + let a = this.operand(1, context); + let b = this.operand(2, context); + return Number(a) / Number(b); + } +} + +export class ModulusExpression extends SimpleOperatorExpression { + static operator = '%'; + + eval(context) { + let a = this.operand(1, context); + let b = this.operand(2, context); + return Number(a) % Number(b); + } +} + + +class FloorExpressionError extends Error { + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, FloorExpressionError.prototype); + this.name = this.constructor.name; + } +} +export class FloorExpression extends Expression { + static operator = 'floor'; + + eval(context) { + const val = this.operand(1, context); + if (typeof val != 'number') { + throw new FloorExpressionError('invalid operand type ' + this.json); + } + return Math.floor(val); + } +} + +export class MinExpression extends Expression { + static operator = 'min'; + + eval(context) { + const {json} = this; + let min = Infinity; + for (let i = 1, {length} = json; i < length; i++) { + let val = this.operand(i, context); + if (val < min) min = val; + } + return min; + } +} + +export class MaxExpression extends Expression { + static operator = 'max'; + + eval(context) { + const {json} = this; + let max = -Infinity; + for (let i = 1, {length} = json; i < length; i++) { + let val = this.operand(i, context); + if (val > max) max = val; + } + return max; + } +} diff --git a/packages/common/src/Expressions/StringExpressions.ts b/packages/common/src/Expressions/StringExpressions.ts new file mode 100644 index 000000000..da1740629 --- /dev/null +++ b/packages/common/src/Expressions/StringExpressions.ts @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2019-2024 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import {Expression} from './Expression'; + +export class StartsWithExpression extends Expression { + static operator = '^='; + + eval(context) { + const {json} = this; + let string = this.operand(1, context); + let searchString = this.operand(2, context); + + if (typeof string != 'string' || typeof searchString != 'string') { + return false; + } + return string.startsWith(searchString); + } +} + +export class EndsWithExpression extends Expression { + static operator = '$='; + + eval(context) { + const {json} = this; + let string = this.operand(1, context); + let searchString = this.operand(2, context); + + if (typeof string != 'string' || typeof searchString != 'string') { + return false; + } + return string.endsWith(searchString); + } +} + +export class SplitExpression extends Expression { + static operator = 'split'; + + eval(context) { + let string = this.operand(1, context); + let separator = this.operand(2, context); + return string.split(separator); + } +} + +export class ToStringExpression extends Expression { + static operator = 'to-string'; + + eval(context) { + let value = this.operand(1, context); + return String(value); + } +} + +export class ConcatExpression extends Expression { + static operator = 'concat'; + + eval(context) { + const {json} = this; + let str = ''; + for (let i = 1, length = json.length; i < length; i++) { + str += String(this.operand(i, context)); + } + return str; + } +} + +export class RegexReplaceExpression extends Expression { + static operator = 'regex-replace'; + + eval(context) { + let input = this.operand(1, context); + if (typeof input != 'string') { + return input; + } + + let pattern = this.operand(2, context); + if (typeof pattern != 'string') { + return input; + } + + let replacement = this.operand(3, context); + if (typeof replacement != 'string') { + return input; + } + return input.replace(new RegExp(pattern, 'g'), replacement); + } +} diff --git a/packages/common/src/Expressions/TypeExpressions.ts b/packages/common/src/Expressions/TypeExpressions.ts new file mode 100644 index 000000000..35cc4bb83 --- /dev/null +++ b/packages/common/src/Expressions/TypeExpressions.ts @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2019-2024 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import {Expression} from './Expression'; + +class TypeOfExpressionError extends Error { + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, TypeOfExpressionError.prototype); + this.name = this.constructor.name; + } +} + +class TypeOfExpression extends Expression { + eval(context) { + const {json} = this; + const type = json[0]; + for (let i = 1, len = json.length; i { return to; }; -const clone = (o) => { +const clone = (obj) => { // return this.extend(true,o); - let newO; - let i; - - if (typeof o !== 'object') { - return o; - } - - if (!o) { - return o; + if (typeof obj !== 'object' || !obj) { + return obj; } - - if (Object.prototype.toString.apply(o) === '[object Array]') { - newO = []; - for (i = 0; i < o.length; i += 1) { - newO[i] = clone(o[i]); + if (Array.isArray(obj)) { + const clonedObj = []; + for (let i = 0; i < obj.length; i += 1) { + clonedObj[i] = clone(obj[i]); } - return newO; + return clonedObj; } - - newO = {}; - for (i in o) { - if (o.hasOwnProperty(i)) { - newO[i] = clone(o[i]); + const clonedObj = {}; + for (let i in obj) { + if (obj.hasOwnProperty(i)) { + clonedObj[i] = clone(obj[i]); } } - return newO; + return clonedObj; }; const JSUtils = { diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 31d7dace9..7f682af39 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -32,6 +32,9 @@ import Queue from './Queue'; import * as vec3 from './Vec3'; import {AStar, AStarNode} from './AStar'; import {BinaryHeap} from './BinaryHeap'; +import {ExpressionParser} from './Expressions/ExpressionParser'; +import {JSONExpression, Expression, ExpressionMode} from './Expressions/Expression'; +import {Colors as Color} from './Color'; // make sure global ns is also available for webpack users. let scp: any = global; @@ -40,10 +43,10 @@ let scp: any = global; // support for deprecated root namespace (global).HERE = (global).here; -const common = {AStar, BinaryHeap, LRU, TaskManager, Listener, parseJSONArray, JSUtils, geotools, global, Queue, Set, Map, vec3, geometry}; +const common = {AStar, BinaryHeap, Color, Expression, ExpressionMode, ExpressionParser, LRU, TaskManager, Listener, parseJSONArray, JSUtils, geotools, global, Queue, Set, Map, vec3, geometry}; scp.common = common; -export {AStar, AStarNode, BinaryHeap, LRU, TaskManager, Task, TaskOptions, Listener, parseJSONArray, JSUtils, geotools, global, Queue, Set, Map, vec3, geometry}; +export {AStar, AStarNode, BinaryHeap, Color, JSONExpression, Expression, ExpressionMode, ExpressionParser, LRU, TaskManager, Task, TaskOptions, Listener, parseJSONArray, JSUtils, geotools, global, Queue, Set, Map, vec3, geometry}; export default common; diff --git a/packages/core/src/features/Feature.ts b/packages/core/src/features/Feature.ts index bc69b8089..6446c08fb 100644 --- a/packages/core/src/features/Feature.ts +++ b/packages/core/src/features/Feature.ts @@ -20,6 +20,7 @@ import {JSUtils} from '@here/xyz-maps-common'; import {FeatureProvider} from '../providers/FeatureProvider'; import {GeoJSONFeature, GeoJSONBBox, GeoJSONCoordinate} from './GeoJSON'; +import {TileLayer} from '@here/xyz-maps-core'; /** * represents a Feature in GeoJSON Feature format. @@ -150,6 +151,10 @@ export class Feature implements GeoJSONFeature typeof fnc == 'function'; - - // @ts-ignore - if (!isFnc(layerStyle.getStyleGroup) || !isFnc(layerStyle.setStyleGroup)) { - layerStyle = new LayerStyleImpl(layerStyle, keepCustom && this._sd && this._sd._c); + setStyle(layerStyle: LayerStyle| XYZLayerStyle, keepCustom: boolean = false) { + const _customFeatureStyles = keepCustom && this._sd?.getCustomStyles(); + // const isFnc = (fnc) => typeof fnc == 'function'; + // if (!isFnc(layerStyle.getStyleGroup) || !isFnc(layerStyle.setStyleGroup)) { + if (!(layerStyle instanceof XYZLayerStyle)) { + layerStyle = new XYZLayerStyle(layerStyle); } - this._sd = layerStyle; + (layerStyle as XYZLayerStyle).init?.(this, _customFeatureStyles); + + this._sd = layerStyle as XYZLayerStyle; this.dispatchEvent(STYLE_CHANGE_EVENT, {style: layerStyle}); }; + + getStyleManager(): XYZLayerStyle { + return this._sd; + }; /** * Get the current layerStyle. */ @@ -727,7 +733,6 @@ export class TileLayer extends Layer { return this._sd; }; - getMargin() { return this.margin; }; @@ -809,6 +814,10 @@ export class TileLayer extends Layer { // this.getProvider(this.max).getCopyright(cb); }; + + getStyleDefinitions(): LayerStyle['definitions'] { + return this._sd?.getDefinitions(); + } } // deprecated fallback.. diff --git a/packages/core/src/providers/MVTProvider/MVTProvider.ts b/packages/core/src/providers/MVTProvider/MVTProvider.ts index f718fb9ac..d9ad96dd3 100644 --- a/packages/core/src/providers/MVTProvider/MVTProvider.ts +++ b/packages/core/src/providers/MVTProvider/MVTProvider.ts @@ -32,6 +32,9 @@ class MvtFeature extends Feature { getMvtLayer() { return this.geometry.__xyz.l; } + getDataSourceLayer(layer:any) { + return this.geometry.__xyz.l; + } } diff --git a/packages/core/src/styles/BoxStyle.ts b/packages/core/src/styles/BoxStyle.ts index e5b6bdf0d..e4b463033 100644 --- a/packages/core/src/styles/BoxStyle.ts +++ b/packages/core/src/styles/BoxStyle.ts @@ -16,7 +16,7 @@ * SPDX-License-Identifier: Apache-2.0 * License-Filename: LICENSE */ -import {Color, StyleValueFunction, StyleZoomRange} from './LayerStyle'; +import {Color, StyleExpression, StyleValueFunction, StyleZoomRange} from './LayerStyle'; /** @@ -34,7 +34,7 @@ export interface BoxStyle { * The zIndex is defined relative to the "zLayer" property. * If "zLayer" is defined all zIndex values are relative to the "zLayer" value. */ - zIndex: number | StyleValueFunction | StyleZoomRange; + zIndex: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Indicates drawing order across multiple layers. @@ -44,21 +44,21 @@ export interface BoxStyle { * * @example \{...zLayer: 2, zIndex: 5\} will be rendered on top of \{...zLayer: 1, zIndex: 10\} */ - zLayer?: number | StyleValueFunction; + zLayer?: number | StyleValueFunction | StyleExpression; /** * Sets the color to fill the Box. * * @see {@link Color} for a detailed list of possible supported formats. */ - fill?: Color | StyleValueFunction | StyleZoomRange; + fill?: Color | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Sets the stroke color of the Box. * * @see {@link Color} for a detailed list of possible supported formats. */ - stroke?: Color | StyleValueFunction | StyleZoomRange; + stroke?: Color | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Sets the width of the stroke. @@ -76,7 +76,7 @@ export interface BoxStyle { * } * ``` */ - strokeWidth?: number | string | StyleValueFunction | StyleZoomRange; + strokeWidth?: number | string | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Defines the opacity of the style. @@ -84,7 +84,7 @@ export interface BoxStyle { * * @defaultValue 1 */ - opacity?: number | StyleValueFunction | StyleZoomRange; + opacity?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * The Width of the Box. @@ -101,7 +101,7 @@ export interface BoxStyle { * } * ``` */ - width: number | StyleValueFunction | StyleZoomRange; + width: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * The Height of the Box. @@ -120,7 +120,7 @@ export interface BoxStyle { * } * ``` */ - height?: number | StyleValueFunction | StyleZoomRange; + height?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * The depth of the Box. @@ -141,7 +141,7 @@ export interface BoxStyle { * } * ``` */ - depth?: number | StyleValueFunction | StyleZoomRange; + depth?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Offset the Box in pixels on x-axis. @@ -154,7 +154,7 @@ export interface BoxStyle { * { type: "Box", zIndex: 0, with: 32, fill: 'red', offsetX: 8} * ``` */ - offsetX?: number | string | StyleValueFunction | StyleZoomRange; + offsetX?: number | string | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Offset the Box in pixels on y-axis. @@ -170,7 +170,7 @@ export interface BoxStyle { * { type: "Box", zIndex: 0, fill: 'blue', width: 32, offsetY: "-1m"} * ``` */ - offsetY?: number | StyleValueFunction | StyleZoomRange; + offsetY?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Offset the Box in pixels on z-axis. @@ -186,7 +186,7 @@ export interface BoxStyle { * { type: "Box", zIndex: 0, fill: 'red', width:32, offsetZ: "1m"} * ``` */ - offsetZ?: number | string | StyleValueFunction | StyleZoomRange; + offsetZ?: number | string | StyleValueFunction | StyleZoomRange | StyleExpression; /** * The altitude of the center of the Box in meters. @@ -199,7 +199,7 @@ export interface BoxStyle { * * @experimental */ - altitude?: number | boolean | StyleValueFunction | StyleZoomRange + altitude?: number | boolean | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Scales the size of a style based on the feature's altitude. @@ -212,5 +212,5 @@ export interface BoxStyle { * * @experimental */ - scaleByAltitude?: boolean | StyleValueFunction | StyleZoomRange + scaleByAltitude?: boolean | StyleValueFunction | StyleZoomRange | StyleExpression; } diff --git a/packages/core/src/styles/CircleStyle.ts b/packages/core/src/styles/CircleStyle.ts index 0bfd3150b..53da068be 100644 --- a/packages/core/src/styles/CircleStyle.ts +++ b/packages/core/src/styles/CircleStyle.ts @@ -16,7 +16,7 @@ * SPDX-License-Identifier: Apache-2.0 * License-Filename: LICENSE */ -import {Color, StyleValueFunction, StyleZoomRange} from './LayerStyle'; +import {Color, StyleValueFunction, StyleZoomRange, StyleExpression} from './LayerStyle'; /** * Interface for configuring the visual appearance of Circles. @@ -33,7 +33,7 @@ export interface CircleStyle { * The zIndex is defined relative to the "zLayer" property. * If "zLayer" is defined all zIndex values are relative to the "zLayer" value. */ - zIndex: number | StyleValueFunction | StyleZoomRange; + zIndex: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Indicates drawing order across multiple layers. @@ -43,21 +43,21 @@ export interface CircleStyle { * * @example \{...zLayer: 2, zIndex: 5\} will be rendered on top of \{...zLayer: 1, zIndex: 10\} */ - zLayer?: number | StyleValueFunction; + zLayer?: number | StyleValueFunction | StyleExpression; /** * Sets the color to fill the Circle. * * @see {@link Color} for a detailed list of possible supported formats. */ - fill?: Color | StyleValueFunction | StyleZoomRange; + fill?: Color | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Sets the stroke color of the Circle. * * @see {@link Color} for a detailed list of possible supported formats. */ - stroke?: Color | StyleValueFunction | StyleZoomRange; + stroke?: Color | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Sets the width of the stroke. @@ -75,7 +75,7 @@ export interface CircleStyle { * } * ``` */ - strokeWidth?: number | string | StyleValueFunction | StyleZoomRange; + strokeWidth?: number | string | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Defines the opacity of the style. @@ -83,7 +83,7 @@ export interface CircleStyle { * It is valid for all style types. * @defaultValue 1 */ - opacity?: number | StyleValueFunction | StyleZoomRange; + opacity?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * The Radius of the Circle. @@ -108,7 +108,7 @@ export interface CircleStyle { * } * ``` */ - radius: number | StyleValueFunction | StyleZoomRange; + radius: number | string | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Offset the Circle in pixels on x-axis. @@ -121,7 +121,7 @@ export interface CircleStyle { * { type: "Circle", zIndex: 0, fill:'blue', radius: 4, offsetX: "-1m"} * ``` */ - offsetX?: number | string | StyleValueFunction | StyleZoomRange; + offsetX?: number | string | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Offset the Circle in pixels on y-axis. @@ -134,7 +134,7 @@ export interface CircleStyle { * { type: "Circle", zIndex: 0, fill:'blue', radius: 4, offsetY: "-1m"} * ``` */ - offsetY?: number | StyleValueFunction | StyleZoomRange; + offsetY?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Offset the Circle in pixels on z-axis. @@ -147,7 +147,7 @@ export interface CircleStyle { * { type: "Circle", zIndex: 0, fill:'blue', radius: 4, offsetZ: "1m"} * ``` */ - offsetZ?: number | string | StyleValueFunction | StyleZoomRange; + offsetZ?: number | string | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Alignment for styles of type "Circle". @@ -155,7 +155,7 @@ export interface CircleStyle { * "map" aligns to the plane of the map and "viewport" aligns to the plane of the viewport/screen. * Default alignment for Text based on point geometries is "viewport" while "map" is the default for line geometries. */ - alignment?: 'map' | 'viewport' | StyleValueFunction | StyleZoomRange; + alignment?: 'map' | 'viewport' | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Sets the anchor point for styles of type "Circle" when used with Line or Polygon geometry. @@ -170,7 +170,7 @@ export interface CircleStyle { * * @defaultValue For Polygon geometry the default is "Center". For Line geometry the default is "Coordinate". */ - anchor?: 'Line' | 'Coordinate' | 'Centroid' + anchor?: 'Line' | 'Coordinate' | 'Centroid' | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Enable or disable the space check for point styles on line geometries. @@ -180,7 +180,7 @@ export interface CircleStyle { * * @defaultValue true */ - checkLineSpace?: boolean + checkLineSpace?: boolean | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Enable or disable collision detection. @@ -192,12 +192,12 @@ export interface CircleStyle { * * @defaultValue true. */ - collide?: boolean | StyleValueFunction | StyleZoomRange; + collide?: boolean | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Enables collision detection and combines all styles of a StyleGroup with the same "CollisionGroup" into a single logical object for collision detection. */ - collisionGroup?: string | StyleValueFunction | StyleZoomRange + collisionGroup?: string | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Minimum distance in pixels between repeated style-groups on line geometries. @@ -205,7 +205,7 @@ export interface CircleStyle { * * @defaultValue 256 (pixels) */ - repeat?: number | StyleValueFunction | StyleZoomRange; + repeat?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * The altitude of the style in meters. @@ -218,7 +218,7 @@ export interface CircleStyle { * * @experimental */ - altitude?: number | boolean | StyleValueFunction | StyleZoomRange + altitude?: number | boolean | StyleValueFunction | StyleZoomRange | StyleExpression; /** @@ -232,5 +232,5 @@ export interface CircleStyle { * * @experimental */ - scaleByAltitude?: boolean | StyleValueFunction | StyleZoomRange + scaleByAltitude?: boolean | StyleValueFunction | StyleZoomRange | StyleExpression; } diff --git a/packages/core/src/styles/GenericStyle.ts b/packages/core/src/styles/GenericStyle.ts index 353582a0c..0d672e61a 100644 --- a/packages/core/src/styles/GenericStyle.ts +++ b/packages/core/src/styles/GenericStyle.ts @@ -16,8 +16,9 @@ * SPDX-License-Identifier: Apache-2.0 * License-Filename: LICENSE */ -import {Color, StyleValueFunction, StyleZoomRange} from './LayerStyle'; +import {Color, StyleExpression, StyleValueFunction, StyleZoomRange} from './LayerStyle'; import {LinearGradient} from '@here/xyz-maps-core'; +import {JSONExpression} from '@here/xyz-maps-common'; /** * The Style object defines how certain features should be rendered. @@ -72,7 +73,7 @@ export interface Style { * The zIndex is defined relative to the "zLayer" property. * If "zLayer" is defined all zIndex values are relative to the "zLayer" value. */ - zIndex: number | StyleValueFunction | StyleZoomRange; + zIndex: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Indicates drawing order across multiple layers. @@ -82,14 +83,14 @@ export interface Style { * * @example \{...zLayer: 2, zIndex: 5\} will be rendered on top of \{...zLayer: 1, zIndex: 10\} */ - zLayer?: number | StyleValueFunction; + zLayer?: number | StyleValueFunction | StyleExpression; /** * Specifies the URL of an image. * It can be either absolute or relative path. * It is only required by "Image". */ - src?: string | StyleValueFunction | StyleZoomRange; + src?: string | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Sets the color to fill the shape. @@ -97,7 +98,7 @@ export interface Style { * * @see {@link Color} for a detailed list of possible supported formats. */ - fill?: Color | StyleValueFunction | StyleZoomRange | LinearGradient; + fill?: Color | StyleValueFunction | StyleZoomRange | LinearGradient | StyleExpression; /** * Sets the stroke color of the shape. @@ -105,7 +106,7 @@ export interface Style { * * @see {@link Color} for a detailed list of possible supported formats. */ - stroke?: Color | StyleValueFunction | StyleZoomRange; + stroke?: Color | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Sets the width of the stroke. @@ -144,7 +145,7 @@ export interface Style { * } * ``` */ - strokeWidth?: number | string | StyleValueFunction | StyleZoomRange; + strokeWidth?: number | string | StyleValueFunction | StyleZoomRange | StyleExpression; /** * This controls the shape of the ends of lines. there are three possible values for strokeLinecap: @@ -155,7 +156,7 @@ export interface Style { * * If "strokeLinecap" is used in combination with "altitude", only "butt" is supported for "strokeLinecap". */ - strokeLinecap?: string | StyleValueFunction | StyleZoomRange; + strokeLinecap?: string | StyleValueFunction | StyleZoomRange | StyleExpression; /** * The joint where the two segments in a line meet is controlled by the strokeLinejoin attribute, There are three possible values for this attribute: @@ -166,7 +167,7 @@ export interface Style { * * If "strokeLinejoin" is used in combination with "altitude", the use of "round" is not supported. */ - strokeLinejoin?: string | StyleValueFunction | StyleZoomRange; + strokeLinejoin?: string | StyleValueFunction | StyleZoomRange | StyleExpression; /** * The strokeDasharray attribute controls the pattern of dashes and gaps used to stroke paths. @@ -186,7 +187,7 @@ export interface Style { * // dash -> 10 meter, gap -> 10 pixel. * strokeDasharray: ["20m",10] || ["20m","10px"] */ - strokeDasharray?: (number|string)[] | StyleValueFunction<(number|string)[]> | StyleZoomRange<(number|string)[]> | 'none'; + strokeDasharray?: (number | string)[] | StyleValueFunction<(number | string)[]> | StyleZoomRange<(number | string)[]> | StyleExpression<(number | string)[]> | 'none'; /** * Defines the opacity of the style. @@ -194,7 +195,7 @@ export interface Style { * It is valid for all style types. * @defaultValue 1 */ - opacity?: number | StyleValueFunction | StyleZoomRange; + opacity?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * The Radius of the Circle and Sphere. @@ -231,7 +232,7 @@ export interface Style { * } * ``` */ - radius?: number | StyleValueFunction | StyleZoomRange; + radius?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Width of the style in pixels. @@ -260,7 +261,7 @@ export interface Style { * } * ``` */ - width?: number | StyleValueFunction | StyleZoomRange; + width?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Height of the style in pixels. @@ -301,7 +302,7 @@ export interface Style { * } * ``` */ - height?: number | StyleValueFunction | StyleZoomRange; + height?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * The depth of the style in pixels. @@ -321,7 +322,7 @@ export interface Style { * } * ``` */ - depth?: number | StyleValueFunction | StyleZoomRange; + depth?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * CSS font string for texts. @@ -329,7 +330,7 @@ export interface Style { * * @defaultValue “normal 12px Arial” */ - font?: string | StyleValueFunction | StyleZoomRange; + font?: string | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Text is either a string or a function that generates the string that should be displayed. @@ -344,7 +345,7 @@ export interface Style { * } * ``` */ - text?: string | number | boolean | StyleValueFunction | StyleZoomRange; + text?: string | number | boolean | StyleValueFunction | StyleZoomRange | StyleExpression; /** * "textRef" Reference to an attribute of an feature that's value should be displayed as text. @@ -363,7 +364,7 @@ export interface Style { * textRef: "id" * ``` */ - textRef?: string | StyleValueFunction | StyleZoomRange; + textRef?: string | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Define the starting position of a segment of the entire line in %. @@ -375,7 +376,7 @@ export interface Style { * from: 0.0 // -\> 0%, the segment has the same starting point as the entire line * from: 0.5 // -\> 50%, the segment starts in the middle of the entire line */ - from?: number | StyleValueFunction | StyleZoomRange; + from?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Define the end position of a segment of the entire line in %. @@ -387,7 +388,7 @@ export interface Style { * to: 0.5 // -\> 50%, the segment ends in the middle of the entire line * to: 1.0 // -\> 100%, the segment has the same end point as the entire line */ - to?: number | StyleValueFunction | StyleZoomRange; + to?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Offset the shape in pixels on x-axis. @@ -404,7 +405,7 @@ export interface Style { * { type: "Circle", zIndex: 0, fill:'blue', radius: 4, offsetX: "-1m"} * ``` */ - offsetX?: number | string | StyleValueFunction | StyleZoomRange; + offsetX?: number | string | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Offset the shape in pixels on y-axis. @@ -421,7 +422,7 @@ export interface Style { * { type: "Circle", zIndex: 0, fill:'blue', radius: 4, offsetY: "-1m"} * ``` */ - offsetY?: number | StyleValueFunction | StyleZoomRange; + offsetY?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Offset the shape in pixels on z-axis. @@ -438,7 +439,7 @@ export interface Style { * { type: "Circle", zIndex: 0, fill:'blue', radius: 4, offsetZ: "1m"} * ``` */ - offsetZ?: number | string | StyleValueFunction | StyleZoomRange; + offsetZ?: number | string | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Offset a line to the left or right side in pixel or meter. @@ -456,7 +457,7 @@ export interface Style { * { type: "Line", zIndex: 0, stroke:'blue', strokeWidth: 4, offset: "2m"} * ``` */ - offset?: number | string | StyleValueFunction | StyleZoomRange; + offset?: number | string | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Alignment for styles of type "Circle", "Rect", "Image" and "Text". @@ -464,20 +465,20 @@ export interface Style { * "map" aligns to the plane of the map and "viewport" aligns to the plane of the viewport/screen. * Default alignment for Text based on point geometries is "viewport" while "map" is the default for line geometries. */ - alignment?: 'map' | 'viewport' | StyleValueFunction | StyleZoomRange; + alignment?: 'map' | 'viewport' | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Rotate the shape of the style to the angle in degrees. * This attribute is validate for Rect and Image. */ - rotation?: number | StyleValueFunction | StyleZoomRange; + rotation?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * In case of label collision, Text with a higher priority (lower value) will be drawn before lower priorities (higher value). * If the collision detection is enabled for multiple Styles within the same StyleGroup, the highest priority (lowest value) * is used. */ - priority?: number | StyleValueFunction | StyleZoomRange; + priority?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Minimum distance in pixels between repeated style-groups on line geometries. @@ -485,7 +486,7 @@ export interface Style { * * @defaultValue 256 (pixels) */ - repeat?: number | StyleValueFunction | StyleZoomRange; + repeat?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Enable oder Disable line wrapping for styles of type "Text". @@ -498,7 +499,7 @@ export interface Style { * * @defaultValue 14 */ - lineWrap?: number | StyleValueFunction | StyleZoomRange; + lineWrap?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Sets the anchor point for styles of type "Circle", "Rect", "Image" and "Text" used with Line or Polygon geometry. @@ -513,7 +514,7 @@ export interface Style { * * @defaultValue For Polygon geometry the default is "Center". For Line geometry the default for styles of type "Text" is "Line", while "Coordinate" is the default for styles of type "Circle", "Rect" or "Image". */ - anchor?: 'Line' | 'Coordinate' | 'Centroid' | 'Center' + anchor?: 'Line' | 'Coordinate' | 'Centroid' | 'Center' | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Enable or disable the space check for point styles on line geometries. @@ -523,7 +524,7 @@ export interface Style { * * @defaultValue true */ - checkLineSpace?: boolean + checkLineSpace?: boolean | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Enable or disable collision detection. @@ -536,18 +537,18 @@ export interface Style { * * @defaultValue false for "Text", true for all other. */ - collide?: boolean | StyleValueFunction | StyleZoomRange; + collide?: boolean | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Enables collision detection and combines all styles of a StyleGroup with the same "CollisionGroup" into a single logical object for collision detection. */ - collisionGroup?: string | StyleValueFunction | StyleZoomRange + collisionGroup?: string | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Extrude a Polygon or MultiPolygon geometry in meters. * This attribute is validate for styles of type "Polygon" only. */ - extrude?: number | StyleValueFunction | StyleZoomRange; + extrude?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * The base of the Extrude in meters. @@ -557,7 +558,7 @@ export interface Style { * * @defaultValue 0 */ - extrudeBase?: number | StyleValueFunction | StyleZoomRange; + extrudeBase?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * The altitude of the style in meters. @@ -571,7 +572,7 @@ export interface Style { * * @experimental */ - altitude?: number | boolean | StyleValueFunction | StyleZoomRange + altitude?: number | boolean | StyleValueFunction | StyleZoomRange | StyleExpression; /** @@ -585,5 +586,34 @@ export interface Style { * * @experimental */ - scaleByAltitude?: boolean | StyleValueFunction | StyleZoomRange + scaleByAltitude?: boolean | StyleValueFunction | StyleZoomRange | StyleExpression; + + /** + * Sets the condition(s) to determine whether a feature should be rendered with the respective style. + * The `filter` expression is evaluated to determine if the respective style should be applied to a feature. + * It must resolve to a boolean value where `true` means the style applies and `false` means it does not. + * + * This property is only used when {@link LayerStyle.assign} is not defined; otherwise, `filter` is ignored. + * + * @example + * // Render features where the "type" property is equal to "park" + * filter: ['==', ['get', 'type'], 'park'] + * + * @example + * // Render features where the "population" property is greater than 1000 + * filter: ['>', ['get', 'population'], 1000] + * + * @example + * // Render features where the "type" property is "park" and "population" is less than 500 + * filter: ['all', ['==', ['get', 'type'], 'park'], ['<', ['get', 'population'], 500]] + * + * @example + * // Render features where the "type" property is not "residential" + * filter: ['!=', ['get', 'type'], 'residential'] + * + * @example + * // Render features where the "type" property is either "park" or "garden" + * filter: ['any', ['==', ['get', 'type'], 'park'], ['==', ['get', 'type'], 'garden']] + */ + filter?: StyleValueFunction | StyleExpression; } diff --git a/packages/core/src/styles/HeatmapStyle.ts b/packages/core/src/styles/HeatmapStyle.ts index 74ad8310f..83507e631 100644 --- a/packages/core/src/styles/HeatmapStyle.ts +++ b/packages/core/src/styles/HeatmapStyle.ts @@ -16,7 +16,7 @@ * SPDX-License-Identifier: Apache-2.0 * License-Filename: LICENSE */ -import {StyleValueFunction, StyleZoomRange} from './LayerStyle'; +import {StyleExpression, StyleValueFunction, StyleZoomRange} from './LayerStyle'; /** * LinearGradient @@ -66,7 +66,7 @@ export interface HeatmapStyle { * The zIndex is defined relative to the "zLayer" property. * If "zLayer" is defined all zIndex values are relative to the "zLayer" value. */ - zIndex: number | StyleValueFunction | StyleZoomRange; + zIndex: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Indicates drawing order across multiple layers. @@ -76,14 +76,14 @@ export interface HeatmapStyle { * * @example \{...zLayer: 2, zIndex: 5\} will be rendered on top of \{...zLayer: 1, zIndex: 10\} */ - zLayer?: number | StyleValueFunction; + zLayer?: number | StyleValueFunction | StyleExpression; /** * The radius in pixels with which to render a single point of the heatmap. * * @defaultValue 24 */ - radius?: number | StyleValueFunction | StyleZoomRange; + radius?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * The fill color is a linear gradient used to colorize the heatmap. @@ -103,7 +103,7 @@ export interface HeatmapStyle { * * @defaultValue 1 */ - weight?: number | StyleValueFunction; + weight?: number | StyleValueFunction | StyleExpression; /** * The intensity of the Heatmap is a global multiplier on top of the weight. @@ -111,7 +111,7 @@ export interface HeatmapStyle { * * @defaultValue 1 */ - intensity?: number | StyleZoomRange; + intensity?: number | StyleZoomRange | StyleExpression; /** * Defines the global opacity of the heatmap. @@ -119,5 +119,5 @@ export interface HeatmapStyle { * * @defaultValue 1 */ - opacity?: number | StyleValueFunction | StyleZoomRange; + opacity?: number | StyleValueFunction | StyleZoomRange | StyleExpression; } diff --git a/packages/core/src/styles/ImageStyle.ts b/packages/core/src/styles/ImageStyle.ts index a84c2ebf0..c6641c211 100644 --- a/packages/core/src/styles/ImageStyle.ts +++ b/packages/core/src/styles/ImageStyle.ts @@ -16,7 +16,7 @@ * SPDX-License-Identifier: Apache-2.0 * License-Filename: LICENSE */ -import {StyleValueFunction, StyleZoomRange} from './LayerStyle'; +import {StyleExpression, StyleValueFunction, StyleZoomRange} from './LayerStyle'; /** * Interface for configuring the visual appearance of Images/Icons. @@ -33,7 +33,7 @@ export interface ImageStyle { * The zIndex is defined relative to the "zLayer" property. * If "zLayer" is defined all zIndex values are relative to the "zLayer" value. */ - zIndex: number | StyleValueFunction | StyleZoomRange; + zIndex: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Indicates drawing order across multiple layers. @@ -43,13 +43,13 @@ export interface ImageStyle { * * @example \{...zLayer: 2, zIndex: 5\} will be rendered on top of \{...zLayer: 1, zIndex: 10\} */ - zLayer?: number | StyleValueFunction; + zLayer?: number | StyleValueFunction | StyleExpression; /** * Specifies the URL of the image to render. * It can be either absolute or relative path. */ - src: string | StyleValueFunction | StyleZoomRange; + src: string | StyleValueFunction | StyleZoomRange | StyleExpression; /** * If specified, the Image provided by {@link src} is considered as an IconAtlas/TextureAtlas. @@ -63,7 +63,7 @@ export interface ImageStyle { * It is valid for all style types. * @defaultValue 1 */ - opacity?: number | StyleValueFunction | StyleZoomRange; + opacity?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Width of the Image in pixels. @@ -80,7 +80,7 @@ export interface ImageStyle { * } * ``` */ - width: number | StyleValueFunction | StyleZoomRange; + width: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Height of the Image in pixels. @@ -100,7 +100,7 @@ export interface ImageStyle { * } * ``` */ - height?: number | StyleValueFunction | StyleZoomRange; + height?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Offset the shape in pixels on x-axis. @@ -114,7 +114,7 @@ export interface ImageStyle { * * ``` */ - offsetX?: number | string | StyleValueFunction | StyleZoomRange; + offsetX?: number | string | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Offset the shape in pixels on y-axis. @@ -127,7 +127,7 @@ export interface ImageStyle { * { type: "Image", zIndex: 0, src: '...', offsetY: 8} * ``` */ - offsetY?: number | StyleValueFunction | StyleZoomRange; + offsetY?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Offset the shape in pixels on z-axis. @@ -140,7 +140,7 @@ export interface ImageStyle { * { type: "Image", zIndex: 0, src: '...', offsetZ: 8} * ``` */ - offsetZ?: number | string | StyleValueFunction | StyleZoomRange; + offsetZ?: number | string | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Alignment for styles of type "Circle". @@ -148,19 +148,19 @@ export interface ImageStyle { * "map" aligns to the plane of the map and "viewport" aligns to the plane of the viewport/screen. * Default alignment for Text based on point geometries is "viewport" while "map" is the default for line geometries. */ - alignment?: 'map' | 'viewport' | StyleValueFunction | StyleZoomRange; + alignment?: 'map' | 'viewport' | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Rotate the shape of the style to the angle in degrees. */ - rotation?: number | StyleValueFunction | StyleZoomRange; + rotation?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * In case of label collision, Text with a higher priority (lower value) will be drawn before lower priorities (higher value). * If the collision detection is enabled for multiple Styles within the same StyleGroup, the highest priority (lowest value) * is used. */ - priority?: number | StyleValueFunction | StyleZoomRange; + priority?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Minimum distance in pixels between repeated style-groups on line geometries. @@ -168,7 +168,7 @@ export interface ImageStyle { * * @defaultValue 256 (pixels) */ - repeat?: number | StyleValueFunction | StyleZoomRange; + repeat?: number | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Sets the anchor point for styles of type "Image" used with Line or Polygon geometry. @@ -183,7 +183,7 @@ export interface ImageStyle { * * @defaultValue For Polygon geometry the default is "Center". For Line geometry the default is "Line". */ - anchor?: 'Line' | 'Coordinate' | 'Centroid' + anchor?: 'Line' | 'Coordinate' | 'Centroid' | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Enable or disable the space check for point styles on line geometries. @@ -192,7 +192,7 @@ export interface ImageStyle { * * @defaultValue true */ - checkLineSpace?: boolean + checkLineSpace?: boolean | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Enable or disable collision detection. @@ -204,12 +204,12 @@ export interface ImageStyle { * * @defaultValue false for "Text", true for all other. */ - collide?: boolean | StyleValueFunction | StyleZoomRange; + collide?: boolean | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Enables collision detection and combines all styles of a StyleGroup with the same "CollisionGroup" into a single logical object for collision detection. */ - collisionGroup?: string | StyleValueFunction | StyleZoomRange + collisionGroup?: string | StyleValueFunction | StyleZoomRange | StyleExpression; /** * The altitude of the style in meters. @@ -222,7 +222,7 @@ export interface ImageStyle { * * @experimental */ - altitude?: number | boolean | StyleValueFunction | StyleZoomRange + altitude?: number | boolean | StyleValueFunction | StyleZoomRange | StyleExpression; /** * Scales the size of a style based on the feature's altitude. @@ -235,5 +235,5 @@ export interface ImageStyle { * * @experimental */ - scaleByAltitude?: boolean | StyleValueFunction | StyleZoomRange + scaleByAltitude?: boolean | StyleValueFunction | StyleZoomRange | StyleExpression; } diff --git a/packages/core/src/styles/LayerStyle.ts b/packages/core/src/styles/LayerStyle.ts index 816633c4d..470af7a79 100644 --- a/packages/core/src/styles/LayerStyle.ts +++ b/packages/core/src/styles/LayerStyle.ts @@ -20,6 +20,161 @@ import {Feature} from '../features/Feature'; import {Style} from './GenericStyle'; +/** + * A StyleExpression is a JSON array representing an expression that returns the desired value + * for a specific style property. It is particularly useful for data-driven styling in map or UI components. + * + * The structure of a StyleExpression is as follows: + * - The first element (index 0) is a string that specifies the operator of the expression. + * - The subsequent elements are the operands required by the operator. + * + * @template ResultType - The type of the value that the expression returns. + * + * ## StyleExpression Operators + * + * A StyleExpression is a JSON array representing an expression that returns the desired value for a specific style property. Below are the possible operators and their descriptions, along with examples. + * + * ### Reference + * - **`ref`**: References another expression by name. + * - Example: `["ref", "otherExpression"]` + * + * ### Data Retrieval + * - **`get`**: Retrieves a property from the input data. The third optional operand specifies the input data from which to retrieve the property. If not provided, it defaults to `feature.properties`. + * + * The property name can also be a global map context variable: + * - `$zoom`: The current zoom level. + * - `$layer`: The name of the datasource layer. + * - `$geometryType`: The type of the current feature geometry ("line", "point", "polygon"). + * - `$id`: The ID of the current feature. + * + * - Example: `["get", "propertyName"]` // Retrieves `propertyName` from `feature.properties`. + * - Example with input data: `["get", "propertyName", { custom: "data" }]` // Retrieves `propertyName` from the specified input data. + * - Example with global map context variable: `["get", "$zoom"]` // Retrieves the current zoom level. + * + * ### Arithmetic + * - **`+`**: Adds two numbers. + * - Example: `["+", 2, 3]` // Outputs: 5 + * - **`-`**: Subtracts the second number from the first. + * - Example: `["-", 5, 3]` // Outputs: 2 + * - **`*`**: Multiplies two numbers. + * - Example: `["*", 2, 3]` // Outputs: 6 + * - **`/`**: Divides the first number by the second. + * - Example: `["/", 6, 3]` // Outputs: 2 + * - **`%`**: Computes the remainder of dividing the first number by the second. + * - Example: `["%", 5, 2]` // Outputs: 1 + * - **`floor`**: Rounds down a number to the nearest integer. + * - Example: `["floor", 4.7]` // Outputs: 4 + * - **`min`**: Returns the smallest number. + * - Example: `["min", 1, 2, 3]` // Outputs: 1 + * - **`max`**: Returns the largest number. + * - Example: `["max", 1, 2, 3]` // Outputs: 3 + * + * ### Logical + * - **`all`**: Returns true if all conditions are true. + * - Example: `["all", true, true]` // Outputs: true + * - **`any`**: Returns true if any condition is true. + * - Example: `["any", true, false]` // Outputs: true + * - **`!`**: Negates a boolean value. + * - Example: `["!", true]` // Outputs: false + * - **`!has`**: Checks if a property does not exist. + * - Example: `["!has", "propertyName"]` + * - **`has`**: Checks if a property exists. + * - Example: `["has", "propertyName"]` + * - **`none`**: Returns true if no conditions are true. + * - Example: `["none", false, false]` // Outputs: true + * + * ### Comparison + * - **`==`**: Checks if two values are equal. + * - Example: `["==", 2, 2]` // Outputs: true + * - **`!=`**: Checks if two values are not equal. + * - Example: `["!=", 2, 3]` // Outputs: true + * - **`>`**: Checks if the first value is greater than the second. + * - Example: `[" >", 3, 2]` // Outputs: true + * - **`>=`**: Checks if the first value is greater than or equal to the second. + * - Example: `[" >=", 3, 3]` // Outputs: true + * - **`<=`**: Checks if the first value is less than or equal to the second. + * - Example: `["<=", 2, 2]` // Outputs: true + * - **`<`**: Checks if the first value is less than the second. + * - Example: `["<", 2, 3]` // Outputs: true + * - **`^=`**: Checks if a string starts with a given substring. + * - Example: `["^=", "hello", "he"]` // Outputs: true + * - **`$=`**: Checks if a string ends with a given substring. + * - Example: `["$=", "hello", "lo"]` // Outputs: true + * + * ### String Manipulation + * - **`split`**: Splits a string by a delimiter. + * - Example: `["split", "a,b,c", ","]` // Outputs: ["a", "b", "c"] + * - **`to-string`**: Converts a value to a string. + * - Example: `["to-string", 123]` // Outputs: "123" + * - **`concat`**: Concatenates multiple strings. + * - Example: `["concat", "hello", " ", "world"]` // Outputs: "hello world" + * - **`regex-replace`**: Replaces parts of a string matching a regex. + * - Example: `["regex-replace", "hello world", "world", "there"]` // Outputs: "hello there" + * - **`slice`**: Extracts a section of a string. + * - Example: `["slice", "hello", 0, 2]` // Outputs: "he" + * - **`at`**: Gets the character at a specified index in a string. + * - Example: `["at", "hello", 1]` // Outputs: "e" + * - **`length`**: Gets the length of a string. + * - Example: `["length", "hello"]` // Outputs: 5 + * + * ### Conditional + * - **`case`**: Evaluates conditions in order and returns the corresponding result for the first true condition. + * - Example: `["case", ["==", 1, 1], "one", ["==", 2, 2], "two", "default"]` // Outputs: "one" + * - **`step`**: Returns a value from a step function based on input. + * - Example: `["step", 3, "small", 5, "medium", 10, "large"]` // Outputs: "small" + * - **`match`**: Returns a value based on matching input values. + * - Example: `["match", "a", "a", 1, "b", 2, 0]` // Outputs: 1 + * + * ### Utility + * - **`literal`**: Returns a literal value. + * - Example: `["literal", [1, 2, 3]]` // Outputs: [1, 2, 3] + * - **`lookup`**: Finds an entry in a table that matches the given key values. The entry with the most matching keys is returned. + * If multiple entries match equally, one of them is returned. + * If no match is found, the default entry (if any) is returned. If no default entry is defined, null is returned. + * - The `lookupTable` should be an array of objects, each with a `keys` member and an `attributes` member: + * - `keys`: An object containing key-value pairs used for matching. + * - `attributes`: An object containing the attributes to be returned when a match is found. + * - Example: + * ```javascript + * { + * "definitions": { + * "lookupTable": [ "literal", [ + * { "keys": { "country": "US" }, "attributes": { "population": 331000000 } }, + * { "keys": { "country": "US", "state": "CA" }, "attributes": { "population": 39500000 } }, + * { "keys": {}, "attributes": { "population": 7800000000 } } // Default entry + * ]] + * } + * } + * // This example looks up the population for the state of California in the United States from the `lookupTable`. + * ["lookup", { "country": "US", "state": "CA" }, ["ref", "lookupTable"]] // Outputts: `{ "population": 39500000 }`. + * ``` + * + * ### Type Conversion + * - **`number`**: Converts a value to a number. + * - Example: `["number", "123"]` // Outputs: 123 + * - **`boolean`**: Converts a value to a boolean. + * - Example: `["boolean", "true"]` // Outputs: true + * - **`to-number`**: Converts a value to a number. + * - Example: `["to-number", "123"]` // Outputs: 123 + * - **`to-boolean`**: Converts a value to a boolean. + * - Example: `["to-boolean", "true"]` // Outputs: true + * + * ### Zoom and Interpolation + * - **`zoom`**: Returns the current zoom level. + * - Example: `["zoom"]` // Outputs: current zoom level + * - **`interpolate`**: Interpolates between values based on zoom level. + * - Example: `["interpolate", ["linear"], ["zoom"], 10, 1, 15, 10]` + * + * Example: + * ```typescript + * const expression: StyleExpression = ["==", ["get", "property"], "value"]; + * ``` + * In this example, `"=="` is the operator, and `["get", "property"]` and `"value"` are the operands. + * + * Operators can include logical, arithmetic, string manipulation, and other types of operations, which are evaluated + * to determine the final value of the style property. + */ +export type StyleExpression = [string, ...any[]]; // JSONExpression /** * A StyleValueFunction is a function that returns the desired value for the respective style property. @@ -52,7 +207,7 @@ export type StyleValueFunction = (feature: Feature, zoom: number) => Type * } * ``` */ -export type StyleZoomRange = { [zoom: number]: Type } +export type StyleZoomRange = { [zoom: number|string]: Type } export {Style}; // /** @@ -72,10 +227,13 @@ export {Style}; export type StyleGroupMap = { [id: string]: StyleGroup } -export type StyleGroup = Array + + +
+ + diff --git a/packages/playground/examples/layer/style_expressions.ts b/packages/playground/examples/layer/style_expressions.ts new file mode 100644 index 000000000..abbcfb014 --- /dev/null +++ b/packages/playground/examples/layer/style_expressions.ts @@ -0,0 +1,80 @@ +import {MVTLayer} from '@here/xyz-maps-core'; +import {Map} from '@here/xyz-maps-display'; + +// setup the Map Display +const display = new Map(document.getElementById('map'), { + zoomlevel: 2, + center: { + longitude: -96.76883, latitude: 39.6104 + }, + // add layers to display + layers: [ + new MVTLayer({ + name: 'mvt-world-layer', + remote: { + url: 'https://vector.hereapi.com/v2/vectortiles/base/mc/{z}/{x}/{y}/omv?apikey=' + YOUR_API_KEY + // optional settings: + // max : 16, // max level for loading data + // min : 1 // min level for loading data + // tileSize : 512 // 512|256 defines mvt tilesize in case it can't be automatically detected in url.. + }, + min: 1, + max: 20, + + style: { + + backgroundColor: '#555555', + + definitions: { + 'isPolygonGeometry': ['==', ['get', '$geometryType'], 'polygon'], + 'isEarthLayer': ['all', ['==', ['get', '$layer'], 'earth'], ['ref', 'isPolygonGeometry']], + 'isWaterLayer': ['all', ['==', ['get', '$layer'], 'water'], ['==', ['get', '$geometryType'], 'polygon']], + 'isLanduseLayer': ['all', ['==', ['get', '$layer'], 'landuse'], ['==', ['get', '$geometryType'], 'polygon']], + 'isRoadsLayer': ['all', ['==', ['get', '$layer'], 'roads'], ['!=', ['get', 'kind'], 'ferry'], ['!=', ['get', 'kind'], 'rail']], + 'isBuildingsLayer': ['all', ['==', ['get', '$layer'], 'buildings'], ['ref', 'isPolygonGeometry']] + }, + + styleGroups: { + 'earth': [{ + filter: ['ref', 'isEarthLayer'], + zIndex: 1, + type: 'Polygon', + fill: '#555555' + }], + 'water': [{ + filter: ['ref', 'isWaterLayer'], + zIndex: 2, + type: 'Polygon', + fill: '#353535' + }], + 'landuse': [{ + filter: ['ref', 'isLanduseLayer'], + zIndex: 3, + type: 'Polygon', + fill: '#666666' + }], + 'roads': [{ + filter: ['all', ['ref', 'isRoadsLayer'], ['!=', ['get', 'kind'], 'highway']], + zIndex: 4, + type: 'Line', + stroke: '#888', + strokeWidth: {14: 1, 15: '4m'} + }, { + filter: ['all', ['ref', 'isRoadsLayer'], ['==', ['get', 'kind'], 'highway']], + zIndex: 5, + type: 'Line', + stroke: '#aaa', + strokeWidth: {14: 1.5, 15: '8m'} + }], + 'buildings': [{ + filter: ['ref', 'isBuildingsLayer'], + zIndex: 7, + type: 'Polygon', + fill: '#999999', + extrude: ['case', ['>', ['get', '$zoom'], 16], ['get', 'height'], null] + }] + } + } + }) + ] +}); diff --git a/packages/tests/specs/common/expressions/expressions_spec.ts b/packages/tests/specs/common/expressions/expressions_spec.ts new file mode 100644 index 000000000..421232bd8 --- /dev/null +++ b/packages/tests/specs/common/expressions/expressions_spec.ts @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2019-2024 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import {JSONExpression, ExpressionMode, ExpressionParser} from '@here/xyz-maps-common'; + +describe('Expressions', function() { + const {expect} = chai; + + function expectExpression(type: string, exp: any) { + expect(exp).to.be.an('object'); + expect(exp.json).to.be.an('array'); + expect(exp.json[0]).equals(type); + } + + + const definitions = { + 'object': ['literal', {'prop': 'value'}] + }; + const environment = { + $zoom: 14, + zoom: 14.5, + $geometryType: 'point', + $id: 'id1', + $layer: 'testLayer' + }; + + let exprParser = new ExpressionParser(definitions, environment); + + const context = {aName: 'testName', aNumber: 123, aString: 'testString'}; + const evalExpression = (exp: JSONExpression, mode = ExpressionMode.static) => { + exprParser.setMode(mode); + return exprParser.evaluate(exp, context); + }; + + it('evaluate simple sum expression', async () => { + const result = evalExpression(['+', 1, 2]); + expect(result).to.equal(3); + }); + + it('evaluate sum expression, expression operand', async () => { + const result = evalExpression(['+', 1, ['get', 'aNumber']]); + expect(result).to.equal(124); + }); + + it('evaluate sum expression, dynamic expression operand', async () => { + const result = evalExpression(['+', 1, ['zoom']]); + expect(result).to.equal(15.5); + }); + + it('(dynamic) evaluate sum expression, dynamic expression operand', async () => { + const result = evalExpression(['+', 1, ['zoom']], ExpressionMode.dynamic); + expectExpression('+', result); + }); + + it('evaluate simple subtract expression', async () => { + const result = evalExpression(['-', 3, 1]); + expect(result).to.equal(2); + }); + + it('evaluate simple get', async () => { + const result = evalExpression(['get', 'aName']); + expect(result).to.equal('testName'); + }); + + it('evaluate get environment variable', async () => { + const result = evalExpression(['get', '$zoom']); + expect(result).to.equal(environment.$zoom); + }); + + it('evaluate get custom object variable', async () => { + const result = evalExpression(['get', 'prop', ['ref', 'object']]); + expect(result).to.equal('value'); + }); + + it('evaluate case expression: condition 1', async () => { + const result = evalExpression(['case', ['==', 2, 2], 111, ['==', 2, 2], ['zoom'], 0]); + expect(result).to.equal(111); + }); + + it('evaluate case expression: condition 2', async () => { + const result = evalExpression(['case', ['==', 1, 2], 111, ['==', 2, 2], 222, 0]); + expect(result).to.equal(222); + }); + + it('evaluate case expression: condition 2, evaluated expr result', async () => { + const result = evalExpression(['case', ['==', 1, 2], 111, ['==', 2, 2], ['get', 'aNumber'], 0]); + expect(result).to.equal(context.aNumber); + }); + + it('(dynamic) evaluate case expression: dynamic condition', async () => { + const result = evalExpression(['case', ['==', 1, 2], 111, ['==', 2, ['zoom']], 222, 0], ExpressionMode.dynamic); + expectExpression('case', result); + }); + + it('evaluate case expression: dynamic expression result', async () => { + const result = evalExpression(['case', ['==', 1, 2], 111, ['==', 2, 2], ['zoom'], 0]); + expect(result).to.equal(environment.zoom); + }); + + it('(dynamic) evaluate case expression: dynamic expression result', async () => { + const result = evalExpression(['case', ['==', 1, 2], 111, ['==', 2, 2], ['zoom'], 0], ExpressionMode.dynamic); + expectExpression('zoom', result); + }); + + it('evaluate nested case expressions', async () => { + let jsonExp = ['case', + ['!=', ['zoom'], 14.5], 555, + ['!=', 2, 1], ['case', ['==', ['zoom'], 1], 333, 'case2Fallback'], + 'case1Fallback' + ]; + const result = evalExpression(jsonExp); + expect(result).to.equal('case2Fallback'); + }); + + it('(dynamic) evaluate nested case expressions', async () => { + let jsonExp = ['case', + ['!=', ['zoom'], 14.5], 555, + ['!=', 2, 1], ['case', ['==', ['zoom'], 1], 333, 'case2Fallback'], + 'case1Fallback' + ]; + const result = evalExpression(jsonExp, ExpressionMode.dynamic); + expectExpression('case', result); + expect(result.json[result.json.length - 1]).to.equal('case1Fallback'); + }); + + it('(dynamic) evaluate nested case expressions, partial result', async () => { + let jsonExp = ['case', + ['!=', 1, 1], 555, + ['!=', 2, 1], ['case', ['==', ['zoom'], 1], 333, 'case2Fallback'], + 'case1Fallback' + ]; + const result = evalExpression(jsonExp, ExpressionMode.dynamic); + expectExpression('case', result); + expect(result.json[result.json.length - 1]).to.equal('case2Fallback'); + }); + + it('evaluate interpolate expression', async () => { + const value = 18; + const result = evalExpression(['interpolate', ['linear'], value, 3, 0, 3, 10, 3, 4, 5, 5, 17, 64, 19, 19, 22, 22]); + expect(result).to.equal(41.5); + }); + + it('evaluate interpolate expression with expression value', async () => { + const value = ['zoom']; + const result = evalExpression(['interpolate', ['linear'], value, 3, 0, 3, 10, 3, 4, 5, 1, 17, 64, 19, 19, 22, 22]); + expect(result).to.equal(50.875); + }); + + it('(dynamic) evaluate interpolate', async () => { + const value = 18; + const result = evalExpression(['interpolate', ['linear'], value, 3, 0, 3, 10, 3, 4, 5, 5, 17, 64, 19, 19, 22, 22], ExpressionMode.dynamic); + expect(result).to.equal(41.5); + }); + + it('(dynamic) evaluate interpolate expression with expression value', async () => { + const value = ['zoom']; + const result = evalExpression(['interpolate', ['linear'], value, 3, 0, 3, 10, 3, 4, 5, 5, 17, 64, 19, 19, 22, 22], ExpressionMode.dynamic); + expectExpression('interpolate', result); + }); +}); diff --git a/packages/tests/specs/display/general/polygon_render_spec.ts b/packages/tests/specs/display/general/polygon_render_spec.ts index 924f7e6fc..b28fb7ba8 100644 --- a/packages/tests/specs/display/general/polygon_render_spec.ts +++ b/packages/tests/specs/display/general/polygon_render_spec.ts @@ -56,7 +56,7 @@ describe('validate polygon rendering', function() { properties: {}, type: 'Feature' }; - layer.addFeature(f1, {'zIndex': 0, 'type': 'Polygon', 'opacity': 1, 'fill': '#000000', 'strokeWidth': 5}); + layer.addFeature(f1, [{'zIndex': 0, 'type': 'Polygon', 'opacity': 1, 'fill': '#000000', 'strokeWidth': 5}]); let f2 = { geometry: { @@ -78,7 +78,7 @@ describe('validate polygon rendering', function() { properties: {}, type: 'Feature' }; - layer.addFeature(f2, {'zIndex': 0, 'type': 'Polygon', 'opacity': 1, 'fill': '#00ffff', 'stroke': '#ff0000', 'strokeWidth': 5}); + layer.addFeature(f2, [{'zIndex': 0, 'type': 'Polygon', 'opacity': 1, 'fill': '#00ffff', 'stroke': '#ff0000', 'strokeWidth': 5}]); }); after(async function() { diff --git a/packages/tests/specs/display/general/zoom_in_20_plus.ts b/packages/tests/specs/display/general/zoom_in_20_plus.ts index 413ff2fa7..65ff9e9ef 100644 --- a/packages/tests/specs/display/general/zoom_in_20_plus.ts +++ b/packages/tests/specs/display/general/zoom_in_20_plus.ts @@ -132,12 +132,12 @@ describe('zoom in 20+', function() { [-77.0077688, 38.9011329] ] } - }, { + }, [{ zIndex: 1, type: 'Line', stroke: '#0000ff', strokeWidth: 14 - }); + }]); display.setCenter(-77.0077688, 38.9011329); diff --git a/packages/tests/specs/display/layer/setstylegroup_invalid_style_spec.ts b/packages/tests/specs/display/layer/setstylegroup_invalid_style_spec.ts index 86636a7dd..d86b82ffd 100644 --- a/packages/tests/specs/display/layer/setstylegroup_invalid_style_spec.ts +++ b/packages/tests/specs/display/layer/setstylegroup_invalid_style_spec.ts @@ -59,26 +59,6 @@ describe('setStyleGroup with invalid style', function() { }); it('style link, validate its new style with invalid value', async function() { - // set link with invalid style - linkLayer.setStyleGroup( - link, - [ - {'zIndex': 0, 'type': 'Line', 'opacity': 1, 'stroke': '#be6b65', 'strokeWidth': 16}, - 'invalid' - ] - ); - - let style = linkLayer.getStyleGroup(link); - - expect(style).to.be.deep.equal([ - {'zIndex': 0, 'type': 'Line', 'opacity': 1, 'stroke': '#be6b65', 'strokeWidth': 16}, - 'invalid' - ]); - - // validate new link style - let color1 = await getCanvasPixelColor(mapContainer, {x: 350, y: 300}); // get link color - expect(color1).to.equal('#be6b65'); - // set link style again linkLayer.setStyleGroup( link, @@ -88,7 +68,7 @@ describe('setStyleGroup with invalid style', function() { ] ); - style = linkLayer.getStyleGroup(link); + let style = linkLayer.getStyleGroup(link); expect(style).to.be.deep.equal([ {'zIndex': 0, 'type': 'Line', 'opacity': 1, 'stroke': '#be6b65', 'strokeWidth': 16}, @@ -102,26 +82,6 @@ describe('setStyleGroup with invalid style', function() { it('style address, validate its new style with invalid value', async function() { - // set address with invalid style - addressLayer.setStyleGroup( - address, - [ - {'zIndex': 1, 'type': 'Rect', 'width': 16, 'height': 16, 'opacity': 1, 'fill': '#765432'}, - 'invalid' - ] - ); - - let style = addressLayer.getStyleGroup(address); - - expect(style).to.be.deep.equal([ - {'zIndex': 1, 'type': 'Rect', 'width': 16, 'height': 16, 'opacity': 1, 'fill': '#765432'}, - 'invalid' - ]); - - // validate new address style - let color1 = await getCanvasPixelColor(mapContainer, {x: 300, y: 200}); // get address color - expect(color1).to.equal('#765432'); - // set address style again addressLayer.setStyleGroup( address, @@ -131,7 +91,7 @@ describe('setStyleGroup with invalid style', function() { ] ); - style = addressLayer.getStyleGroup(address); + let style = addressLayer.getStyleGroup(address); expect(style).to.be.deep.equal([ {'zIndex': 1, 'type': 'Rect', 'width': 16, 'height': 16, 'opacity': 1, 'fill': '#765432'}, diff --git a/packages/tests/specs/display/layer/setstylegroup_point_spec.ts b/packages/tests/specs/display/layer/setstylegroup_point_spec.ts index 563aa730f..ad79eae3c 100644 --- a/packages/tests/specs/display/layer/setstylegroup_point_spec.ts +++ b/packages/tests/specs/display/layer/setstylegroup_point_spec.ts @@ -21,11 +21,12 @@ import {waitForViewportReady} from 'displayUtils'; import {getCanvasPixelColor, prepare} from 'utils'; import {Map} from '@here/xyz-maps-display'; import dataset from './setstylegroup_point_spec.json'; +import {TileLayer} from '@here/xyz-maps-core'; describe('setStyleGroup Point', function() { const expect = chai.expect; - let paLayer; + let paLayer: TileLayer; let display; let mapContainer; let feature1; @@ -61,12 +62,12 @@ describe('setStyleGroup Point', function() { // set style for the added feature paLayer.setStyleGroup( feature1, - {'zIndex': 0, 'type': 'Rect', 'width': 16, 'height': 16, 'opacity': 1, 'stroke': '#be6b65', 'strokeWidth': 16} + [{'zIndex': 0, 'type': 'Rect', 'width': 16, 'height': 16, 'opacity': 1, 'stroke': '#be6b65', 'strokeWidth': 16}] ); paLayer.setStyleGroup( feature2, - {'zIndex': 0, 'type': 'Rect', 'width': 16, 'height': 16, 'opacity': 1, 'stroke': '#be6b65', 'strokeWidth': 16} + [{'zIndex': 0, 'type': 'Rect', 'width': 16, 'height': 16, 'opacity': 1, 'stroke': '#be6b65', 'strokeWidth': 16}] ); // validate features have new style diff --git a/packages/tests/specs/display/layer/setstylegroup_point_with_same_zindex.ts b/packages/tests/specs/display/layer/setstylegroup_point_with_same_zindex.ts index 122320d13..d930baabd 100644 --- a/packages/tests/specs/display/layer/setstylegroup_point_with_same_zindex.ts +++ b/packages/tests/specs/display/layer/setstylegroup_point_with_same_zindex.ts @@ -21,12 +21,13 @@ import {waitForViewportReady} from 'displayUtils'; import {getCanvasPixelColor, prepare} from 'utils'; import {Map} from '@here/xyz-maps-display'; import dataset from './setstylegroup_point_with_same_zindex.json'; +import {TileLayer} from '@here/xyz-maps-core'; describe('setStyleGroup point with same zIndex', function() { const expect = chai.expect; - let buildingLayer; - let paLayer; + let buildingLayer: TileLayer; + let paLayer: TileLayer; let display; let mapContainer; let feature; @@ -201,7 +202,7 @@ describe('setStyleGroup point with same zIndex', function() { // style build as background color buildingLayer.setStyleGroup( building, - {'zIndex': 0, 'type': 'Polygon', 'fill': '#000000', 'stroke': '#000000'} + [{'zIndex': 0, 'type': 'Polygon', 'fill': '#000000', 'stroke': '#000000'}] ); // set style for the added feature with same values @@ -223,7 +224,7 @@ describe('setStyleGroup point with same zIndex', function() { // style build as background color buildingLayer.setStyleGroup( building, - {'zIndex': 0, 'type': 'Polygon', 'fill': '#000000', 'stroke': '#000000'} + [{'zIndex': 0, 'type': 'Polygon', 'fill': '#000000', 'stroke': '#000000'}] ); // set style for the added feature with different opacity @@ -245,7 +246,7 @@ describe('setStyleGroup point with same zIndex', function() { // style build as background color buildingLayer.setStyleGroup( building, - {'zIndex': 0, 'type': 'Polygon', 'fill': '#000000', 'stroke': '#000000'} + [{'zIndex': 0, 'type': 'Polygon', 'fill': '#000000', 'stroke': '#000000'}] ); // set style for the added feature with different opacity and zIndex diff --git a/packages/tests/specs/display/layer/setstylegroup_polygon_spec.ts b/packages/tests/specs/display/layer/setstylegroup_polygon_spec.ts index a65a7f810..3507090c5 100644 --- a/packages/tests/specs/display/layer/setstylegroup_polygon_spec.ts +++ b/packages/tests/specs/display/layer/setstylegroup_polygon_spec.ts @@ -22,6 +22,7 @@ import {getCanvasPixelColor, prepare} from 'utils'; import {Map} from '@here/xyz-maps-display'; import dataset from './setstylegroup_polygon_spec.json'; import chaiAlmost from 'chai-almost'; +import {TileLayer} from '@here/xyz-maps-core'; const IMG_SRC_RED = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAE0lEQVR42mP8z8AARLgB48hQAAAdEw/5itjcFgAAAABJRU5ErkJggg=='; const IMG_SRC_BLUE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAE0lEQVR42mNkYPj/nwEPYBwZCgANIw/5um5PVwAAAABJRU5ErkJggg=='; @@ -32,7 +33,7 @@ const FONT = 'bold 30px Arial,Helvetica,sans-serif'; describe('setStyleGroup Polygon', () => { const expect = chai.expect; - let buildingLayer; + let buildingLayer: TileLayer; let display; let mapContainer; let feature; diff --git a/packages/tests/specs/display/layer/stylegroup_line_spec.ts b/packages/tests/specs/display/layer/stylegroup_line_spec.ts index 7b7bdc603..58c2c9abb 100644 --- a/packages/tests/specs/display/layer/stylegroup_line_spec.ts +++ b/packages/tests/specs/display/layer/stylegroup_line_spec.ts @@ -21,12 +21,13 @@ import {waitForViewportReady} from 'displayUtils'; import {getCanvasPixelColor, prepare} from 'utils'; import {Map} from '@here/xyz-maps-display'; import dataset from './stylegroup_line_spec.json'; +import {TileLayer} from '@here/xyz-maps-core'; describe('setStyleGroup Line', () => { const {expect} = chai; let preparedData; - let linkLayer; + let linkLayer: TileLayer; let display; let mapContainer; let feature; @@ -61,7 +62,7 @@ describe('setStyleGroup Line', () => { // set link style linkLayer.setStyleGroup( feature, - {'zIndex': 0, 'type': 'Line', 'opacity': 1, 'stroke': '#be6b65', 'strokeWidth': 16} + [{'zIndex': 0, 'type': 'Line', 'opacity': 1, 'stroke': '#be6b65', 'strokeWidth': 16}] ); // validate new link style diff --git a/packages/tests/specs/editor/link/link_get_functions_spec.ts b/packages/tests/specs/editor/link/link_get_functions_spec.ts index b9d450180..8d1c1a18c 100644 --- a/packages/tests/specs/editor/link/link_get_functions_spec.ts +++ b/packages/tests/specs/editor/link/link_get_functions_spec.ts @@ -68,8 +68,11 @@ describe('Link getters return correct value', function() { pedestrianOnly: true }); expect(link.getZLevels()).to.deep.equal([0, 0]); - expect(link.style()).to.deep.equal([ - {zIndex: 0, type: 'Line', strokeWidth: 10, stroke: '#ff0000'} - ]); + expect(link.style()[0]).to.deep.include({ + zIndex: 0, + type: 'Line', + strokeWidth: 10, + stroke: '#ff0000' + }); }); });