From e3f7d29bb2be6da51c063363d809d384abb9cc75 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Thu, 24 Oct 2019 16:39:04 +0200 Subject: [PATCH] Expressions / add utilities for using strings in GLSL & more type checking It is now possible to specify a type hint when parsing an expression, which helps determine the output value type. When no single output type can be inferred, an error is thrown. For strings, every literal value will be replaced by a number and a map of these associations will be kept in the parsing context, which is passed recursively. --- src/ol/style/expressions.js | 54 ++++++++++++++++++++++---- test/spec/ol/style/expressions.test.js | 42 +++++++++++++++++--- 2 files changed, 83 insertions(+), 13 deletions(-) diff --git a/src/ol/style/expressions.js b/src/ol/style/expressions.js index 40b9774195..d69affa497 100644 --- a/src/ol/style/expressions.js +++ b/src/ol/style/expressions.js @@ -4,6 +4,7 @@ */ import {asArray, isStringColor} from '../color.js'; +import {assign} from '../obj.js'; /** * Base type used for literal style parameters; can be a number literal or the output of an operator, @@ -67,7 +68,8 @@ export const ValueTypes = { COLOR: 0b00100, BOOLEAN: 0b01000, NUMBER_ARRAY: 0b10000, - ANY: 0b11111 + ANY: 0b11111, + NONE: 0 }; /** @@ -112,12 +114,22 @@ export function getValueType(value) { return operator.getReturnType(valueArr.slice(1)); } +/** + * Checks if only one value type is enabled in the input number. + * @param {ValueTypes|number} valueType Number containing value type binary flags + * @return {boolean} True if only one type flag is enabled, false if zero or multiple + */ +export function isTypeUnique(valueType) { + return Math.log2(valueType) % 1 === 0; +} + /** * Context available during the parsing of an expression. * @typedef {Object} ParsingContext * @property {boolean} [inFragmentShader] If false, means the expression output should be made for a vertex shader * @property {Array} variables List of variables used in the expression; contains **unprefixed names** * @property {Array} attributes List of attributes used in the expression; contains **unprefixed names** + * @property {Object} stringLiteralsMap This object maps all encountered string values to a number */ /** @@ -161,6 +173,20 @@ export function colorToGlsl(color) { ); } +/** + * Returns a stable equivalent number for the string literal, for use in shaders. This number is then + * converted to be a GLSL-compatible string. + * @param {ParsingContext} context Parsing context + * @param {string} string String literal value + * @returns {string} GLSL-compatible string containing a number + */ +export function stringToGlsl(context, string) { + if (context.stringLiteralsMap[string] === undefined) { + context.stringLiteralsMap[string] = Object.keys(context.stringLiteralsMap).length; + } + return numberToGlsl(context.stringLiteralsMap[string]); +} + /** * Recursively parses a style expression and outputs a GLSL-compatible string. Takes in a parsing context that * will be read and modified during the parsing operation. @@ -176,7 +202,7 @@ export function expressionToGlsl(context, value, typeHint) { if (operator === undefined) { throw new Error(`Unrecognized expression operator: ${JSON.stringify(value)}`); } - return operator.toGlsl(context, value.slice(1)); + return operator.toGlsl(context, value.slice(1), typeHint); } else if ((getValueType(value) & ValueTypes.NUMBER) > 0) { return numberToGlsl(/** @type {number} */(value)); } else if ((getValueType(value) & ValueTypes.BOOLEAN) > 0) { @@ -185,7 +211,7 @@ export function expressionToGlsl(context, value, typeHint) { ((getValueType(value) & ValueTypes.STRING) > 0) && (typeHint === undefined || typeHint == ValueTypes.STRING) ) { - return value.toString(); + return stringToGlsl(context, value.toString()); } else if ( ((getValueType(value) & ValueTypes.COLOR) > 0) && (typeHint === undefined || typeHint == ValueTypes.COLOR) @@ -221,6 +247,16 @@ function assertArgsCount(args, count) { throw new Error(`Exactly ${count} arguments were expected, got ${args.length} instead`); } } +function assertArgsEven(args) { + if (args.length % 2 !== 0) { + throw new Error(`An even amount of arguments was expected, got ${args} instead`); + } +} +function assertUniqueInferredType(args, types) { + if (!isTypeUnique(types)) { + throw new Error(`Could not infer only one type from the following expression: ${JSON.stringify(args)}`); + } +} /** * An operator declaration must contain two methods: `getReturnType` which returns a type based on @@ -228,7 +264,8 @@ function assertArgsCount(args, count) { * Note: both methods can process arguments recursively. * @typedef {Object} Operator * @property {function(Array): ValueTypes|number} getReturnType Returns one or several types - * @property {function(ParsingContext, Array): string} toGlsl Returns a GLSL-compatible string + * @property {function(ParsingContext, Array, ValueTypes=): string} toGlsl Returns a GLSL-compatible string + * Note: takes in an optional type hint as 3rd parameter */ /** @@ -243,7 +280,7 @@ export const Operators = { toGlsl: function(context, args) { assertArgsCount(args, 1); assertString(args[0]); - const value = expressionToGlsl(context, args[0]); + const value = args[0].toString(); if (context.attributes.indexOf(value) === -1) { context.attributes.push(value); } @@ -258,7 +295,7 @@ export const Operators = { toGlsl: function(context, args) { assertArgsCount(args, 1); assertString(args[0]); - const value = expressionToGlsl(context, args[0]); + const value = args[0].toString(); if (context.variables.indexOf(value) === -1) { context.variables.push(value); } @@ -461,8 +498,9 @@ export const Operators = { assertNumber(args[0]); assertColor(args[1]); assertColor(args[2]); - const start = expressionToGlsl(context, args[1], ValueTypes.COLOR); - const end = expressionToGlsl(context, args[2], ValueTypes.COLOR); + const newContext = assign({}, context); + const start = expressionToGlsl(newContext, args[1], ValueTypes.COLOR); + const end = expressionToGlsl(newContext, args[2], ValueTypes.COLOR); return `mix(${start}, ${end}, ${expressionToGlsl(context, args[0])})`; } } diff --git a/test/spec/ol/style/expressions.test.js b/test/spec/ol/style/expressions.test.js index fedd624d26..5ad20d0632 100644 --- a/test/spec/ol/style/expressions.test.js +++ b/test/spec/ol/style/expressions.test.js @@ -1,8 +1,8 @@ import { arrayToGlsl, colorToGlsl, expressionToGlsl, - getValueType, - numberToGlsl, + getValueType, isTypeUnique, + numberToGlsl, stringToGlsl, ValueTypes } from '../../../../src/ol/style/expressions.js'; @@ -53,6 +53,38 @@ describe('ol.style.expressions', function() { }); }); + describe('stringToGlsl', function() { + let context; + beforeEach(function() { + context = { + stringLiteralsMap: {} + }; + }); + + it('maps input string to stable numbers', function() { + expect(stringToGlsl(context, 'abcd')).to.eql('0.0'); + expect(stringToGlsl(context, 'defg')).to.eql('1.0'); + expect(stringToGlsl(context, 'hijk')).to.eql('2.0'); + expect(stringToGlsl(context, 'abcd')).to.eql('0.0'); + expect(stringToGlsl(context, 'def')).to.eql('3.0'); + }); + }); + + describe('isTypeUnique', function() { + it('return true if only one value type', function() { + expect(isTypeUnique(ValueTypes.NUMBER)).to.eql(true); + expect(isTypeUnique(ValueTypes.STRING)).to.eql(true); + expect(isTypeUnique(ValueTypes.COLOR)).to.eql(true); + }); + it('return false if several value types', function() { + expect(isTypeUnique(ValueTypes.NUMBER | ValueTypes.COLOR)).to.eql(false); + expect(isTypeUnique(ValueTypes.ANY)).to.eql(false); + }); + it('return false if no value type', function() { + expect(isTypeUnique(ValueTypes.NUMBER & ValueTypes.COLOR)).to.eql(false); + }); + }); + describe('getValueType', function() { it('correctly analyzes a literal value', function() { @@ -116,7 +148,8 @@ describe('ol.style.expressions', function() { beforeEach(function() { context = { variables: [], - attributes: [] + attributes: [], + stringLiteralsMap: {} }; }); @@ -156,7 +189,7 @@ describe('ol.style.expressions', function() { }); it('gives precedence to the string type unless asked otherwise', function() { - expect(expressionToGlsl(context, 'lightgreen')).to.eql('lightgreen'); + expect(expressionToGlsl(context, 'lightgreen')).to.eql('0.0'); expect(expressionToGlsl(context, 'lightgreen', ValueTypes.COLOR)).to.eql( 'vec4(0.5647058823529412, 0.9333333333333333, 0.5647058823529412, 1.0)'); }); @@ -225,7 +258,6 @@ describe('ol.style.expressions', function() { } expect(thrown).to.be(true); }); - }); });