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 = <number[]><unknown>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<Expression, any> & { 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<T>(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 <len; i++) { + let value = this.operand(i, context); + if (typeof value == type) return value; + } + throw new TypeOfExpressionError('expected type: ' + type); + } +} + +export class NumberExpression extends TypeOfExpression { + static operator = 'number'; +} + +export class BooleanExpression extends TypeOfExpression { + static operator = 'boolean'; +} + + +class ToNumberExpressionError extends Error { + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, ToNumberExpressionError.prototype); + this.name = this.constructor.name; + } +} +export class ToNumberExpression extends Expression { + static operator = 'to-number'; + + eval(context) { + const {json} = this; + for (let i = 1; i < json.length; i++) { + let value = this.operand(i, context); + value = Number(value); + if (!isNaN(value)) return value; + } + throw new ToNumberExpressionError('convert error: ' + json); + } +} + +export class ToBooleanExpression extends Expression { + static operator = 'to-boolean'; + + eval(context) { + const {json} = this; + let value = this.operand(1, context); + return Boolean(value); + } +} diff --git a/packages/common/src/JSUtils.ts b/packages/common/src/JSUtils.ts index e26d63450..53f68692b 100644 --- a/packages/common/src/JSUtils.ts +++ b/packages/common/src/JSUtils.ts @@ -59,34 +59,25 @@ const mixin = (to, from) => { 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 (<any>global).HERE = (<any>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<GeometryType = string> implements GeoJSONFeature<GeometryTy getBBox(): GeoJSONBBox { return this._provider.decBBox(this); }; + + getDataSourceLayer(layer: TileLayer) { + return layer.name ?? layer.id; + } } Feature.prototype.type = 'Feature'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1e91ded81..a8575ddd0 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -34,6 +34,7 @@ export * from './styles/SphereStyle'; export * from './styles/TextStyle'; export * from './styles/VerticalLineStyle'; export * from './styles/HeatmapStyle'; +export * from './styles/XYZLayerStyle'; export * from './layers/MVTLayer'; export * from './layers/CustomLayer'; export * from './layers/Layer'; diff --git a/packages/core/src/layers/CustomLayer.ts b/packages/core/src/layers/CustomLayer.ts index 7ec69a0b9..19c886a1c 100644 --- a/packages/core/src/layers/CustomLayer.ts +++ b/packages/core/src/layers/CustomLayer.ts @@ -18,6 +18,7 @@ */ import {Layer} from './Layer'; +import {LayerStyle} from '../styles/LayerStyle'; /** * Options to configure the CustomLayer. @@ -192,7 +193,7 @@ export class CustomLayer extends Layer { } - getStyle() { - return this.style; + getStyle(): LayerStyle { + return this.style as LayerStyle; } } diff --git a/packages/core/src/layers/TileLayer.ts b/packages/core/src/layers/TileLayer.ts index b9ff696c2..1abaca8e5 100644 --- a/packages/core/src/layers/TileLayer.ts +++ b/packages/core/src/layers/TileLayer.ts @@ -19,7 +19,7 @@ import {Listener as Listeners} from '@here/xyz-maps-common'; import defaultStylesDef from '../styles/default'; -import LayerStyleImpl from '../styles/LayerStyleImpl'; +import {XYZLayerStyle} from '../styles/XYZLayerStyle'; import {LayerStyle, Style, StyleGroup} from '../styles/LayerStyle'; @@ -61,7 +61,7 @@ export class TileLayer extends Layer { private _fp: FeatureProvider; - private _sd = null; + private _sd: XYZLayerStyle = null; // pointer events active private _pev = true; @@ -345,11 +345,11 @@ export class TileLayer extends Layer { */ setStyleGroup(feature: Feature, styleGroup?: Style[] | false | null): void; - setStyleGroup(feature, style?, merge?) { + setStyleGroup(feature, styleGroup?, merge?) { if (this._sd) { this.dispatchEvent(STYLEGROUP_CHANGE_EVENT, { feature, - styleGroup: this._sd.setStyleGroup(feature, style, merge) + styleGroup: this._sd.setStyleGroup(feature, styleGroup, merge) }); } }; @@ -361,7 +361,7 @@ export class TileLayer extends Layer { * @param zoomlevel - specify the zoomlevel for the feature style * */ - getStyleGroup(feature: Feature, zoomlevel?: number, layerDefault?: boolean): Style[] { + getStyleGroup(feature: Feature, zoomlevel?: number, layerDefault?: boolean): readonly Style[] { return this._sd?.getStyleGroup(feature, zoomlevel, layerDefault); }; @@ -707,19 +707,25 @@ export class TileLayer extends Layer { * @param layerStyle - the layerStyle * @param keepCustom - keep and reuse custom set feature styles that have been set via layer.setStyleGroup(...) */ - setStyle(layerStyle: LayerStyle, keepCustom: boolean = false) { - const isFnc = (fnc) => 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<number> | StyleZoomRange<number>; + zIndex: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * 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<number>; + zLayer?: number | StyleValueFunction<number> | StyleExpression<number>; /** * Sets the color to fill the Box. * * @see {@link Color} for a detailed list of possible supported formats. */ - fill?: Color | StyleValueFunction<Color> | StyleZoomRange<Color>; + fill?: Color | StyleValueFunction<Color> | StyleZoomRange<Color> | StyleExpression<Color>; /** * Sets the stroke color of the Box. * * @see {@link Color} for a detailed list of possible supported formats. */ - stroke?: Color | StyleValueFunction<Color> | StyleZoomRange<Color>; + stroke?: Color | StyleValueFunction<Color> | StyleZoomRange<Color> | StyleExpression<Color>; /** * Sets the width of the stroke. @@ -76,7 +76,7 @@ export interface BoxStyle { * } * ``` */ - strokeWidth?: number | string | StyleValueFunction<number | number> | StyleZoomRange<string | number>; + strokeWidth?: number | string | StyleValueFunction<number | number> | StyleZoomRange<string | number> | StyleExpression<number | string>; /** * Defines the opacity of the style. @@ -84,7 +84,7 @@ export interface BoxStyle { * * @defaultValue 1 */ - opacity?: number | StyleValueFunction<number> | StyleZoomRange<number>; + opacity?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * The Width of the Box. @@ -101,7 +101,7 @@ export interface BoxStyle { * } * ``` */ - width: number | StyleValueFunction<number> | StyleZoomRange<number>; + width: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * The Height of the Box. @@ -120,7 +120,7 @@ export interface BoxStyle { * } * ``` */ - height?: number | StyleValueFunction<number> | StyleZoomRange<number>; + height?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * The depth of the Box. @@ -141,7 +141,7 @@ export interface BoxStyle { * } * ``` */ - depth?: number | StyleValueFunction<number> | StyleZoomRange<number>; + depth?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * 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<number | string> | StyleZoomRange<number | string>; + offsetX?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string> | StyleExpression<number | string>; /** * 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<number> | StyleZoomRange<number>; + offsetY?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * 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<number | string> | StyleZoomRange<number | string>; + offsetZ?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string> | StyleExpression<number | string>; /** * The altitude of the center of the Box in meters. @@ -199,7 +199,7 @@ export interface BoxStyle { * * @experimental */ - altitude?: number | boolean | StyleValueFunction<number | boolean> | StyleZoomRange<number | boolean> + altitude?: number | boolean | StyleValueFunction<number | boolean> | StyleZoomRange<number | boolean> | StyleExpression<number | boolean>; /** * Scales the size of a style based on the feature's altitude. @@ -212,5 +212,5 @@ export interface BoxStyle { * * @experimental */ - scaleByAltitude?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean> + scaleByAltitude?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean> | StyleExpression<boolean>; } 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<number> | StyleZoomRange<number>; + zIndex: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * 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<number>; + zLayer?: number | StyleValueFunction<number> | StyleExpression<number>; /** * Sets the color to fill the Circle. * * @see {@link Color} for a detailed list of possible supported formats. */ - fill?: Color | StyleValueFunction<Color> | StyleZoomRange<Color>; + fill?: Color | StyleValueFunction<Color> | StyleZoomRange<Color> | StyleExpression<Color>; /** * Sets the stroke color of the Circle. * * @see {@link Color} for a detailed list of possible supported formats. */ - stroke?: Color | StyleValueFunction<Color> | StyleZoomRange<Color>; + stroke?: Color | StyleValueFunction<Color> | StyleZoomRange<Color> | StyleExpression<Color>; /** * Sets the width of the stroke. @@ -75,7 +75,7 @@ export interface CircleStyle { * } * ``` */ - strokeWidth?: number | string | StyleValueFunction<number | number> | StyleZoomRange<string | number>; + strokeWidth?: number | string | StyleValueFunction<number | number> | StyleZoomRange<string | number> | StyleExpression<string | number>; /** * 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<number> | StyleZoomRange<number>; + opacity?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * The Radius of the Circle. @@ -108,7 +108,7 @@ export interface CircleStyle { * } * ``` */ - radius: number | StyleValueFunction<number> | StyleZoomRange<number>; + radius: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string> | StyleExpression<number | string>; /** * 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<number | string> | StyleZoomRange<number | string>; + offsetX?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string> | StyleExpression<string | number>; /** * 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<number> | StyleZoomRange<number>; + offsetY?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * 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<number | string> | StyleZoomRange<number | string>; + offsetZ?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string> | StyleExpression<string | number>; /** * 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<string> | StyleZoomRange<string>; + alignment?: 'map' | 'viewport' | StyleValueFunction<string> | StyleZoomRange<string> | StyleExpression<string>; /** * 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<string> | StyleZoomRange<string> | StyleExpression<string>; /** * 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<boolean> | StyleZoomRange<boolean> | StyleExpression<boolean>; /** * Enable or disable collision detection. @@ -192,12 +192,12 @@ export interface CircleStyle { * * @defaultValue true. */ - collide?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean>; + collide?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean> | StyleExpression<boolean>; /** * 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<string> | StyleZoomRange<string> + collisionGroup?: string | StyleValueFunction<string> | StyleZoomRange<string> | StyleExpression<string>; /** * Minimum distance in pixels between repeated style-groups on line geometries. @@ -205,7 +205,7 @@ export interface CircleStyle { * * @defaultValue 256 (pixels) */ - repeat?: number | StyleValueFunction<number> | StyleZoomRange<number>; + repeat?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * The altitude of the style in meters. @@ -218,7 +218,7 @@ export interface CircleStyle { * * @experimental */ - altitude?: number | boolean | StyleValueFunction<number | boolean> | StyleZoomRange<number | boolean> + altitude?: number | boolean | StyleValueFunction<number | boolean> | StyleZoomRange<number | boolean> | StyleExpression<number | boolean>; /** @@ -232,5 +232,5 @@ export interface CircleStyle { * * @experimental */ - scaleByAltitude?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean> + scaleByAltitude?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean> | StyleExpression<boolean>; } 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<number> | StyleZoomRange<number>; + zIndex: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * 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<number>; + zLayer?: number | StyleValueFunction<number> | StyleExpression<number>; /** * Specifies the URL of an image. * It can be either absolute or relative path. * It is only required by "Image". */ - src?: string | StyleValueFunction<string> | StyleZoomRange<string>; + src?: string | StyleValueFunction<string> | StyleZoomRange<string> | StyleExpression<string>; /** * 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<Color> | StyleZoomRange<Color> | LinearGradient; + fill?: Color | StyleValueFunction<Color> | StyleZoomRange<Color> | LinearGradient | StyleExpression<Color>; /** * 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<Color> | StyleZoomRange<Color>; + stroke?: Color | StyleValueFunction<Color> | StyleZoomRange<Color> | StyleExpression<Color>; /** * Sets the width of the stroke. @@ -144,7 +145,7 @@ export interface Style { * } * ``` */ - strokeWidth?: number | string | StyleValueFunction<number | number> | StyleZoomRange<string | number>; + strokeWidth?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string> | StyleExpression<number | string>; /** * 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<string> | StyleZoomRange<string>; + strokeLinecap?: string | StyleValueFunction<string> | StyleZoomRange<string> | StyleExpression<string>; /** * 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<string> | StyleZoomRange<string>; + strokeLinejoin?: string | StyleValueFunction<string> | StyleZoomRange<string> | StyleExpression<string>; /** * 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<number> | StyleZoomRange<number>; + opacity?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * The Radius of the Circle and Sphere. @@ -231,7 +232,7 @@ export interface Style { * } * ``` */ - radius?: number | StyleValueFunction<number> | StyleZoomRange<number>; + radius?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Width of the style in pixels. @@ -260,7 +261,7 @@ export interface Style { * } * ``` */ - width?: number | StyleValueFunction<number> | StyleZoomRange<number>; + width?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Height of the style in pixels. @@ -301,7 +302,7 @@ export interface Style { * } * ``` */ - height?: number | StyleValueFunction<number> | StyleZoomRange<number>; + height?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * The depth of the style in pixels. @@ -321,7 +322,7 @@ export interface Style { * } * ``` */ - depth?: number | StyleValueFunction<number> | StyleZoomRange<number>; + depth?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * CSS font string for texts. @@ -329,7 +330,7 @@ export interface Style { * * @defaultValue “normal 12px Arial” */ - font?: string | StyleValueFunction<string> | StyleZoomRange<string>; + font?: string | StyleValueFunction<string> | StyleZoomRange<string> | StyleExpression<string>; /** * 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<string | number | boolean> | StyleZoomRange<string | number | boolean>; + text?: string | number | boolean | StyleValueFunction<string | number | boolean> | StyleZoomRange<string | number | boolean> | StyleExpression<string | number | boolean>; /** * "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<string> | StyleZoomRange<string>; + textRef?: string | StyleValueFunction<string> | StyleZoomRange<string> | StyleExpression<string>; /** * 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<number> | StyleZoomRange<number>; + from?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * 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<number> | StyleZoomRange<number>; + to?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * 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<number | string> | StyleZoomRange<number | string>; + offsetX?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string> | StyleExpression<number | string>; /** * 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<number> | StyleZoomRange<number>; + offsetY?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<string>; /** * 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<number | string> | StyleZoomRange<number | string>; + offsetZ?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string> | StyleExpression<number | string>; /** * 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<number | string> | StyleZoomRange<number | string>; + offset?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string> | StyleExpression<number | string>; /** * 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<string> | StyleZoomRange<string>; + alignment?: 'map' | 'viewport' | StyleValueFunction<string> | StyleZoomRange<string> | StyleExpression<string>; /** * Rotate the shape of the style to the angle in degrees. * This attribute is validate for Rect and Image. */ - rotation?: number | StyleValueFunction<number> | StyleZoomRange<number>; + rotation?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * 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<number> | StyleZoomRange<number>; + priority?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Minimum distance in pixels between repeated style-groups on line geometries. @@ -485,7 +486,7 @@ export interface Style { * * @defaultValue 256 (pixels) */ - repeat?: number | StyleValueFunction<number> | StyleZoomRange<number>; + repeat?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Enable oder Disable line wrapping for styles of type "Text". @@ -498,7 +499,7 @@ export interface Style { * * @defaultValue 14 */ - lineWrap?: number | StyleValueFunction<number> | StyleZoomRange<number>; + lineWrap?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * 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<string> | StyleZoomRange<string> | StyleExpression<string>; /** * 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<boolean> | StyleZoomRange<boolean> | StyleExpression<boolean>; /** * Enable or disable collision detection. @@ -536,18 +537,18 @@ export interface Style { * * @defaultValue false for "Text", true for all other. */ - collide?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean>; + collide?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean> | StyleExpression<boolean>; /** * 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<string> | StyleZoomRange<string> + collisionGroup?: string | StyleValueFunction<string> | StyleZoomRange<string> | StyleExpression<string>; /** * Extrude a Polygon or MultiPolygon geometry in meters. * This attribute is validate for styles of type "Polygon" only. */ - extrude?: number | StyleValueFunction<number> | StyleZoomRange<number>; + extrude?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * The base of the Extrude in meters. @@ -557,7 +558,7 @@ export interface Style { * * @defaultValue 0 */ - extrudeBase?: number | StyleValueFunction<number> | StyleZoomRange<number>; + extrudeBase?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * The altitude of the style in meters. @@ -571,7 +572,7 @@ export interface Style { * * @experimental */ - altitude?: number | boolean | StyleValueFunction<number | boolean> | StyleZoomRange<number | boolean> + altitude?: number | boolean | StyleValueFunction<number | boolean> | StyleZoomRange<number | boolean> | StyleExpression<number | boolean>; /** @@ -585,5 +586,34 @@ export interface Style { * * @experimental */ - scaleByAltitude?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean> + scaleByAltitude?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean> | StyleExpression<boolean>; + + /** + * 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<boolean> | StyleExpression<boolean>; } 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<number> | StyleZoomRange<number>; + zIndex: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * 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<number>; + zLayer?: number | StyleValueFunction<number> | StyleExpression<number>; /** * The radius in pixels with which to render a single point of the heatmap. * * @defaultValue 24 */ - radius?: number | StyleValueFunction<number> | StyleZoomRange<number>; + radius?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * The fill color is a linear gradient used to colorize the heatmap. @@ -103,7 +103,7 @@ export interface HeatmapStyle { * * @defaultValue 1 */ - weight?: number | StyleValueFunction<number>; + weight?: number | StyleValueFunction<number> | StyleExpression<number>; /** * 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<number>; + intensity?: number | StyleZoomRange<number> | StyleExpression<number>; /** * Defines the global opacity of the heatmap. @@ -119,5 +119,5 @@ export interface HeatmapStyle { * * @defaultValue 1 */ - opacity?: number | StyleValueFunction<number> | StyleZoomRange<number>; + opacity?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; } 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<number> | StyleZoomRange<number>; + zIndex: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * 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<number>; + zLayer?: number | StyleValueFunction<number> | StyleExpression<number>; /** * Specifies the URL of the image to render. * It can be either absolute or relative path. */ - src: string | StyleValueFunction<string> | StyleZoomRange<string>; + src: string | StyleValueFunction<string> | StyleZoomRange<string> | StyleExpression<string>; /** * 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<number> | StyleZoomRange<number>; + opacity?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Width of the Image in pixels. @@ -80,7 +80,7 @@ export interface ImageStyle { * } * ``` */ - width: number | StyleValueFunction<number> | StyleZoomRange<number>; + width: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Height of the Image in pixels. @@ -100,7 +100,7 @@ export interface ImageStyle { * } * ``` */ - height?: number | StyleValueFunction<number> | StyleZoomRange<number>; + height?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Offset the shape in pixels on x-axis. @@ -114,7 +114,7 @@ export interface ImageStyle { * * ``` */ - offsetX?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string>; + offsetX?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string> | StyleExpression<number | string>; /** * 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<number> | StyleZoomRange<number>; + offsetY?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * 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<number | string> | StyleZoomRange<number | string>; + offsetZ?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string> | StyleExpression<number | string>; /** * 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<string> | StyleZoomRange<string>; + alignment?: 'map' | 'viewport' | StyleValueFunction<string> | StyleZoomRange<string> | StyleExpression<string>; /** * Rotate the shape of the style to the angle in degrees. */ - rotation?: number | StyleValueFunction<number> | StyleZoomRange<number>; + rotation?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * 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<number> | StyleZoomRange<number>; + priority?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Minimum distance in pixels between repeated style-groups on line geometries. @@ -168,7 +168,7 @@ export interface ImageStyle { * * @defaultValue 256 (pixels) */ - repeat?: number | StyleValueFunction<number> | StyleZoomRange<number>; + repeat?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * 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<string> | StyleZoomRange<string> | StyleExpression<string>; /** * 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<boolean> | StyleZoomRange<boolean> | StyleExpression<boolean>; /** * Enable or disable collision detection. @@ -204,12 +204,12 @@ export interface ImageStyle { * * @defaultValue false for "Text", true for all other. */ - collide?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean>; + collide?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean> | StyleExpression<boolean>; /** * 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<string> | StyleZoomRange<string> + collisionGroup?: string | StyleValueFunction<string> | StyleZoomRange<string> | StyleExpression<string>; /** * The altitude of the style in meters. @@ -222,7 +222,7 @@ export interface ImageStyle { * * @experimental */ - altitude?: number | boolean | StyleValueFunction<number | boolean> | StyleZoomRange<number | boolean> + altitude?: number | boolean | StyleValueFunction<number | boolean> | StyleZoomRange<number | boolean> | StyleExpression<number | boolean>; /** * Scales the size of a style based on the feature's altitude. @@ -235,5 +235,5 @@ export interface ImageStyle { * * @experimental */ - scaleByAltitude?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean> + scaleByAltitude?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean> | StyleExpression<boolean>; } 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<ResultType = any> = [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<Type> = (feature: Feature, zoom: number) => Type * } * ``` */ -export type StyleZoomRange<Type> = { [zoom: number]: Type } +export type StyleZoomRange<Type> = { [zoom: number|string]: Type } export {Style}; // /** @@ -72,10 +227,13 @@ export {Style}; export type StyleGroupMap = { [id: string]: StyleGroup } -export type StyleGroup = Array<Style>; +export type StyleGroup = Style[]; + +// type StrictExclude<T, U> = T extends U ? U extends T ? never : T : T; +export type ParsedStyleProperty<S> = Exclude<S, StyleExpression<any> | StyleValueFunction<any> | StyleZoomRange<any>>; -// (<LineStyle>testStyle[0]).stroke; +var test: ParsedStyleProperty<number|StyleZoomRange<number>>; /** * The Color is an RGBA color value representing RED, GREEN, and BLUE light sources with an optional alpha channel. @@ -110,6 +268,11 @@ export type Color = string | number | [number, number, number, number]; */ export interface LayerStyle { + /** + * Option LayerStyle definitions that can be references and reused by {@link Style|Styles} within the Layer. + */ + definitions?: { [definitionName: string]: boolean | number | StyleExpression | any[] | null }; + /** * @deprecated define strokeWidth style property using a "StyleZoomRange" value instead. * @hidden @@ -132,10 +295,12 @@ export interface LayerStyle { * This function will be called for each feature being rendered by the display. * The display expects this method to return the key for the styleGroup of how the feature should be rendered for the respective zoomlevel. * + * If `assign` is not defined, the {@link Style.filter} property must be used to determine whether the feature should be rendered. + * * @param feature - the feature to which style is applied * @param zoomlevel - the zoomlevel of the tile the feature should be rendered in * * @returns the key/identifier of the styleGroup in the styleGroupMap, or null/undefined if the feature should not be rendered. */ - assign: (feature: Feature, zoomlevel: number) => string | null | undefined; + assign?: (feature: Feature, zoomlevel: number) => string | null | undefined; } diff --git a/packages/core/src/styles/LayerStyleImpl.ts b/packages/core/src/styles/LayerStyleImpl.ts deleted file mode 100644 index 3ff1a755f..000000000 --- a/packages/core/src/styles/LayerStyleImpl.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* - * 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 - */ - -import {Feature} from '../features/Feature'; -import {StyleGroup, StyleGroupMap} from '../styles/LayerStyle'; - -const isTypedArray = (() => { - const TypedArray = Object.getPrototypeOf(Uint8Array); - return (obj) => obj instanceof TypedArray; -})(); - -function mixin(to, from) { - for (const f in from) { - to[f] = from[f]; - } - return to; -} - -const EMPTY_STYLE = []; - -let UNDEF; - - -class LayerStyleImpl { - styleGroups = null; - private _c: StyleGroupMap = null; - - constructor(styleCfg, customStyles?: StyleGroupMap) { - const layerStyle = this; - const deepCopy = (from, to?) => { - if (typeof from !== 'object' || from === null) { - return from; - } - if (!to) { - if (Array.isArray(from) || isTypedArray(from)) { - return from.slice(); - } else { - to = {}; - } - } - // to = to || (Array.isArray(from) ? [] : {}); - for (let key in from) { - to[key] = deepCopy(from[key]); - } - return to; - }; - for (let name in styleCfg) { - this[name] = name == 'styleGroups' - ? deepCopy(styleCfg.styleGroups, {}) - : styleCfg[name]; - } - // custom styles - layerStyle._c = customStyles || {}; - // layerStyle._l = layer; - } - - // default: simple assignment based on geometryType. - assign(feature: Feature, level?: number): string | null | undefined { - return feature.geometry.type; - }; - - // get : function( feature, level ) - // { - // return this.styleGroups[ - // this.assign( feature, level ) - // ]; - // }, - - getCustomStyleGroup(feature: Feature) { - return this._c[feature.id]; - } - - getStyleGroup(feature: Feature, level?: number, getDefault?: boolean) { - let style = this._c[feature.id]; - if (style == UNDEF || getDefault) { - (<{}>style) = this.assign(feature, level); - - if (typeof style != 'object') { - style = this.styleGroups[style]; - } - } - return style; - }; - - setStyleGroup(feature: Feature, group: StyleGroup | false | null, merge?: boolean) { - const id = feature.id; - const custom = this._c; - - if ( - group && (merge /* || merge == UNDEF*/) - ) { - group = this.merge(this.getStyleGroup(feature), group); - } - - if (group) { - custom[id] = group; - } else { - if (group === null || group === false) { - group = custom[id] = EMPTY_STYLE; - } else if (custom[id]) { - delete custom[id]; - } - } - - return group; - - // if( layer = this._l ) - // { - // layer._l.trigger( 'style', [ feature, group, layer ], true ); - // } - }; - - merge(grp1: StyleGroup, grp2: StyleGroup | false) { - if (grp2 === null || grp2 === false) { - return null; - } - - const mergedGroups = []; - let group; - - for (let i = 0, len = grp1.length; i < len; i++) { - group = mixin({}, grp1[i]); - - if (grp2[i]) { - mixin(group, grp2[i]); - } - - mergedGroups[i] = group; - } - - return mergedGroups; - } -} - -export default LayerStyleImpl; diff --git a/packages/core/src/styles/LineStyle.ts b/packages/core/src/styles/LineStyle.ts index 1b69efae6..c29222b87 100644 --- a/packages/core/src/styles/LineStyle.ts +++ b/packages/core/src/styles/LineStyle.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'; /** * Interface for configuring the visual appearance of Lines. @@ -37,7 +37,7 @@ export interface LineStyle { * * @example \{...zLayer: 2, zIndex: 5\} will be rendered on top of \{...zLayer: 1, zIndex: 10\} */ - zLayer?: number | StyleValueFunction<number>; + zLayer?: number | StyleValueFunction<number> | StyleExpression<number>; /** * Defines the opacity of the style. @@ -45,14 +45,14 @@ export interface LineStyle { * It is valid for all style types. * @defaultValue 1 */ - opacity?: number | StyleValueFunction<number> | StyleZoomRange<number>; + opacity?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Sets the stroke color of the Line. * * @see {@link Color} for a detailed list of possible supported formats. */ - stroke?: Color | StyleValueFunction<Color> | StyleZoomRange<Color>; + stroke?: Color | StyleValueFunction<Color> | StyleZoomRange<Color> | StyleExpression<Color>; /** * Sets the width of the line. @@ -78,7 +78,7 @@ export interface LineStyle { * } * ``` */ - strokeWidth: number | string | StyleValueFunction<number | number> | StyleZoomRange<string | number>; + strokeWidth: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string> | StyleExpression<number | string>; /** * This controls the shape of the ends of lines. there are three possible values for strokeLinecap: @@ -89,7 +89,7 @@ export interface LineStyle { * * If "strokeLinecap" is used in combination with "altitude", only "butt" is supported for "strokeLinecap". */ - strokeLinecap?: string | StyleValueFunction<string> | StyleZoomRange<string>; + strokeLinecap?: string | StyleValueFunction<string> | StyleZoomRange<string> | StyleExpression<string>; /** * The joint where the two segments in a line meet is controlled by the strokeLinejoin attribute, There are three possible values for this attribute: @@ -100,7 +100,7 @@ export interface LineStyle { * * If "strokeLinejoin" is used in combination with "altitude", the use of "round" is not supported. */ - strokeLinejoin?: string | StyleValueFunction<string> | StyleZoomRange<string>; + strokeLinejoin?: string | StyleValueFunction<string> | StyleZoomRange<string> | StyleExpression<string>; /** * The strokeDasharray attribute controls the pattern of dashes and gaps used to stroke paths. @@ -119,7 +119,7 @@ export interface LineStyle { * // 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'; /** * Specifies the URL of the image to be rendered at the positions of the dashes. @@ -138,7 +138,7 @@ export interface LineStyle { * 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<number> | StyleZoomRange<number>; + from?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Define the end position of a segment of the entire line in %. @@ -150,7 +150,7 @@ export interface LineStyle { * 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<number> | StyleZoomRange<number>; + to?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Offset a line to the left or right side in pixel or meter. @@ -168,7 +168,7 @@ export interface LineStyle { * { type: "Line", zIndex: 0, stroke:'blue', strokeWidth: 4, offset: "2m"} * ``` */ - offset?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string>; + offset?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string> | StyleExpression<number | string>; /** * The altitude of the line in meters. @@ -181,7 +181,7 @@ export interface LineStyle { * * @experimental */ - altitude?: number | boolean | StyleValueFunction<number | boolean> | StyleZoomRange<number | boolean>; + altitude?: number | boolean | StyleValueFunction<number | boolean> | StyleZoomRange<number | boolean> | StyleExpression<number | boolean>; /** * Scales the size of a style based on the feature's altitude. @@ -194,5 +194,5 @@ export interface LineStyle { * * @experimental */ - scaleByAltitude?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean>; + scaleByAltitude?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean> | StyleExpression<boolean>; } diff --git a/packages/core/src/styles/ModelStyle.ts b/packages/core/src/styles/ModelStyle.ts index 0b584a847..184257ee2 100644 --- a/packages/core/src/styles/ModelStyle.ts +++ b/packages/core/src/styles/ModelStyle.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'; type TypedArray = Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array; @@ -214,7 +214,7 @@ export interface ModelStyle { * 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<number> | StyleZoomRange<number>; + zIndex: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Indicates drawing order across multiple layers. * Styles using zLayer with a high value are rendered on top of zLayers with a low value. @@ -223,7 +223,7 @@ export interface ModelStyle { * * @example \{...zLayer: 2, zIndex: 5\} will be rendered on top of \{...zLayer: 1, zIndex: 10\} */ - zLayer?: number | StyleValueFunction<number>; + zLayer?: number | StyleValueFunction<number> | StyleExpression<number>; /** * The Model data that should be rendered. diff --git a/packages/core/src/styles/PolygonStyle.ts b/packages/core/src/styles/PolygonStyle.ts index 50827b279..90654e663 100644 --- a/packages/core/src/styles/PolygonStyle.ts +++ b/packages/core/src/styles/PolygonStyle.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'; /** * Interface for configuring the visual appearance of Polygons. @@ -33,7 +33,7 @@ export interface PolygonStyle { * 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<number> | StyleZoomRange<number>; + zIndex: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Indicates drawing order across multiple layers. @@ -43,21 +43,21 @@ export interface PolygonStyle { * * @example \{...zLayer: 2, zIndex: 5\} will be rendered on top of \{...zLayer: 1, zIndex: 10\} */ - zLayer?: number | StyleValueFunction<number>; + zLayer?: number | StyleValueFunction<number> | StyleExpression<number>; /** * Sets the color to fill the polygon. * * @see {@link Color} for a detailed list of possible supported formats. */ - fill?: Color | StyleValueFunction<Color> | StyleZoomRange<Color>; + fill?: Color | StyleValueFunction<Color> | StyleZoomRange<Color> | StyleExpression<Color>; /** * Sets the stroke color of the polygon. * * @see {@link Color} for a detailed list of possible supported formats. */ - stroke?: Color | StyleValueFunction<Color> | StyleZoomRange<Color>; + stroke?: Color | StyleValueFunction<Color> | StyleZoomRange<Color> | StyleExpression<Color>; /** * Sets the width of the stroke of the polygon (outline). @@ -77,7 +77,7 @@ export interface PolygonStyle { * } * ``` */ - strokeWidth?: number | string | StyleValueFunction<number | number> | StyleZoomRange<string | number>; + strokeWidth?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string> | StyleExpression<number | string>; /** * This controls the shape of the ends of lines. there are three possible values for strokeLinecap: @@ -88,7 +88,7 @@ export interface PolygonStyle { * * If "strokeLinecap" is used in combination with "altitude", only "butt" is supported for "strokeLinecap". */ - strokeLinecap?: string | StyleValueFunction<string> | StyleZoomRange<string>; + strokeLinecap?: string | StyleValueFunction<string> | StyleZoomRange<string> | StyleExpression<string>; /** * The joint where the two segments in a line meet is controlled by the strokeLinejoin attribute, There are three possible values for this attribute: @@ -99,7 +99,7 @@ export interface PolygonStyle { * * If "strokeLinejoin" is used in combination with "altitude", the use of "round" is not supported. */ - strokeLinejoin?: string | StyleValueFunction<string> | StyleZoomRange<string>; + strokeLinejoin?: string | StyleValueFunction<string> | StyleZoomRange<string> | StyleExpression<string>; /** * The strokeDasharray attribute controls the pattern of dashes and gaps used to stroke paths. @@ -118,7 +118,7 @@ export interface PolygonStyle { * // dash -> 10 meter, gap -> 10 pixel. * strokeDasharray: ["20m",10] || ["20m","10px"] */ - strokeDasharray?: number[] | StyleValueFunction<number[]> | StyleZoomRange<number[]> | 'none'; + strokeDasharray?: (number | string)[] | StyleValueFunction<(number | string)[]> | StyleZoomRange<(number | string)[]> | StyleExpression<(number | string)[]> | 'none'; /** * Defines the opacity of the style. @@ -126,7 +126,7 @@ export interface PolygonStyle { * It is valid for all style types. * @defaultValue 1 */ - opacity?: number | StyleValueFunction<number> | StyleZoomRange<number>; + opacity?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Define the starting position of a segment of the entire line in %. @@ -138,7 +138,7 @@ export interface PolygonStyle { * 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<number> | StyleZoomRange<number>; + from?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Define the end position of a segment of the entire line in %. @@ -150,7 +150,7 @@ export interface PolygonStyle { * 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<number> | StyleZoomRange<number>; + to?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Offset a line to the left or right side in pixel or meter. @@ -168,7 +168,7 @@ export interface PolygonStyle { * { type: "Line", zIndex: 0, stroke:'blue', strokeWidth: 4, offset: "2m"} * ``` */ - offset?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string>; + offset?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string> | StyleExpression<number | string>; /** * The altitude of the style in meters. @@ -182,12 +182,12 @@ export interface PolygonStyle { * * @experimental */ - altitude?: number | boolean | StyleValueFunction<number | boolean> | StyleZoomRange<number | boolean> + altitude?: number | boolean | StyleValueFunction<number | boolean> | StyleZoomRange<number | boolean> | StyleExpression<number | boolean>; /** * Extrude a Polygon or MultiPolygon geometry in meters. */ - extrude?: number | StyleValueFunction<number> | StyleZoomRange<number>; + extrude?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * The base of the Extrude in meters. @@ -196,5 +196,5 @@ export interface PolygonStyle { * * @defaultValue 0 */ - extrudeBase?: number | StyleValueFunction<number> | StyleZoomRange<number>; + extrudeBase?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; } diff --git a/packages/core/src/styles/RectStyle.ts b/packages/core/src/styles/RectStyle.ts index 16fe5ae70..613b222b6 100644 --- a/packages/core/src/styles/RectStyle.ts +++ b/packages/core/src/styles/RectStyle.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'; export interface RectStyle { /** @@ -30,7 +30,7 @@ export interface RectStyle { * 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<number> | StyleZoomRange<number>; + zIndex: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Indicates drawing order across multiple layers. @@ -40,21 +40,21 @@ export interface RectStyle { * * @example \{...zLayer: 2, zIndex: 5\} will be rendered on top of \{...zLayer: 1, zIndex: 10\} */ - zLayer?: number | StyleValueFunction<number>; + zLayer?: number | StyleValueFunction<number> | StyleExpression<number>; /** * Sets the color to fill the Rectangle. * * @see {@link Color} for a detailed list of possible supported formats. */ - fill?: Color | StyleValueFunction<Color> | StyleZoomRange<Color>; + fill?: Color | StyleValueFunction<Color> | StyleZoomRange<Color> | StyleExpression<Color>; /** * Sets the stroke color of the Rectangle. * * @see {@link Color} for a detailed list of possible supported formats. */ - stroke?: Color | StyleValueFunction<Color> | StyleZoomRange<Color>; + stroke?: Color | StyleValueFunction<Color> | StyleZoomRange<Color> | StyleExpression<Color>; /** * Sets the width of the Rectangle. @@ -72,7 +72,7 @@ export interface RectStyle { * } * ``` */ - strokeWidth?: number | string | StyleValueFunction<number | number> | StyleZoomRange<string | number>; + strokeWidth?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string> | StyleExpression<number | string>; /** * Defines the opacity of the style. @@ -80,12 +80,12 @@ export interface RectStyle { * It is valid for all style types. * @defaultValue 1 */ - opacity?: number | StyleValueFunction<number> | StyleZoomRange<number>; + opacity?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Rotate the Rectangle by the angle in degrees. */ - rotation?: number | StyleValueFunction<number> | StyleZoomRange<number>; + rotation?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Width of the style in pixels. @@ -112,7 +112,7 @@ export interface RectStyle { * } * ``` */ - width?: number | StyleValueFunction<number> | StyleZoomRange<number>; + width?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Height of the REctangle in pixels. @@ -143,7 +143,7 @@ export interface RectStyle { * } * ``` */ - height?: number | StyleValueFunction<number> | StyleZoomRange<number>; + height?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Offset the rectangle in pixels on x-axis. @@ -156,7 +156,7 @@ export interface RectStyle { * { type: "Rect", zIndex: 0, fill:'blue', width: 24, offsetX: "-1m"} * ``` */ - offsetX?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string>; + offsetX?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string> | StyleExpression<number | string>; /** * Offset the rectangle in pixels on y-axis. @@ -169,7 +169,7 @@ export interface RectStyle { * { type: "Rect", zIndex: 0, fill:'blue', width: 24, offsetY: "-1m"} * ``` */ - offsetY?: number | StyleValueFunction<number> | StyleZoomRange<number>; + offsetY?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Offset the rectangle in pixels on z-axis. @@ -182,7 +182,7 @@ export interface RectStyle { * { type: "Rect", zIndex: 0, fill:'blue', radius: 24, offsetZ: "1m"} * ``` */ - offsetZ?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string>; + offsetZ?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string> | StyleExpression<number | string>; /** * Alignment for styles of type "Rect". @@ -190,7 +190,7 @@ export interface RectStyle { * "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<string> | StyleZoomRange<string>; + alignment?: 'map' | 'viewport' | StyleValueFunction<string> | StyleZoomRange<string> | StyleExpression<string>; /** * Sets the anchor point for rectangle style used with Line or Polygon geometry. @@ -205,7 +205,7 @@ export interface RectStyle { * * @defaultValue For Polygon geometry the default is "Center". For Line geometry the default is "Coordinate". */ - anchor?: 'Line' | 'Coordinate' | 'Centroid' + anchor?: 'Line' | 'Coordinate' | 'Centroid' | StyleValueFunction<string> | StyleZoomRange<string> | StyleExpression<string>; /** * Enable or disable the space check for point styles on line geometries. @@ -215,7 +215,7 @@ export interface RectStyle { * * @defaultValue true */ - checkLineSpace?: boolean + checkLineSpace?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean> | StyleExpression<boolean>; /** * Enable or disable collision detection. @@ -227,12 +227,12 @@ export interface RectStyle { * * @defaultValue true */ - collide?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean>; + collide?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean> | StyleExpression<boolean>; /** * 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<string> | StyleZoomRange<string> + collisionGroup?: string | StyleValueFunction<string> | StyleZoomRange<string> | StyleExpression<string>; /** * Minimum distance in pixels between repeated style-groups on line geometries. @@ -240,7 +240,7 @@ export interface RectStyle { * * @defaultValue 256 (pixels) */ - repeat?: number | StyleValueFunction<number> | StyleZoomRange<number>; + repeat?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * The altitude of the style in meters. @@ -253,7 +253,7 @@ export interface RectStyle { * * @experimental */ - altitude?: number | boolean | StyleValueFunction<number | boolean> | StyleZoomRange<number | boolean> + altitude?: number | boolean | StyleValueFunction<number | boolean> | StyleZoomRange<number | boolean> | StyleExpression<number | boolean>; /** @@ -267,5 +267,5 @@ export interface RectStyle { * * @experimental */ - scaleByAltitude?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean> + scaleByAltitude?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean> | StyleExpression<boolean>; } diff --git a/packages/core/src/styles/SphereStyle.ts b/packages/core/src/styles/SphereStyle.ts index d889a579c..d9e1f4999 100644 --- a/packages/core/src/styles/SphereStyle.ts +++ b/packages/core/src/styles/SphereStyle.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'; /** * Interface for configuring the visual appearance of Rectangles. @@ -33,7 +33,7 @@ export interface SphereStyle { * 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<number> | StyleZoomRange<number>; + zIndex: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Indicates drawing order across multiple layers. @@ -43,7 +43,7 @@ export interface SphereStyle { * * @example \{...zLayer: 2, zIndex: 5\} will be rendered on top of \{...zLayer: 1, zIndex: 10\} */ - zLayer?: number | StyleValueFunction<number>; + zLayer?: number | StyleValueFunction<number> | StyleExpression<number>; /** * Sets the color to fill the Sphere. @@ -51,7 +51,7 @@ export interface SphereStyle { * * @see {@link Color} for a detailed list of possible supported formats. */ - fill?: Color | StyleValueFunction<Color> | StyleZoomRange<Color>; + fill?: Color | StyleValueFunction<Color> | StyleZoomRange<Color> | StyleExpression<Color>; /** * Defines the opacity of the style. @@ -59,7 +59,7 @@ export interface SphereStyle { * It is valid for all style types. * @defaultValue 1 */ - opacity?: number | StyleValueFunction<number> | StyleZoomRange<number>; + opacity?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * The Radius of the Sphere. @@ -77,7 +77,7 @@ export interface SphereStyle { * } * ``` */ - radius?: number | StyleValueFunction<number> | StyleZoomRange<number>; + radius?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Offset the shape in pixels on x-axis. @@ -90,7 +90,7 @@ export interface SphereStyle { * { type: "Sphere", zIndex: 0, fill:'blue', radius: 24, offsetX: "-1m"} * ``` */ - offsetX?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string>; + offsetX?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string> | StyleExpression<number | string>; /** * Offset the shape in pixels on y-axis. @@ -103,7 +103,7 @@ export interface SphereStyle { * { type: "Sphere", zIndex: 0, fill:'blue', radius: 24, offsetY: "-1m"} * ``` */ - offsetY?: number | StyleValueFunction<number> | StyleZoomRange<number>; + offsetY?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Offset the shape in pixels on z-axis. @@ -116,7 +116,7 @@ export interface SphereStyle { * { type: "Sphere", zIndex: 0, fill:'blue', radius: 24, offsetZ: "1m"} * ``` */ - offsetZ?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string>; + offsetZ?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string> | StyleExpression<number | string>; /** * The altitude of the style in meters. @@ -129,7 +129,7 @@ export interface SphereStyle { * * @experimental */ - altitude?: number | boolean | StyleValueFunction<number | boolean> | StyleZoomRange<number | boolean> + altitude?: number | boolean | StyleValueFunction<number | boolean> | StyleZoomRange<number | boolean> | StyleExpression<number | boolean>; /** @@ -143,5 +143,5 @@ export interface SphereStyle { * * @experimental */ - scaleByAltitude?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean> + scaleByAltitude?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean> | StyleExpression<boolean>; } diff --git a/packages/core/src/styles/TextStyle.ts b/packages/core/src/styles/TextStyle.ts index 6139bb967..deaff093a 100644 --- a/packages/core/src/styles/TextStyle.ts +++ b/packages/core/src/styles/TextStyle.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'; /** * Interface for configuring the visual appearance of Text. @@ -33,7 +33,7 @@ export interface TextStyle { * 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<number> | StyleZoomRange<number>; + zIndex: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Indicates drawing order across multiple layers. @@ -43,21 +43,21 @@ export interface TextStyle { * * @example \{...zLayer: 2, zIndex: 5\} will be rendered on top of \{...zLayer: 1, zIndex: 10\} */ - zLayer?: number | StyleValueFunction<number>; + zLayer?: number | StyleValueFunction<number> | StyleExpression<number>; /** * Sets the color to fill the text. * * @see {@link Color} for a detailed list of possible supported formats. */ - fill?: Color | StyleValueFunction<Color> | StyleZoomRange<Color>; + fill?: Color | StyleValueFunction<Color> | StyleZoomRange<Color> | StyleExpression<Color>; /** * Sets the stroke color of the text (outline). * * @see {@link Color} for a detailed list of possible supported formats. */ - stroke?: Color | StyleValueFunction<Color> | StyleZoomRange<Color>; + stroke?: Color | StyleValueFunction<Color> | StyleZoomRange<Color> | StyleExpression<Color>; /** * Sets the width of the stroke (outline) to display the text with. @@ -76,7 +76,7 @@ export interface TextStyle { * } * ``` */ - strokeWidth?: number | string | StyleValueFunction<number | number> | StyleZoomRange<string | number>; + strokeWidth?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string> | StyleExpression<number | string>; /** * Defines the opacity of the style. @@ -84,7 +84,7 @@ export interface TextStyle { * It is valid for all style types. * @defaultValue 1 */ - opacity?: number | StyleValueFunction<number> | StyleZoomRange<number>; + opacity?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * CSS font string for texts. @@ -92,7 +92,7 @@ export interface TextStyle { * * @defaultValue “normal 12px Arial” */ - font?: string | StyleValueFunction<string> | StyleZoomRange<string>; + font?: string | StyleValueFunction<string> | StyleZoomRange<string> | StyleExpression<string>; /** * Text is either a string or a function that generates the string that should be displayed. @@ -125,7 +125,7 @@ export interface TextStyle { * * @defaultValue "Center" */ - textAnchor?: 'Left' | 'Center' | 'Right' | 'Top' | 'TopLeft' | 'TopRight' | 'Bottom' | 'BottomLeft' | 'BottomRight' + textAnchor?: 'Left' | 'Center' | 'Right' | 'Top' | 'TopLeft' | 'TopRight' | 'Bottom' | 'BottomLeft' | 'BottomRight' | StyleValueFunction<string> | StyleZoomRange<string> | StyleExpression<string>; /** * "textRef" Reference to an attribute of an feature that's value should be displayed as text. @@ -144,7 +144,7 @@ export interface TextStyle { * textRef: "id" * ``` */ - textRef?: string | StyleValueFunction<string> | StyleZoomRange<string>; + textRef?: string | StyleValueFunction<string> | StyleZoomRange<string> | StyleExpression<string>; /** * Offset the text in pixels on x-axis. @@ -157,7 +157,7 @@ export interface TextStyle { * { type: "Text", zIndex: 0, fill:'blue', offsetX: "-1m", text: 'XYZ'} * ``` */ - offsetX?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string>; + offsetX?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string> | StyleExpression<number | string>; /** * Offset the text in pixels on y-axis. @@ -170,7 +170,7 @@ export interface TextStyle { * { type: "Text", zIndex: 0, fill:'blue', offsetY: "-1m", text: 'XYZ'} * ``` */ - offsetY?: number | StyleValueFunction<number> | StyleZoomRange<number>; + offsetY?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Offset the text in pixels on z-axis. @@ -183,7 +183,7 @@ export interface TextStyle { * { type: "Text", zIndex: 0, fill:'blue', text: 'XYZ', offsetZ: "1m"} * ``` */ - offsetZ?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string>; + offsetZ?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string> | StyleExpression<number | string>; /** * Alignment for styles of type "Text". @@ -191,19 +191,19 @@ export interface TextStyle { * "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<string> | StyleZoomRange<string>; + alignment?: 'map' | 'viewport' | StyleValueFunction<string> | StyleZoomRange<string> | StyleExpression<string>; /** * Rotate text around it's center in degrees. */ - rotation?: number | StyleValueFunction<number> | StyleZoomRange<number>; + rotation?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * 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<number> | StyleZoomRange<number>; + priority?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Minimum distance in pixels between repeated style-groups on line geometries. @@ -211,7 +211,7 @@ export interface TextStyle { * * @defaultValue 256 (pixels) */ - repeat?: number | StyleValueFunction<number> | StyleZoomRange<number>; + repeat?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Enable oder Disable line wrapping for styles of type "Text". @@ -224,7 +224,7 @@ export interface TextStyle { * * @defaultValue 14 */ - lineWrap?: number | StyleValueFunction<number> | StyleZoomRange<number>; + lineWrap?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Sets the anchor point for styles of type "Text" used with Line or Polygon geometry. @@ -239,7 +239,7 @@ export interface TextStyle { * * @defaultValue For Polygon geometry the default is "Center". For Line geometry the default for styles of type "Text" is "Line". */ - anchor?: 'Line' | 'Coordinate' | 'Centroid' | 'Center' + anchor?: 'Line' | 'Coordinate' | 'Centroid' | 'Center' | StyleValueFunction<string> | StyleZoomRange<string> | StyleExpression<string>; /** * Enable or disable the space check for point styles on line geometries. @@ -249,7 +249,7 @@ export interface TextStyle { * * @defaultValue true */ - checkLineSpace?: boolean + checkLineSpace?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean> | StyleExpression<boolean>; /** * Enable or disable collision detection. @@ -261,12 +261,12 @@ export interface TextStyle { * * @defaultValue false */ - collide?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean>; + collide?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean> | StyleExpression<boolean>; /** * 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<string> | StyleZoomRange<string> + collisionGroup?: string | StyleValueFunction<string> | StyleZoomRange<string> | StyleExpression<string>; /** * The altitude of the style in meters. @@ -279,7 +279,7 @@ export interface TextStyle { * * @experimental */ - altitude?: number | boolean | StyleValueFunction<number | boolean> | StyleZoomRange<number | boolean> + altitude?: number | boolean | StyleValueFunction<number | boolean> | StyleZoomRange<number | boolean> | StyleExpression<number | boolean>; /** * Scales the size of a style based on the feature's altitude. @@ -292,5 +292,5 @@ export interface TextStyle { * * @experimental */ - scaleByAltitude?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean> + scaleByAltitude?: boolean | StyleValueFunction<boolean> | StyleZoomRange<boolean> | StyleExpression<boolean>; } diff --git a/packages/core/src/styles/VerticalLineStyle.ts b/packages/core/src/styles/VerticalLineStyle.ts index c6098b86c..8e2bd67b3 100644 --- a/packages/core/src/styles/VerticalLineStyle.ts +++ b/packages/core/src/styles/VerticalLineStyle.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 VerticalLineStyle { * 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<number> | StyleZoomRange<number>; + zIndex: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Indicates drawing order across multiple layers. @@ -44,14 +44,14 @@ export interface VerticalLineStyle { * * @example \{...zLayer: 2, zIndex: 5\} will be rendered on top of \{...zLayer: 1, zIndex: 10\} */ - zLayer?: number | StyleValueFunction<number>; + zLayer?: number | StyleValueFunction<number> | StyleExpression<number>; /** * Sets the stroke color of the shape. * * @see {@link Color} for a detailed list of possible supported formats. */ - stroke?: Color | StyleValueFunction<Color> | StyleZoomRange<Color>; + stroke?: Color | StyleValueFunction<Color> | StyleZoomRange<Color> | StyleExpression<Color>; /** * Defines the opacity of the style. @@ -59,7 +59,7 @@ export interface VerticalLineStyle { * It is valid for all style types. * @defaultValue 1 */ - opacity?: number | StyleValueFunction<number> | StyleZoomRange<number>; + opacity?: number | StyleValueFunction<number> | StyleZoomRange<number> | StyleExpression<number>; /** * Offset the shape in pixels on z-axis. @@ -75,5 +75,5 @@ export interface VerticalLineStyle { * { type: "VerticalLine", zIndex: 0, stoke: 'black', offsetZ: "1m"} * ``` */ - offsetZ?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string>; + offsetZ?: number | string | StyleValueFunction<number | string> | StyleZoomRange<number | string> | StyleExpression<number | string>; } diff --git a/packages/core/src/styles/XYZLayerStyle.ts b/packages/core/src/styles/XYZLayerStyle.ts new file mode 100644 index 000000000..3a3746e8f --- /dev/null +++ b/packages/core/src/styles/XYZLayerStyle.ts @@ -0,0 +1,289 @@ +/* + * 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 + */ + +import {Feature} from '../features/Feature'; +import {Color, LayerStyle, Style, StyleGroup, StyleGroupMap, StyleValueFunction, StyleZoomRange} from '../styles/LayerStyle'; +import {ExpressionParser, JSONExpression} from '@here/xyz-maps-common'; +import {TileLayer} from '../layers/TileLayer'; +// import {JsonExpression} from '@here/xyz-maps-common'; + +const isTypedArray = (() => { + const TypedArray = Object.getPrototypeOf(Uint8Array); + return (obj) => obj instanceof TypedArray; +})(); + +function mixin(to, from) { + for (const f in from) { + to[f] = from[f]; + } + return to; +} + +const EMPTY_STYLE = []; + +let UNDEF; + +const deepCopy = (src: any) => { + if (typeof src !== 'object' || src === null) { + return src; + } + let cpy; + if (Array.isArray(src)) { + cpy = new Array(src.length); + for (let i = 0, len = src.length; i < len; i++) { + cpy[i] = deepCopy(src[i]); + } + } else if (isTypedArray(src)) { + cpy = src.slice(); + } else { + cpy = {}; + for (let key in src) { + cpy[key] = deepCopy(src[key]); + } + } + return cpy; +}; + +/** + * StyleManager + */ +export class XYZLayerStyle implements LayerStyle { + styleGroups = null; + private _c: StyleGroupMap = null; + definitions: LayerStyle['definitions']; + + backgroundColor?: Color | StyleZoomRange<Color> | ((zoomlevel: number) => Color); + + protected exprContext: { + $geometryType: string; + $layer: string; + $zoom: number; + $id: string | number; + [$name: string]: any; + } = { + $geometryType: null, + $layer: null, + $zoom: 0, + $id: null + }; + private layer: TileLayer; + private expParser: ExpressionParser; + private flatStyles?: Style[]; + private _filteredStyleGrp: StyleGroup; + + constructor(styleCfg) { + for (let p in styleCfg) { + const property = styleCfg[p]; + this[p] = p == 'styleGroups' ? deepCopy(property) : property; + } + // layerStyle._l = layer; + this.definitions ||= {}; + + this.expParser = new ExpressionParser(this.definitions, this.exprContext); + + if (!styleCfg.assign) { + const flatStyles = []; + for (let name in this.styleGroups) { + let styleGrp = this.styleGroups[name]; + if (!Array.isArray(styleGrp)) { + styleGrp = [styleGrp]; + } + for (let style of styleGrp) { + // for (let prop in style) { + // if ( ExpressionParser.isJSONExp(style[prop])) { + // style[prop] = this.createExpEvaluator(style[prop]); + // } + // } + if (style.filter) { + if (ExpressionParser.isJSONExp(style.filter)) { + style.filter = this.createExpEvaluator(style.filter); + } + flatStyles.push(style); + } + } + } + this.flatStyles = flatStyles; + + this._filteredStyleGrp = []; + } + } + + private createExpEvaluator(expr: JSONExpression) { + return (feature) => { + return this.expParser.evaluate(expr, feature.properties); + // window._measure.count('filter-calls'); + // window._measure.start('filter-eval'); + // let filtered = this.expParser.evaluate(expr, feature.properties); + // window._measure.stop('filter-eval'); + // return filtered; + }; + }; + + // default: simple assignment based on geometryType. + assign(feature: Feature, level?: number): string | null | undefined { + // return <string><unknown> this.flatStyles; + // let filteredStyleGroups = this.flatStyles.filter((style) => { + // const {filter} = style; + // return typeof filter === 'function' + // ? style.filter(feature, level) + // : ExpressionParser.isJSONExp(filter) + // ? this.expParser.evaluate(filter, feature.properties) + // : filter; + // }); + // return filteredStyleGroups.length && <string><unknown>filteredStyleGroups; + let filteredStyleGroups = this._filteredStyleGrp; + let i = 0; + for (let style of this.flatStyles) { + const {filter} = style; + + const filtered = + typeof filter === 'function' ? (style.filter as StyleValueFunction<boolean>)(feature, level) + // : ExpressionParser.isJSONExp(filter) ? this.expParser.evaluate(filter, feature.properties) + : filter; + + if (filtered) { + filteredStyleGroups[i++] = style; + } + } + + if (i > 0) { + filteredStyleGroups.length = i; + // filteredStyleGroups.__p = false; + return <string><unknown>filteredStyleGroups; + } + // return feature.geometry.type; + }; + + // get : function( feature, level ) + // { + // return this.styleGroups[ + // this.assign( feature, level ) + // ]; + // }, + + getDefinitions() { + return this.definitions; + } + + getExpressionParser() { + return this.expParser; + } + + getExpressionContext() { + return this.exprContext; + } + + getCustomStyles() { + return this._c; + } + + getCustomStyleGroup(feature: Feature) { + return this._c[feature.id]; + } + + protected initMapContext(feature: Feature, zoom: number): XYZLayerStyle['exprContext'] { + const geometryType = feature.geometry.type; + const {exprContext} = this; + exprContext.$geometryType = (geometryType == 'Point' || geometryType == 'MultiPoint') + ? 'point' + : (geometryType == 'LineString' || geometryType == 'MultiLineString') + ? 'line' + : 'polygon'; + + exprContext.$id = feature.properties.$id ?? feature.id; + exprContext.$layer = feature.getDataSourceLayer(this.layer); + + let z = exprContext.$zoom; + exprContext.$zoom = zoom; + + this.expParser.clearResultCache(); + return exprContext; + } + + getStyleGroup(feature: Feature, level?: number, getDefault?: boolean): (readonly Style[]) { + let style = this._c[feature.id]; + if (style == UNDEF || getDefault) { + // initialize the map context + this.initMapContext(feature, level); + + (<{}>style) = this.assign(feature, level); + + if (typeof style != 'object') { + style = this.styleGroups[style]; + } + } + return style; + }; + + setStyleGroup(feature: Feature, group: StyleGroup | false | null, merge?: boolean) { + const id = feature.id; + const custom = this._c; + + if ( + group && (merge /* || merge == UNDEF*/) + ) { + group = this.merge(this.getStyleGroup(feature), group); + } + + if (group) { + custom[id] = group; + } else { + if (group === null || group === false) { + group = custom[id] = EMPTY_STYLE; + } else if (custom[id]) { + delete custom[id]; + } + } + + return group; + + // if( layer = this._l ) + // { + // layer._l.trigger( 'style', [ feature, group, layer ], true ); + // } + }; + + merge(grp1: readonly Style[], grp2: StyleGroup | false) { + if (grp2 === null || grp2 === false) { + return null; + } + const mergedGroups = []; + let group; + + for (let i = 0, len = grp1.length; i < len; i++) { + group = mixin({}, grp1[i]); + if (grp2[i]) { + mixin(group, grp2[i]); + } + mergedGroups[i] = group; + } + + return mergedGroups; + } + + init(layer: TileLayer, _customFeatureStyles?: StyleGroupMap) { + this.layer = layer; + // custom styles + this._c = _customFeatureStyles || {}; + } + + clearCache() { + this.expParser.clearCache?.(); + } +} diff --git a/packages/display/src/Map.ts b/packages/display/src/Map.ts index 3503bd5f2..738b4732b 100644 --- a/packages/display/src/Map.ts +++ b/packages/display/src/Map.ts @@ -46,7 +46,6 @@ import {FlightAnimator} from './animation/FlightAnimator'; import Copyright from './ui/copyright/Copyright'; import Logo from './ui/Logo'; import {fillMap} from './displays/styleTools'; -import {toRGB} from './displays/webgl/color'; const project = webMercator; @@ -116,7 +115,6 @@ export class Map { } static fillZoomMap = fillMap; - static toRGB = toRGB; id: number; @@ -280,7 +278,7 @@ export class Map { this.setBackgroundColor(options.backgroundColor); - this._search = new Search(tigerMap, display.dpr); + this._search = new Search(tigerMap, display.layers, display.dpr); const pointerEvents = this._evDispatcher = new EventDispatcher(mapEl, tigerMap, layers, options); @@ -394,8 +392,7 @@ export class Map { display.setView(this.initViewPort(), this._s, this._rz, this._rx, this._groundResolution, this._worldSize); - // display.setView(this.initViewPort(), this.getZoomlevel(), this._s, this._rz, this._rx, this._groundResolution); - display.updateGrid(this._z, this._ox, this._oy); + display.updateGrid(this._z, this.getZoomlevel(), this._ox, this._oy); // display.setTransform(this._s, this._rz, this._rx, this._groundResolution); @@ -785,7 +782,7 @@ export class Map { if (featureInfo.id != null) { const layer = layers[featureInfo.layerIndex]; const provider = <FeatureProvider>layer.getProvider(this.getZoomlevel() ^ 0); - const feature = provider?.search && provider.search(featureInfo.id); + const feature = provider?.search?.(featureInfo.id); if (feature) { const {pointWorld} = featureInfo; @@ -1268,7 +1265,7 @@ export class Map { index = layers.length; } // initLayer(layer, index); - this._display.addLayer(layer, layer.getStyle(), index); + this._display.addLayer(layer, index, (layer as TileLayer).getStyleManager?.()); // if layer get's cleared -> refresh/re-fetch data // layer.addEventListener('clear', (ev)=>this.refresh(ev.detail.layer)); layer.addEventListener('clear', this._layerClearListener); diff --git a/packages/display/src/MapViewListener.ts b/packages/display/src/MapViewListener.ts index e9e793dea..2711dea0c 100644 --- a/packages/display/src/MapViewListener.ts +++ b/packages/display/src/MapViewListener.ts @@ -27,7 +27,7 @@ const EVENT_MVC = 'mapviewchange'; const EVENT_MVC_START = EVENT_MVC + 'start'; const EVENT_MVC_END = EVENT_MVC + 'end'; const MAPVIEWCHANGE_MS = 1e3 / 30; -let MAPVIEWCHANGE_END_DELAY_MS = 100; +let MAPVIEWCHANGE_END_DELAY_MS = 10; const SYNC = true; diff --git a/packages/display/src/displays/BasicDisplay.ts b/packages/display/src/displays/BasicDisplay.ts index 620097331..231e10b98 100644 --- a/packages/display/src/displays/BasicDisplay.ts +++ b/packages/display/src/displays/BasicDisplay.ts @@ -17,8 +17,8 @@ * License-Filename: LICENSE */ -import {global, TaskManager} from '@here/xyz-maps-common'; -import {Tile, TileLayer, CustomLayer, Color} from '@here/xyz-maps-core'; +import {global, TaskManager, Color as ColorUtils} from '@here/xyz-maps-common'; +import {Tile, TileLayer, CustomLayer, XYZLayerStyle, LayerStyle, Color, Style} from '@here/xyz-maps-core'; import {getElDimension, createCanvas} from '../DOMTools'; import {Layers, Layer, ScreenTile} from './Layers'; import FeatureModifier from './FeatureModifier'; @@ -26,10 +26,10 @@ import BasicRender from './BasicRender'; import BasicTile from './BasicTile'; import BasicBucket from './BasicBucket'; import Preview from './Preview'; -import LayerClusterer from './LayerClusterer'; import Grid, {ViewportTile} from '../Grid'; -import {createZoomRangeFunction, parseColorMap} from './styleTools'; -import {RGBA} from './webgl/color'; +import {createZoomRangeFunction, getValue, parseColorMap} from './styleTools'; + +type RGBA = ColorUtils.RGBA; const CREATE_IF_NOT_EXISTS = true; const MAX_PITCH_GRID = 60 / 180 * Math.PI; @@ -44,8 +44,6 @@ function toggleLayerEventListener(toggle: string, layer: any, listeners: any) { } } -const exclusiveTimeMS = 4; - let UNDEF; abstract class Display { @@ -76,7 +74,7 @@ abstract class Display { buckets: BasicBucket; listeners: { [event: string]: (a1?, a2?) => void }; tiles: { [tilesize: string]: ScreenTile[] }; - cluster: LayerClusterer; + grid: Grid; constructor(mapEl: HTMLElement, tileSize: number, dpr: string | number, bucketPool, tileRenderer: BasicRender, previewLookAhead: number | [number, number]) { @@ -87,13 +85,13 @@ abstract class Display { const canvas = createCanvas(mapEl, w, h, 0); display.previewer = new Preview(display, previewLookAhead); - display.cluster = new LayerClusterer(TaskManager.getInstance(), exclusiveTimeMS); display.grid = new Grid(tileSize); display.tiles = { 256: [], 512: [] }; display.render = tileRenderer; + // tileRenderer.mapContext = this.mapContext; display.tileSize = tileSize; display.buckets = bucketPool; display.layers = new Layers(); @@ -154,22 +152,21 @@ abstract class Display { return dpr < 1 ? 1 : dpr; } - addLayer(layer: TileLayer | CustomLayer, styles, index: number): boolean { + addLayer(layer: TileLayer | CustomLayer, index: number, styles?: XYZLayerStyle): boolean { const display = this; const layers = display.layers; - let added = false; - if (layers.add(layer, index)) { + let added = layers.add(layer, index); + if (added) { const dLayer = layers.get(layer); - added = true; - display.buckets.forEach((dTile) => { dTile.addLayer(index); }); - toggleLayerEventListener('add', layer, display.listeners); if (layer.custom) return added; + styles?.clearCache(); + // new function needs to be created per layer otherwise a setup with same provider used // accross multiple layers will lead in case of cancel to cancel all layers. dLayer.handleTile = (tile) => { @@ -278,7 +275,6 @@ abstract class Display { private setLayerBgColor(style, dLayer: Layer) { let {backgroundColor} = style; - if (backgroundColor) { if (typeof backgroundColor == 'object' && !Array.isArray(backgroundColor)) { backgroundColor = createZoomRangeFunction(parseColorMap(backgroundColor)); @@ -461,19 +457,14 @@ abstract class Display { return _gridClip.top; } - // updateGrid2(zoomlevel: number) { - // this.updateGrid(this.centerWorld, zoomlevel, this.sx, this.sy); - // } - // updateGrid(centerWorldPixel: [number, number], zoomlevel: number, screenOffsetX: number, screenOffsetY: number) { - // console.log('centerWorldPixel', centerWorldPixel.map((x) => x * window._wSize), window._x, window._y); - updateGrid(zoomlevel: number, screenOffsetX: number, screenOffsetY: number) { + updateGrid(tileGridZoom: number, zoomLevel: number, screenOffsetX: number, screenOffsetY: number) { const centerWorldPixel = this.centerWorld; // const screenOffsetX = this.sx; // const screenOffsetY = this.sy; - const tileGridZoom = zoomlevel ^ 0; // const worldSize = Math.pow(2, zoomlevel) * this.tileSize; - this.processLayerBackgroundColor(zoomlevel); + this.processLayerBackgroundColor(zoomLevel); + this.layers.setZoom(zoomLevel); this.viewChange = true; this.sx = screenOffsetX; diff --git a/packages/display/src/displays/Layers.ts b/packages/display/src/displays/Layers.ts index 78727b727..f8740d76c 100644 --- a/packages/display/src/displays/Layers.ts +++ b/packages/display/src/displays/Layers.ts @@ -17,8 +17,11 @@ * License-Filename: LICENSE */ -import {Tile, Layer as BasicLayer, TileLayer} from '@here/xyz-maps-core'; +import {Tile, Layer as BasicLayer, TileLayer, Feature} from '@here/xyz-maps-core'; +import {Expression, ExpressionParser} from '@here/xyz-maps-common'; import BasicTile from './BasicTile'; +import {parseStyleGroup} from './styleTools'; + class ScreenTile { x: number; @@ -35,6 +38,16 @@ class ScreenTile { } } +interface ResultCache<K, V> extends Map<K, V> { + hits?: number; +} + +export type StyleExpressionParser = ExpressionParser & { + context: { [name: string]: any, _dynamicExpResultCache: ResultCache<Expression, any> }, + _dynamicExpCache: Map<Expression, any> +} + + class Layer { id: number; ready: boolean = false; @@ -53,11 +66,19 @@ class Layer { private zd: boolean = false; // dirty bgColor: any; + private expParser: StyleExpressionParser | undefined; + constructor(layer: BasicLayer, layers: Layers) { this.layer = layer; this.tileSize = (<TileLayer>layer).tileSize || null; this.layers = layers; this.id = Math.floor(Math.random() * 1e16); + + this.expParser = (this.layer as TileLayer).getStyleManager?.().getExpressionParser?.() as StyleExpressionParser; + } + + getExpressionParser(): StyleExpressionParser { + return this.expParser; } getZ(z: number | string): number { @@ -116,6 +137,14 @@ class Layer { _z3d: number = Infinity; z3d: number; + + processStyleGroup(feature, tileGridZoom: number) { + const styleGroup = (this.layer as TileLayer).getStyleGroup?.(feature, tileGridZoom); + if (styleGroup) { + parseStyleGroup(styleGroup, this.expParser); + } + return styleGroup; + } } class Layers extends Array<Layer> { @@ -183,6 +212,18 @@ class Layers extends Array<Layer> { return this._map[layer]; } + setZoom(zoomlevel: number) { + for (let layer of this) { + const expParser = layer.getExpressionParser(); + const expContext = expParser?.context; + if (expContext && expContext.zoom != zoomlevel) { + expContext.zoom = zoomlevel; + expContext.$zoom = zoomlevel^0; + // expParser.dynamicResultCache.clear(); + } + } + } + // clear(): TileMap { // const _tiles = this.tiles; // this.tiles = {}; diff --git a/packages/display/src/displays/canvas/Display.ts b/packages/display/src/displays/canvas/Display.ts index 365127730..81f144cc1 100644 --- a/packages/display/src/displays/canvas/Display.ts +++ b/packages/display/src/displays/canvas/Display.ts @@ -23,6 +23,8 @@ import BasicDisplay from '../BasicDisplay'; import {TileLayer} from '@here/xyz-maps-core'; import CanvasTile from './CanvasTile'; import CanvasRenderer from './Canvas'; +import LayerClusterer from './LayerClusterer'; +import {TaskManager} from '@here/xyz-maps-common'; let DISPLAY_CFG_PR = { '1': [ @@ -71,12 +73,14 @@ class RenderBucket { } } +const CLUSTER_EXCLUSIVE_TIME_MS = 4; class CanvasDisplay extends BasicDisplay { - static zoomBehavior:'fixed'|'float' = 'fixed'; + static zoomBehavior: 'fixed' | 'float' = 'fixed'; buckets: DisplayTilePool; render: CanvasRenderer; + private cluster: LayerClusterer; constructor(mapEl, tileSize, devicePixelRatio, renderOptions?: {}) { tileSize = tileSize || DEFAULT_TILE_SIZE; @@ -95,6 +99,8 @@ class CanvasDisplay extends BasicDisplay { super(mapEl, tileSize, devicePixelRatio, buckets, tileRenderer, PREVIEW_LOOK_AHEAD_LEVELS); + this.cluster = new LayerClusterer(TaskManager.getInstance(), CLUSTER_EXCLUSIVE_TIME_MS); + tileRenderer.init(this.canvas); }; @@ -199,10 +205,10 @@ class CanvasDisplay extends BasicDisplay { } } - addLayer(layer: TileLayer, styles, index) { + addLayer(layer: TileLayer, index: number, styles?) { // Workaround: canvas only supports 256pixel rendering layer.tileSize = 256; - const added = super.addLayer(layer, styles, index); + const added = super.addLayer(layer, index, styles); if (added) { this.setupTilePool(); } diff --git a/packages/display/src/displays/LayerClusterer.ts b/packages/display/src/displays/canvas/LayerClusterer.ts similarity index 99% rename from packages/display/src/displays/LayerClusterer.ts rename to packages/display/src/displays/canvas/LayerClusterer.ts index 845b5c6ab..58119cbd5 100644 --- a/packages/display/src/displays/LayerClusterer.ts +++ b/packages/display/src/displays/canvas/LayerClusterer.ts @@ -24,9 +24,9 @@ // //* ****************************************************************************************** -import {getValue, isStyle} from './styleTools'; +import {getValue, isStyle} from '../styleTools'; -import {defaultFont} from './textUtils'; +import {defaultFont} from '../textUtils'; const DEFAULT_FONT = defaultFont; const DEFAULT_STROKE_WIDTH_ZOOM_SCALE = () => 1; diff --git a/packages/display/src/displays/styleTools.ts b/packages/display/src/displays/styleTools.ts index b0bdfdd80..a16e926c6 100644 --- a/packages/display/src/displays/styleTools.ts +++ b/packages/display/src/displays/styleTools.ts @@ -18,12 +18,13 @@ */ import {wrapText} from './textUtils'; -import {Feature, StyleZoomRange} from '@here/xyz-maps-core'; -import {RGBA, toRGB} from './webgl/color'; import {getRotatedBBox} from '../geometry'; -import {webMercator, Style, StyleGroup} from '@here/xyz-maps-core'; - import {GlyphManager} from './webgl/GlyphManager'; +import {Expression, ExpressionParser, JSONExpression} from '@here/xyz-maps-common'; +import {Feature, StyleZoomRange, webMercator, Style, StyleGroup, LayerStyle} from '@here/xyz-maps-core'; +import {Color} from '@here/xyz-maps-common'; +import toRGB = Color.toRGB; +import {StyleExpressionParser} from './Layers'; const glyphManager = GlyphManager.getInstance(); @@ -34,15 +35,14 @@ const getTileGridZoom = (zoom) => Math.min(zoom, 20) ^ 0; const INFINITY = Infinity; let UNDEF; - enum ValueType { - Number, - String, - Color, - // Size values can be defined in Meter or Pixel, needs to be parsed. - Size, - Boolean, - Float + Number, + String, + Color, + // Size values can be defined in Meter or Pixel, needs to be parsed. + Size, + Boolean, + Float } const parsePropertyNames = { @@ -71,7 +71,9 @@ const parsePropertyNames = { 'extrude': {value: ValueType.Size}, 'extrudeBase': {value: ValueType.Size}, 'intensity': {value: ValueType.Float}, - 'weight': {value: ValueType.Float} + 'weight': {value: ValueType.Float}, + 'opacity': {value: ValueType.Float}, + 'strokeDasharray': {value: ValueType.Size} }; const allowedFloatProperties: { [name: string]: true } = {}; for (let name in parsePropertyNames) { @@ -103,17 +105,34 @@ export const getTextString = (style, feature: Feature, level: number) => { } }; +export const fillMap = (searchMap, parseSizeValue: boolean, map = {}) => { + let fixedZoomMap = {}; + for (let zoom in searchMap) { + fixedZoomMap[Math.round(Number(zoom))] = searchMap[zoom]; + } + for (let zoom = 1; zoom <= 20; zoom++) { + map[zoom] = searchLerp(fixedZoomMap, zoom, parseSizeValue); + } + return map; +}; + // const getValue = < // Property extends keyof Style, // Value = Style[Property], // Return = Value extends (...args: any[]) => any ? ReturnType<Value> : Exclude<Value,StyleZoomRange<any>> // >(property: Property, style: Style, feature: Feature, zoom: number): Return { -const getValue = (name: string, style: Style, feature: Feature, tileGridZoom: number) => { +const getValue = (name: string, style: Style, feature: Feature, tileGridZoom: number, mode?: 0 | 1/* 0->Static mode, 1-> Dynamic mode*/) => { let value = style[name]; - return typeof value == 'function' - // @ts-ignore, 3rd param can be used internally - ? value(feature, tileGridZoom, style) + + if (value instanceof Expression) { + return value.resolve(feature.properties, mode || 0); + } + + value = typeof value == 'function' + // @ts-ignore, 3rd param can be used internally + ? value(feature, tileGridZoom, style, mode) : value; + return value; }; const parseSizeValue = (size: string | number, float: boolean = false): [number, string] => { @@ -197,7 +216,6 @@ const getLineWidth = (groups: StyleGroup, feature: Feature, zoom: number, layerI // } const value = getSizeInPixel('strokeWidth', style, feature, zoom, true); - if (value > width) { width = value; } @@ -226,10 +244,10 @@ export const calcBBox = (style: Style, feature: Feature, zoom: number, dpr: numb if ( // it's not a icon.. type != 'Image' && - // .. and no fill/stroke is defined - !fill && !stroke + // .. and no fill/stroke is defined + !fill && !stroke ) { - // -> it's not visible! + // -> it's not visible! return null; } @@ -497,68 +515,58 @@ const searchLerp = (map, search: number, parseSize: boolean = true) => { } return rawVal; }; -export const fillMap = (searchMap, parseSizeValue: boolean, map = {}) => { - let fixedZoomMap = {}; - for (let zoom in searchMap) { - fixedZoomMap[Math.round(Number(zoom))] = searchMap[zoom]; - } - for (let zoom = 1; zoom <= 20; zoom++) { - map[zoom] = searchLerp(fixedZoomMap, zoom, parseSizeValue); - } - return map; -}; - -export const parseColorMap = (map: { [zoom: string]: RGBA }) => { +export const parseColorMap = (map: { [zoom: string]: Color.RGBA }) => { for (let z in map) { map[z] = toRGB(map[z]); } return map; }; -export const createZoomRangeFunction = (map: StyleZoomRange<RGBA>, /* isFeatureContext?:boolean,*/ parseSizeValue?: boolean) => { +export const createZoomRangeFunction = (map: StyleZoomRange<Color.RGBA>, /* isFeatureContext?:boolean,*/ parseSizeValue?: boolean) => { map = fillMap(map, parseSizeValue); // return new Function('f,zoom', `return (${JSON.stringify(map)})[zoom];`); const range = (feature, zoom: number) => { return map[zoom ?? feature]; }; range.map = map; // dbg - return range; }; -const parseStyleGroup = (styleGroup: Style[]) => { - if (!(<any>styleGroup).__p) { - (<any>styleGroup).__p = true; - for (let style of styleGroup) { - for (let name in style) { - if (name in parsePropertyNames) { - let value = style[name]; - if ( - typeof value == 'object' && - !Array.isArray(value) && +const parseStyleGroup = (styleGroup: readonly(Style & { __p?: true })[], expParser: StyleExpressionParser) => { + for (let style of styleGroup) { + // process style only once + if (style.__p) continue; + for (let name in style) { + if (name in parsePropertyNames) { + const value = style[name]; + const skipExpressions = name == 'strokeDasharray'; + + if (!skipExpressions && ExpressionParser.isJSONExp(value)) { + // if (name != 'strokeDasharray' || typeof parseInt(value[0]) != 'number') { + style[name] = expParser.parseJSON(value); + // } + } else { + // check if value is a "StyleZoomRange" + if (value && typeof value == 'object' && !Array.isArray(value) && // skip Gradients !value.type ) { - // "zoomrange" value detected if (name == 'stroke' || name == 'fill') { // convert to [r,g,b,a] parseColorMap(value); } - const parseSizeValue = !allowedFloatProperties[name]; - style[name] = createZoomRangeFunction(value, parseSizeValue); - // let map = fillMap(value, parseSizeValue, {}); - // style[name] = createZoomRangeFunction(map); } } } } + style.__p = true; } + return styleGroup; }; - export { getValue, parseSizeValue, diff --git a/packages/display/src/displays/webgl/Display.ts b/packages/display/src/displays/webgl/Display.ts index 82e8d507e..ceb08ef30 100644 --- a/packages/display/src/displays/webgl/Display.ts +++ b/packages/display/src/displays/webgl/Display.ts @@ -30,7 +30,7 @@ import GLTile from './GLTile'; import {FeatureFactory} from './buffer/FeatureFactory'; import {CollisionHandler} from './CollisionHandler'; import {GeometryBuffer} from './buffer/GeometryBuffer'; -import {CustomLayer, tile, TileLayer, Layer as BasicLayer} from '@here/xyz-maps-core'; +import {CustomLayer, TileLayer} from '@here/xyz-maps-core'; import {PASS} from './program/GLStates'; import {Raycaster} from './Raycaster'; diff --git a/packages/display/src/displays/webgl/GLRender.ts b/packages/display/src/displays/webgl/GLRender.ts index e0046fb72..bd83071b1 100644 --- a/packages/display/src/displays/webgl/GLRender.ts +++ b/packages/display/src/displays/webgl/GLRender.ts @@ -20,8 +20,6 @@ import BasicRender from '../BasicRender'; import {CustomLayer, Tile, TileLayer} from '@here/xyz-maps-core'; import GLTile from './GLTile'; -import {RGBA, toRGB} from './color'; - import RectProgram from './program/Rect'; import CircleProgram from './program/Circle'; import LineProgram from './program/Line'; @@ -64,6 +62,10 @@ import {GLExtensions} from './GLExtensions'; import {Texture} from './Texture'; import {ScreenTile} from '../Layers'; +import {Color, ExpressionParser} from '@here/xyz-maps-common'; +import toRGB = Color.toRGB; + + const mat4 = {create, lookAt, multiply, perspective, rotateX, rotateZ, translate, scale, clone, copy, invert, identity}; const PI2 = 2 * Math.PI; @@ -222,11 +224,11 @@ export class GLRender implements BasicRender { } } - convertColor(color: string | RGBA) { + convertColor(color: string | Color.RGBA) { return toRGB(color); } - setBackgroundColor(color: RGBA) { + setBackgroundColor(color: Color.RGBA) { this.gl?.clearColor(color[0], color[1], color[2], color[3] ?? 1.0); } @@ -621,6 +623,7 @@ export class GLRender implements BasicRender { program.initAttributes(bufAttributes); program.initUniforms(this.sharedUniforms); + program.initUniforms(buffer.uniforms); } diff --git a/packages/display/src/displays/webgl/Raycaster.ts b/packages/display/src/displays/webgl/Raycaster.ts index c5509679f..feafe8aaa 100644 --- a/packages/display/src/displays/webgl/Raycaster.ts +++ b/packages/display/src/displays/webgl/Raycaster.ts @@ -303,7 +303,7 @@ class Raycaster { buffer: GeometryBuffer, layerIndex?: number ) { - if (buffer.type == 'Image' || buffer.pointerEvents === false) return; + if (buffer.pointerEvents === false) return; const {result} = this; diff --git a/packages/display/src/displays/webgl/buffer/FeatureFactory.ts b/packages/display/src/displays/webgl/buffer/FeatureFactory.ts index e02af4ca7..dc7a62ed3 100644 --- a/packages/display/src/displays/webgl/buffer/FeatureFactory.ts +++ b/packages/display/src/displays/webgl/buffer/FeatureFactory.ts @@ -23,19 +23,25 @@ import {addExtrude} from './addExtrude'; import {addIcon} from './addIcon'; import earcut from 'earcut'; import {calcBBox, getTextString, getValue, parseSizeValue, Style, StyleGroup} from '../../styleTools'; +// import {calcBBox, getTextString, getValue as parseValue, parseSizeValue, Style, StyleGroup} from '../../styleTools'; import {defaultFont, wrapText} from '../../textUtils'; import {FontStyle, GlyphTexture} from '../GlyphTexture'; -import {RGBA, toRGB} from '../color'; import {BBox, CollisionData, CollisionHandler} from '../CollisionHandler'; import {LineFactory} from './LineFactory'; - import {TextBuffer} from './templates/TextBuffer'; import {SymbolBuffer} from './templates/SymbolBuffer'; import {PointBuffer} from './templates/PointBuffer'; import {PolygonBuffer} from './templates/PolygonBuffer'; import {ExtrudeBuffer} from './templates/ExtrudeBuffer'; import {toPresentationFormB} from '../arabic'; -import {Tile, Feature, GeoJSONCoordinate, TextStyle, ImageStyle} from '@here/xyz-maps-core'; +import { + Tile, + Feature, + GeoJSONCoordinate, + TextStyle, + ImageStyle, + ParsedStyleProperty +} from '@here/xyz-maps-core'; import {TemplateBuffer} from './templates/TemplateBuffer'; import {addVerticalLine} from './addVerticalLine'; import {BoxBuffer} from './templates/BoxBuffer'; @@ -51,6 +57,11 @@ import {GradientFactory} from '../GradientFactory'; import {HeatmapBuffer} from './templates/HeatmapBuffer'; import {TextureAtlasManager} from '../TextureAtlasManager'; import {LineBuffer} from './templates/LineBuffer'; +import {Color as ColorUtils, Expression} from '@here/xyz-maps-common'; + +const {toRGB} = ColorUtils; +type RGBA = ColorUtils.RGBA; + const DEFAULT_STROKE_WIDTH = 1; const DEFAULT_LINE_CAP = 'round'; @@ -116,6 +127,12 @@ type ZDrawGroup = { export type GroupMap = { [zIndex: string]: ZDrawGroup }; +const isDynamicProperty = (prop: any) => prop instanceof Expression; +// const isDynamicProperty = (prop: any) => typeof prop == 'function'; + +const DYNAMIC_MODE = 1; +let dynamicValueId = 0; + export class FeatureFactory { private readonly gl: WebGLRenderingContext; private atlasManager: TextureAtlasManager; @@ -187,7 +204,7 @@ export class FeatureFactory { rotationY: number | undefined, text?: string, defaultLineWrap?: number | boolean, - textAnchor?: TextStyle['textAnchor'] + textAnchor?: ParsedStyleProperty<TextStyle['textAnchor']> | string ) { const isFlat = z === null; const level = this.z; @@ -361,7 +378,7 @@ export class FeatureFactory { create( feature: Feature, - geomType: string, + geomType: 'Point' | 'LineString' | 'Polygon', coordinates: GeoJSONCoordinate | GeoJSONCoordinate[] | GeoJSONCoordinate[][], styleGroups: StyleGroup, strokeWidthScale: number, @@ -519,20 +536,23 @@ export class FeatureFactory { processPointOffset = true; } else { stroke = getValue('stroke', style, feature, level); - strokeWidth = getValue('strokeWidth', style, feature, level); - if (type == 'VerticalLine') { - offsetZ = getValue('offsetZ', style, feature, level) || 0; - groupId = 'VL' + stroke + offsetZ; - if (altitude == UNDEF) { - altitude = true; - } - } else if (type == 'Line') { - if (!stroke || !strokeWidth) continue; + let widthId; + + if (type == 'Line') { + if (!stroke) continue; + + strokeWidth = getValue('strokeWidth', style, feature, level); + // strokeWidth = getValue('strokeWidth', style, feature, level, DYNAMIC_MODE); + + if (!strokeWidth) continue; - const [value, unit] = parseSizeValue(strokeWidth, false); - strokeWidth = value; - sizeUnit = unit; + // if (isDynamicProperty(strokeWidth)) { + // widthId = (strokeWidth.id ||= dynamicValueId++); + // } else { + [strokeWidth, sizeUnit] = parseSizeValue(strokeWidth); + widthId = strokeWidth; + // } strokeLinecap = getValue('strokeLinecap', style, feature, level) || DEFAULT_LINE_CAP; strokeLinejoin = getValue('strokeLinejoin', style, feature, level) || DEFAULT_LINE_JOIN; @@ -574,97 +594,114 @@ export class FeatureFactory { strokeDasharray = UNDEF; } } else { - fill = getValue('fill', style, feature, level); - - if (type == 'Polygon') { - if (!fill || geomType != 'Polygon') { - continue; + strokeWidth = getValue('strokeWidth', style, feature, level); + widthId = strokeWidth; + + if (type == 'VerticalLine') { + offsetZ = getValue('offsetZ', style, feature, level) || 0; + groupId = 'VL' + stroke + offsetZ; + if (altitude == UNDEF) { + altitude = true; } - extrude = getValue('extrude', style, feature, level); - extrudeBase = getValue('extrudeBase', style, feature, level); + } else { + fill = getValue('fill', style, feature, level); - if (typeof extrude == 'number' || extrudeBase) { - groupId = 'E'; - type = 'Extrude'; + if (type == 'Polygon') { + if (!fill || geomType != 'Polygon') { + continue; + } + extrude = getValue('extrude', style, feature, level); + extrudeBase = getValue('extrudeBase', style, feature, level); + + if (typeof extrude == 'number' || extrudeBase) { + groupId = 'E'; + type = 'Extrude'; + } else { + groupId = 'P'; + } } else { - groupId = 'P'; - } - } else { - if (geomType == 'Polygon') { - continue; - } + if (geomType == 'Polygon') { + continue; + } - alignment = getValue('alignment', style, feature, level); + alignment = getValue('alignment', style, feature, level); - if (type == 'Text') { - text = getTextString(style, feature, level); + if (type == 'Text') { + text = getTextString(style, feature, level); - if (!text) { - continue; - } + if (!text) { + continue; + } - text = toPresentationFormB(text); + text = toPresentationFormB(text); - font = getValue('font', style, feature, level) || defaultFont; + font = getValue('font', style, feature, level) || defaultFont; - if (alignment == UNDEF) { - alignment = geomType == 'Point' ? 'viewport' : 'map'; - } - groupId = 'T' + (font || NONE); - } else if (type == 'Circle') { - width = height = getValue('radius', style, feature, level); - [width, sizeUnit] = parseSizeValue(width); + if (alignment == UNDEF) { + alignment = geomType == 'Point' ? 'viewport' : 'map'; + } + groupId = 'T' + (font || NONE); + } else if (type == 'Circle') { + width = height = getValue('radius', style, feature, level, DYNAMIC_MODE); + + let widthId; + if (isDynamicProperty(width)) { + widthId = (width.id ||= width++); + } else { + [width, sizeUnit] = parseSizeValue(width); + widthId = width; + } - groupId = (altitude ? 'AC' : 'C') + sizeUnit + width || NONE; - } else if (type == 'Heatmap') { - width = getValue('radius', style, feature, level); - [width, sizeUnit] = parseSizeValue(width); + groupId = (altitude ? 'AC' : 'C') + sizeUnit + widthId || NONE; + } else if (type == 'Heatmap') { + width = getValue('radius', style, feature, level); + [width, sizeUnit] = parseSizeValue(width); - fillRGBA = fill; + fillRGBA = fill; - const intensity = getValue('intensity', style, feature, level); + const intensity = getValue('intensity', style, feature, level); - // TODO: refactor shared property usage.. - height = intensity; + // TODO: refactor shared property usage.. + height = intensity; - // if (fill?.type) { - // fill._id = Math.random() * 1e6; - // } - groupId = 'H' + (width || NONE) + JSON.stringify(fill) + intensity; - } else if (type == 'Rect') { - width = getValue('width', style, feature, level); - [width, sizeUnit] = parseSizeValue(width); - - height = getValue('height', style, feature, level); - height = !height ? width : parseSizeValue(height)[0]; - - groupId = (altitude ? 'AR' : 'R') + rotation + sizeUnit + width + height; - } else if (type == 'Box') { - // width = getValue('width', style, feature, level); - // [width, sizeUnit] = parseSizeValue(width); - // height = getValue('height', style, feature, level); - // depth = getValue('depth', style, feature, level); - - pointerEvents = getValue('pointerEvents', style, feature, level); - - groupId = 'B' + rotation + pointerEvents; // + sizeUnit + width + height; - } else if (type == 'Sphere') { - width = height = getValue('radius', style, feature, level); - [width, sizeUnit] = parseSizeValue(width); - - pointerEvents = getValue('pointerEvents', style, feature, level); - groupId = 'S' + width + pointerEvents; - } else { - continue; - } + // if (fill?.type) { + // fill._id = Math.random() * 1e6; + // } + groupId = 'H' + (width || NONE) + JSON.stringify(fill) + intensity; + } else if (type == 'Rect') { + width = getValue('width', style, feature, level); + [width, sizeUnit] = parseSizeValue(width); + + height = getValue('height', style, feature, level); + height = !height ? width : parseSizeValue(height)[0]; + + groupId = (altitude ? 'AR' : 'R') + rotation + sizeUnit + width + height; + } else if (type == 'Box') { + // width = getValue('width', style, feature, level); + // [width, sizeUnit] = parseSizeValue(width); + // height = getValue('height', style, feature, level); + // depth = getValue('depth', style, feature, level); + + pointerEvents = getValue('pointerEvents', style, feature, level); - processPointOffset = true; + groupId = 'B' + rotation + pointerEvents; // + sizeUnit + width + height; + } else if (type == 'Sphere') { + width = height = getValue('radius', style, feature, level); + [width, sizeUnit] = parseSizeValue(width); - groupId += alignment || ''; + pointerEvents = getValue('pointerEvents', style, feature, level); + groupId = 'S' + width + pointerEvents; + } else { + continue; + } + + processPointOffset = true; + + groupId += alignment || ''; + } } } - if (!fillRGBA && fill) { fillRGBA = this.toRGBA(fill, opacity); } @@ -677,18 +714,19 @@ export class FeatureFactory { strokeScale = 1; } - if (typeof strokeWidth != 'number') { - strokeWidth = DEFAULT_STROKE_WIDTH; - } else { - strokeWidth *= strokeScale; - - if (strokeWidth < 0) { + if (!isDynamicProperty(strokeWidth)) { + if (typeof strokeWidth != 'number') { strokeWidth = DEFAULT_STROKE_WIDTH; + } else { + strokeWidth *= strokeScale; + if (strokeWidth < 0) { + strokeWidth = DEFAULT_STROKE_WIDTH; + } } } } - groupId += (stroke || NONE) + (strokeWidth || NONE) + (fill || NONE); + groupId += (stroke || NONE) + (widthId || NONE) + (fill || NONE); } if (processPointOffset) { @@ -814,7 +852,7 @@ export class FeatureFactory { } this.createPoint(type, group, x, y, z, style, feature, collisionData, rotation, UNDEF, text, UNDEF, - type == 'Text' && getValue('textAnchor', style, feature, level) + (type == 'Text' && getValue('textAnchor', style, feature, level)) as ParsedStyleProperty<TextStyle['textAnchor']> ); } } else if (geomType == 'LineString') { @@ -853,7 +891,7 @@ export class FeatureFactory { let anchor = getValue('anchor', style, feature, level); anchor ??= isText ? 'Line' : 'Coordinate'; - const textAnchor = isText && getValue('textAnchor', style, feature, level); + const textAnchor = isText && getValue('textAnchor', style, feature, level) as ParsedStyleProperty<TextStyle['textAnchor']>; const checkCollisions = isText ? !collide : collide === false; let w; diff --git a/packages/display/src/displays/webgl/buffer/GeometryBuffer.ts b/packages/display/src/displays/webgl/buffer/GeometryBuffer.ts index d6fc19c2a..0a39e672f 100644 --- a/packages/display/src/displays/webgl/buffer/GeometryBuffer.ts +++ b/packages/display/src/displays/webgl/buffer/GeometryBuffer.ts @@ -24,7 +24,9 @@ import {ConstantAttribute, FlexAttribute, TemplateBuffer} from './templates/Temp import {Raycaster} from '../Raycaster'; import {PASS} from '../program/GLStates'; -export type Uniform = number | number[] | Float32Array | Int32Array | boolean | Texture; +export type Uniform = number | number[] | Float32Array | Float64Array | Int32Array | boolean | Texture; +export type DynamicUniform = () => Uniform; + export type IndexData = { data: Uint16Array | Uint32Array @@ -117,7 +119,7 @@ class GeometryBuffer { static MODE_GL_TRIANGLES: number = 0x0004; // private size: number; attributes: { [name: string]: Attribute | ConstantAttribute } = {}; - uniforms: { [name: string]: Uniform } = {}; + uniforms: { [name: string]: Uniform|DynamicUniform } = {}; type: string; pass: number = PASS.OPAQUE; zIndex?: number; @@ -182,6 +184,8 @@ class GeometryBuffer { geoBuffer.rayIntersects = templBuffer.rayIntersects; + // geoBuffer.finalize = templBuffer.finalize; + geoBuffer.cullFace(templBuffer.cullFace); for (let name in templBuffer.uniforms) { @@ -252,7 +256,7 @@ class GeometryBuffer { this.uniforms[name] = uniform; } - getUniform(name: string): Uniform { + getUniform(name: string): Uniform|DynamicUniform { return this.uniforms[name]; } diff --git a/packages/display/src/displays/webgl/buffer/addText.ts b/packages/display/src/displays/webgl/buffer/addText.ts index 67832f17b..3fde01e63 100644 --- a/packages/display/src/displays/webgl/buffer/addText.ts +++ b/packages/display/src/displays/webgl/buffer/addText.ts @@ -20,11 +20,14 @@ import {createTextData, OFFSET_SCALE} from './createText'; import {GlyphAtlas} from '../GlyphAtlas'; import {FlexArray} from './templates/FlexArray'; -import {TextStyle} from '@here/xyz-maps-core'; +import {ParsedStyleProperty, StyleExpression, StyleValueFunction, StyleZoomRange, TextStyle} from '@here/xyz-maps-core'; +import ZoomControl from '../../../ui/ZoomControl'; const EXTENT_SCALE = 64; -const ANCHOR_OFFSET: Record<TextStyle['textAnchor'], { x: number, y: number }> = { +type TextAnchors = ParsedStyleProperty<TextStyle['textAnchor']>; + +const ANCHOR_OFFSET: Record<TextAnchors, { x: number, y: number }> = { Center: {x: .5, y: 0}, Left: {x: 0, y: 0}, Right: {x: 1, y: 0}, @@ -47,7 +50,7 @@ const addText = ( glyphAtlas: GlyphAtlas, rotationZ = 0, rotationY: number | undefined, - textAnchor: TextStyle['textAnchor'] | string = 'Center' + textAnchor: ParsedStyleProperty<TextStyle['textAnchor']> | string = 'Center' ) => { const lineOffset = lines.length - 1; const lineHeight = glyphAtlas.lineHeight; @@ -73,7 +76,7 @@ const addText = ( // } if (hasHeight) { - // normalize float meters to uint16 (0m ... +9000m) + // normalize float meters to uint16 (0m ... +9000m) z = Math.round(z / 9000 * 0xffff); dim = 3; } diff --git a/packages/display/src/displays/webgl/buffer/createBuffer.ts b/packages/display/src/displays/webgl/buffer/createBuffer.ts index 43185e070..782218028 100644 --- a/packages/display/src/displays/webgl/buffer/createBuffer.ts +++ b/packages/display/src/displays/webgl/buffer/createBuffer.ts @@ -17,11 +17,11 @@ * License-Filename: LICENSE */ -import {geometry, TaskManager} from '@here/xyz-maps-common'; +import {geometry, ExpressionParser, JSONExpression, TaskManager} from '@here/xyz-maps-common'; import {GeometryBuffer} from './GeometryBuffer'; import {getValue, parseStyleGroup} from '../../styleTools'; -import {Feature, GeoJSONCoordinate, LinearGradient, StyleGroup, Tile, TileLayer, webMercator} from '@here/xyz-maps-core'; -import {Layer} from '../../Layers'; +import {Feature, GeoJSONCoordinate, LayerStyle, LinearGradient, StyleGroup, Tile, TileLayer, webMercator} from '@here/xyz-maps-core'; +import {Layer, StyleExpressionParser} from '../../Layers'; import {CollisionGroup, FeatureFactory, GroupMap} from './FeatureFactory'; import {GlyphTexture} from '../GlyphTexture'; import {TemplateBufferBucket} from './templates/TemplateBufferBucket'; @@ -31,6 +31,7 @@ import {PASS} from '../program/GLStates'; import {DEFAULT_HEATMAP_GRADIENT, HeatmapBuffer} from './templates/HeatmapBuffer'; import {DisplayTileTask} from '../../BasicTile'; + const {centroid} = geometry; const PROCESS_FEATURE_CHUNK_SIZE = 16; @@ -94,7 +95,7 @@ type TaskData = { zoomScale: number, featureIndex: number, chunkSize: number, - layer: TileLayer, + layer: Layer, zoom: number, collisions: null | CollisionGroup[], groups: GroupMap @@ -102,14 +103,14 @@ type TaskData = { const createBuffer = ( - renderLayer: Layer, + displayLayer: Layer, tileSize: number, tile: Tile, factory: FeatureFactory, onInit: () => void, onDone: (data: GeometryBuffer[], pendingResources: Promise<any>[]) => void ): DisplayTileTask => { - const layer = <TileLayer>renderLayer.layer; + const layer = <TileLayer>displayLayer.layer; // const groups: GroupMap = {}; const pendingResources = []; const waitAndRefresh = (promise: Promise<any>) => { @@ -146,7 +147,7 @@ const createBuffer = ( zoomScale: lsZoomScale, featureIndex: 0, // featureIndex chunkSize: PROCESS_FEATURE_CHUNK_SIZE, - layer, + layer: displayLayer, zoom, collisions: null, groups @@ -221,7 +222,8 @@ const createBuffer = ( geoBuffer.addUniform('u_fill', stroke); - geoBuffer.addUniform('u_strokeWidth', [strokeWidth * .5, shared.unit == 'm' ? meterToPixel : 0]); + geoBuffer.addUniform('u_strokeWidth', [strokeWidth, shared.unit == 'm' ? meterToPixel : 0]); + // geoBuffer.addUniform('u_strokeWidth', [strokeWidth * .5, shared.unit == 'm' ? meterToPixel : 0]); geoBuffer.addUniform('u_offset', [shared.offsetX, shared.offsetUnit == 'm' ? meterToPixel : 0 @@ -387,7 +389,7 @@ const createBuffer = ( } } - renderLayer.addZ(zIndex, !geoBuffer.flat); + displayLayer.addZ(zIndex, !geoBuffer.flat); geoBuffer.zIndex = zIndex; geoBuffer.zLayer = typeof zLayer == 'number' ? Math.ceil(zLayer) : null; @@ -406,7 +408,6 @@ const createBuffer = ( exec: function(taskData: TaskData) { const {tile, data, layer, zoomScale: lsScale, zoom: level} = taskData; let dataLen = data.length; - let styleGroups; let feature; let geom; @@ -417,7 +418,7 @@ const createBuffer = ( notDone ||= false; while (taskData.chunkSize--) { if (feature = data[taskData.featureIndex++]) { - styleGroups = layer.getStyleGroup(feature, level); + styleGroups = layer.processStyleGroup(feature, level); if (styleGroups) { geom = feature.geometry; @@ -427,9 +428,6 @@ const createBuffer = ( styleGroups = [styleGroups]; } - parseStyleGroup(styleGroups); - - // const coordinates = geom.coordinates; const coordinates = feature.getProvider().decCoord(feature); if (geomType == 'MultiLineString' || geomType == 'MultiPoint') { @@ -442,9 +440,7 @@ const createBuffer = ( factory.create(feature, 'Polygon', coordinates, styleGroups, lsScale); for (let p = 0; p < coordinates.length; p++) { - let polygon = coordinates[p]; - // for (let polygon of coordinates) { - handlePolygons(factory, feature, polygon, styleGroups, lsScale, tile, p); + handlePolygons(factory, feature, coordinates[p], styleGroups, lsScale, tile, p); } } else { factory.create(feature, geomType, coordinates, styleGroups, lsScale); @@ -453,7 +449,6 @@ const createBuffer = ( handlePolygons(factory, feature, coordinates, styleGroups, lsScale, tile); } } - // if (task.paused) { // awaiting asynchronous operation... // return notDone = true; diff --git a/packages/display/src/displays/webgl/buffer/createImageBuffer.ts b/packages/display/src/displays/webgl/buffer/createImageBuffer.ts index 995ecfc56..48f1d0c85 100644 --- a/packages/display/src/displays/webgl/buffer/createImageBuffer.ts +++ b/packages/display/src/displays/webgl/buffer/createImageBuffer.ts @@ -37,6 +37,7 @@ const createImageBuffer = (img: Image, gl: WebGLRenderingContext, size: number, // const texInfo = atlas.get(id) || atlas.set(id, img); // [UNIT,X,Y] const tileBuffer = new GeometryBuffer({first: 0, count: 6}, 'Image'); + // 0 ------- 1 // | / | // | / | @@ -67,7 +68,7 @@ const createImageBuffer = (img: Image, gl: WebGLRenderingContext, size: number, tileBuffer.blend = alpha; tileBuffer.pass = alpha ? PASS.ALPHA : PASS.OPAQUE; tileBuffer.pixelPerfect = true; - + tileBuffer.pointerEvents = false; tileBuffer.cullFace(FRONT); // tileBuffer.uniforms.u_snapGrid = true; diff --git a/packages/display/src/displays/webgl/buffer/debugTileBuffer.ts b/packages/display/src/displays/webgl/buffer/debugTileBuffer.ts index 2091321f9..a679c3664 100644 --- a/packages/display/src/displays/webgl/buffer/debugTileBuffer.ts +++ b/packages/display/src/displays/webgl/buffer/debugTileBuffer.ts @@ -21,12 +21,13 @@ import {GeometryBuffer} from './GeometryBuffer'; import {addLineString} from './addLineString'; import {createTextData} from './createText'; import {GlyphTexture} from '../GlyphTexture'; -import {toRGB} from '../color'; import {LineBuffer} from './templates/LineBuffer'; import {FlexArray} from './templates/FlexArray'; import {FlexAttribute} from './templates/TemplateBuffer'; import {PASS} from '../program/GLStates'; import {createImageBuffer} from './createImageBuffer'; +import {Color as ColorUtils} from '@here/xyz-maps-common'; +const {toRGB} = ColorUtils; export const createStencilTileBuffer = (tileSize: number, gl: WebGLRenderingContext) => { const tileBuffer = createImageBuffer({ diff --git a/packages/display/src/displays/webgl/buffer/templates/HeatmapBuffer.ts b/packages/display/src/displays/webgl/buffer/templates/HeatmapBuffer.ts index e32c52762..4ab35d4f9 100644 --- a/packages/display/src/displays/webgl/buffer/templates/HeatmapBuffer.ts +++ b/packages/display/src/displays/webgl/buffer/templates/HeatmapBuffer.ts @@ -22,7 +22,8 @@ import {FlexArray} from './FlexArray'; import {PointBuffer} from './PointBuffer'; import {addPoint} from '../addPoint'; import {LinearGradient} from '@here/xyz-maps-core'; -import {toRGB} from '../../color'; +import {Color} from '@here/xyz-maps-common'; +import toRGB = Color.toRGB; export const DEFAULT_HEATMAP_GRADIENT: LinearGradient = { type: 'LinearGradient', diff --git a/packages/display/src/displays/webgl/buffer/templates/LineBuffer.ts b/packages/display/src/displays/webgl/buffer/templates/LineBuffer.ts index e11b44cbc..80f725f39 100644 --- a/packages/display/src/displays/webgl/buffer/templates/LineBuffer.ts +++ b/packages/display/src/displays/webgl/buffer/templates/LineBuffer.ts @@ -70,7 +70,7 @@ export class LineBuffer extends TemplateBuffer { // let feature; // const scaleX = 2 / rayCaster.w; // const scaleY = 2 / rayCaster.h; - let strokeWidth = buffer.getUniform('u_strokeWidth')[0] / rayCaster.scale; + let strokeWidth = 0.5 * buffer.getUniform('u_strokeWidth')[0] / rayCaster.scale; const scaleByAltitude = <boolean>buffer.getUniform('u_scaleByAltitude'); const N_SCALE = 1.0 / 8192.0; diff --git a/packages/display/src/displays/webgl/buffer/templates/ModelBuffer.ts b/packages/display/src/displays/webgl/buffer/templates/ModelBuffer.ts index 678daa86c..f56ede114 100644 --- a/packages/display/src/displays/webgl/buffer/templates/ModelBuffer.ts +++ b/packages/display/src/displays/webgl/buffer/templates/ModelBuffer.ts @@ -26,9 +26,11 @@ import {scale, rotate, multiply, translate, identity, create} from 'gl-matrix/ma import {transformMat4, subtract, scale as scaleVec3, normalize as normalizeVec3} from 'gl-matrix/vec3'; import {isTypedArray, TypedArray} from '../glType'; import {Attribute} from '../Attribute'; -import {toRGB} from '../../color'; +import {Color} from '@here/xyz-maps-common'; import {ModelGeometry} from '@here/xyz-maps-core'; +import toRGB = Color.toRGB; + enum HitTest { bbox, geometry diff --git a/packages/display/src/displays/webgl/color.ts b/packages/display/src/displays/webgl/color.ts deleted file mode 100644 index 55af68b9f..000000000 --- a/packages/display/src/displays/webgl/color.ts +++ /dev/null @@ -1,245 +0,0 @@ -/* - * 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 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 = <number[]><unknown>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/display/src/displays/webgl/glsl/line_vertex.glsl b/packages/display/src/displays/webgl/glsl/line_vertex.glsl index 2736990c4..200a390d5 100644 --- a/packages/display/src/displays/webgl/glsl/line_vertex.glsl +++ b/packages/display/src/displays/webgl/glsl/line_vertex.glsl @@ -28,8 +28,8 @@ const float N_SCALE = 1.0 / 8191.0; void main(void){ - float strokeWidth = toPixel(u_strokeWidth, u_scale); - float alias = u_no_antialias + float strokeWidth = toPixel(u_strokeWidth, u_scale) * 0.5; + float alias = (strokeWidth==0.0||u_no_antialias) ? 0.0 : strokeWidth < 1. ? .65 : 1.; diff --git a/packages/display/src/displays/webgl/program/Program.ts b/packages/display/src/displays/webgl/program/Program.ts index e6b457b9c..2e1c745a6 100644 --- a/packages/display/src/displays/webgl/program/Program.ts +++ b/packages/display/src/displays/webgl/program/Program.ts @@ -21,16 +21,21 @@ import {createProgram} from '../glTools'; import {GLStates, PASS} from './GLStates'; // @ts-ignore import introVertex from '../glsl/intro_vertex.glsl'; -import {ArrayGrp, GeometryBuffer, IndexData, IndexGrp} from '../buffer/GeometryBuffer'; +import {ArrayGrp, DynamicUniform, GeometryBuffer, IndexData, IndexGrp, Uniform} from '../buffer/GeometryBuffer'; import {BufferCache} from '../GLRender'; import {Attribute} from '../buffer/Attribute'; import {ConstantAttribute} from '../buffer/templates/TemplateBuffer'; let UNDEF; -type UniformMap = { +type UniformLocations = { [name: string]: WebGLUniformLocation }; + +type UniformMap = { + [name: string]: Uniform | DynamicUniform +}; + type AttributeMap = { [name: string]: Attribute | ConstantAttribute }; @@ -72,7 +77,7 @@ class Program { } } = {}; attributeDivisors: number[] = []; - uniforms: UniformMap = {}; + uniforms: UniformLocations = {}; private usage; private buffers: BufferCache; @@ -158,24 +163,20 @@ class Program { private createUniformSetter(uInfo: WebGLActiveInfo, location: WebGLUniformLocation) { const {gl} = this; + const getVal = (u) => typeof u == 'function' ? u() : u; switch (uInfo.type) { case gl.FLOAT: - return (v) => gl.uniform1f(location, v); - + return (v) => gl.uniform1f(location, getVal(v)); case gl.FLOAT_MAT4: return (v) => gl.uniformMatrix4fv(location, false, v); - case gl.FLOAT_VEC2: - return (v) => gl.uniform2fv(location, v); - + return (v) => gl.uniform2f(location, getVal(v[0]), getVal(v[1])); case gl.FLOAT_VEC3: return (v) => gl.uniform3fv(location, v); - case gl.FLOAT_VEC4: return (v) => gl.uniform4fv(location, v); - case gl.BOOL: - return (v) => gl.uniform1i(location, v); + return (v) => gl.uniform1i(location, getVal(v)); case gl.SAMPLER_2D: const tu = this.textureUnits++; return (v) => { diff --git a/packages/display/src/search/Search.ts b/packages/display/src/search/Search.ts index f4c98c31c..ca56e93b9 100644 --- a/packages/display/src/search/Search.ts +++ b/packages/display/src/search/Search.ts @@ -20,6 +20,7 @@ import Hit from './Hit'; import {CustomLayer, Feature, TileLayer} from '@here/xyz-maps-core'; import {Map} from '../Map'; +import {Layers} from '../displays/Layers'; const MAX_GRID_ZOOM = 20; // increase to make sure points (no bbox) are in hitbox of spatial check. @@ -38,9 +39,11 @@ const isNumber = (o) => typeof o == 'number'; export class Search { private map: Map; private hit: Hit; + private layers: Layers; - constructor(map: Map, dpr: number) { + constructor(map: Map, layers: Layers, dpr: number) { this.map = map; + this.layers = layers; this.hit = new Hit(map, dpr); } @@ -134,13 +137,15 @@ export class Search { provider = layer.getProvider?.(zoomlevel); let maxZ = 0; + const displayLayer = this.layers.get(layer); + if (zoomlevel <= layer.max && zoomlevel >= layer.min && provider?.search) { features = provider.search(viewbounds); length = features.length; while (length--) { feature = features[length]; - if (featureStyle = layer.getStyleGroup(feature, tileGridZoom)) { + if (featureStyle = displayLayer.processStyleGroup(feature, tileGridZoom)) { if (dimensions = hit.feature(halfWidth, halfHeight, feature, featureStyle, layerIndex, zoomlevel, skip3d)) { let zIndex = dimensions[dimensions.length - 1]; let zOrdered = results[zIndex] = results[zIndex] || []; diff --git a/packages/editor/src/IEditor.ts b/packages/editor/src/IEditor.ts index bbb30af71..e7a0c01c5 100644 --- a/packages/editor/src/IEditor.ts +++ b/packages/editor/src/IEditor.ts @@ -44,7 +44,7 @@ export default class InternalEditor { _db: DrawingBoard; private _dListener: DisplayListener; - private prvIdLayerMap: { [providerId: string]: TileLayer } + private prvIdLayerMap: { [providerId: string]: TileLayer }; display: Display; objects: ObjectManager; diff --git a/packages/editor/src/features/Overlay.ts b/packages/editor/src/features/Overlay.ts index 9f0395d61..4c5e13e1d 100644 --- a/packages/editor/src/features/Overlay.ts +++ b/packages/editor/src/features/Overlay.ts @@ -165,7 +165,7 @@ class Overlay { } - getStyles(obj): Style[] { + getStyles(obj): readonly Style[] { return this.layer.getStyleGroup(obj); } diff --git a/packages/playground/examples/examples.json b/packages/playground/examples/examples.json index 85d04fc53..2cbe501e6 100644 --- a/packages/playground/examples/examples.json +++ b/packages/playground/examples/examples.json @@ -164,6 +164,10 @@ "file": "./layer/custom_style.html", "docs": "interfaces/core.layerstyle.html" }, + { + "file": "./layer/style_expressions.html", + "docs": "modules/core.html#styleexpression" + }, { "file": "./layer/preprocessor.html", "docs": "interfaces/core.remotetileprovideroptions.html#preprocessor" diff --git a/packages/playground/examples/layer/style_expressions.html b/packages/playground/examples/layer/style_expressions.html new file mode 100644 index 000000000..f730942b6 --- /dev/null +++ b/packages/playground/examples/layer/style_expressions.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> + <head> + <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, user-scalable=no"> + <title>XYZ Maps Example: Expression based Styling</title> + <style type="text/css"> + #map { + position: absolute; + overflow: hidden; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + </style> + </head> + <body> + <div id="map"></div> + </body> +</html> 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' + }); }); });