ol
Version:
OpenLayers mapping library
512 lines (478 loc) • 17.4 kB
JavaScript
/**
* @module ol/expr/gpu
*/
import {asArray} from '../color.js';
import {Uniforms} from '../renderer/webgl/TileLayer.js';
import {toSize} from '../size.js';
import PaletteTexture from '../webgl/PaletteTexture.js';
import {
BooleanType,
CallExpression,
ColorType,
NumberArrayType,
NumberType,
Ops,
SizeType,
StringType,
isType,
parse,
typeName,
} from './expression.js';
/**
* @param {string} operator Operator
* @param {CompilationContext} context Compilation context
* @return {string} A function name based on the operator, unique in the given context
*/
function computeOperatorFunctionName(operator, context) {
return `operator_${operator}_${Object.keys(context.functions).length}`;
}
/**
* Will return the number as a float with a dot separator, which is required by GLSL.
* @param {number} v Numerical value.
* @return {string} The value as string.
*/
export function numberToGlsl(v) {
const s = v.toString();
return s.includes('.') ? s : s + '.0';
}
/**
* Will return the number array as a float with a dot separator, concatenated with ', '.
* @param {Array<number>} array Numerical values array.
* @return {string} The array as a vector, e. g.: `vec3(1.0, 2.0, 3.0)`.
*/
export function arrayToGlsl(array) {
if (array.length < 2 || array.length > 4) {
throw new Error(
'`formatArray` can only output `vec2`, `vec3` or `vec4` arrays.',
);
}
return `vec${array.length}(${array.map(numberToGlsl).join(', ')})`;
}
/**
* Will normalize and converts to string a `vec4` color array compatible with GLSL.
* @param {string|import("../color.js").Color} color Color either in string format or [r, g, b, a] array format,
* with RGB components in the 0..255 range and the alpha component in the 0..1 range.
* Note that the final array will always have 4 components.
* @return {string} The color expressed in the `vec4(1.0, 1.0, 1.0, 1.0)` form.
*/
export function colorToGlsl(color) {
const array = asArray(color);
const alpha = array.length > 3 ? array[3] : 1;
return arrayToGlsl([array[0] / 255, array[1] / 255, array[2] / 255, alpha]);
}
/**
* Normalizes and converts a number or array toa `vec2` array compatible with GLSL.
* @param {number|import('../size.js').Size} size Size.
* @return {string} The color expressed in the `vec4(1.0, 1.0, 1.0, 1.0)` form.
*/
export function sizeToGlsl(size) {
const array = toSize(size);
return arrayToGlsl(array);
}
/** @type {Object<string, number>} */
const stringToFloatMap = {};
let stringToFloatCounter = 0;
/**
* Returns a stable equivalent number for the string literal.
* @param {string} string String literal value
* @return {number} Number equivalent
*/
export function getStringNumberEquivalent(string) {
if (!(string in stringToFloatMap)) {
stringToFloatMap[string] = stringToFloatCounter++;
}
return stringToFloatMap[string];
}
/**
* Returns a stable equivalent number for the string literal, for use in shaders. This number is then
* converted to be a GLSL-compatible string.
* Note: with a float precision of `mediump`, the amount of unique strings supported is 16,777,216
* @param {string} string String literal value
* @return {string} GLSL-compatible string containing a number
*/
export function stringToGlsl(string) {
return numberToGlsl(getStringNumberEquivalent(string));
}
/**
* Get the uniform name given a variable name.
* @param {string} variableName The variable name.
* @return {string} The uniform name.
*/
export function uniformNameForVariable(variableName) {
return 'u_var_' + variableName;
}
/**
* @typedef {import('./expression.js').ParsingContext} ParsingContext
*/
/**
*
* @typedef {import("./expression.js").Expression} Expression
*/
/**
*
* @typedef {import("./expression.js").LiteralExpression} LiteralExpression
*/
/**
* @typedef {Object} CompilationContextProperty
* @property {string} name Name
* @property {number} type Resolved property type
*/
/**
* @typedef {Object} CompilationContextVariable
* @property {string} name Name
* @property {number} type Resolved variable type
*/
/**
* @typedef {Object} CompilationContext
* @property {Object<string, CompilationContextProperty>} properties The values for properties used in 'get' expressions.
* @property {Object<string, CompilationContextVariable>} variables The values for variables used in 'var' expressions.
* @property {Object<string, string>} functions Lookup of functions used by the style.
* @property {number} [bandCount] Number of bands per pixel.
* @property {Array<PaletteTexture>} [paletteTextures] List of palettes used by the style.
* @property {boolean} featureId Whether the feature ID is used in the expression
* @property {boolean} geometryType Whether the geometry type is used in the expression
*/
/**
* @return {CompilationContext} A new compilation context.
*/
export function newCompilationContext() {
return {
variables: {},
properties: {},
functions: {},
bandCount: 0,
featureId: false,
geometryType: false,
};
}
const GET_BAND_VALUE_FUNC = 'getBandValue';
export const PALETTE_TEXTURE_ARRAY = 'u_paletteTextures';
export const FEATURE_ID_PROPERTY_NAME = 'featureId';
export const GEOMETRY_TYPE_PROPERTY_NAME = 'geometryType';
/**
* The value `-9999999` will be used to indicate that a property on a feature is not defined, similar to a "no data" value.
*/
export const UNDEFINED_PROP_VALUE = -9999999;
/**
* @typedef {string} CompiledExpression
*/
/**
* @typedef {function(CompilationContext, CallExpression, number): string} Compiler
* Third argument is the expected value types
*/
/**
* @param {import('./expression.js').EncodedExpression} encoded The encoded expression.
* @param {number} type The expected type.
* @param {import('./expression.js').ParsingContext} parsingContext The parsing context.
* @param {CompilationContext} compilationContext An existing compilation context
* @return {CompiledExpression} The compiled expression.
*/
export function buildExpression(
encoded,
type,
parsingContext,
compilationContext,
) {
const expression = parse(encoded, type, parsingContext);
return compile(expression, type, compilationContext);
}
/**
* @param {function(Array<CompiledExpression>, CompilationContext): string} output Function that takes in parsed arguments and returns a string
* @return {function(CompilationContext, import("./expression.js").CallExpression, number): string} Compiler for the call expression
*/
function createCompiler(output) {
return (context, expression, type) => {
const length = expression.args.length;
const args = new Array(length);
for (let i = 0; i < length; ++i) {
args[i] = compile(expression.args[i], type, context);
}
return output(args, context);
};
}
/**
* @type {Object<string, Compiler>}
*/
const compilers = {
[Ops.Get]: (context, expression) => {
const firstArg = /** @type {LiteralExpression} */ (expression.args[0]);
const propName = /** @type {string} */ (firstArg.value);
const isExisting = propName in context.properties;
if (!isExisting) {
context.properties[propName] = {
name: propName,
type: expression.type,
};
}
let result = 'a_prop_' + propName;
if (isType(expression.type, BooleanType)) {
result = `(${result} > 0.0)`;
}
return result;
},
[Ops.Id]: (context) => {
context.featureId = true;
return 'a_' + FEATURE_ID_PROPERTY_NAME;
},
[Ops.GeometryType]: (context) => {
context.geometryType = true;
return 'a_' + GEOMETRY_TYPE_PROPERTY_NAME;
},
[Ops.LineMetric]: () => 'currentLineMetric', // this variable is assumed to always be present in shaders, default is 0.
[Ops.Var]: (context, expression) => {
const firstArg = /** @type {LiteralExpression} */ (expression.args[0]);
const varName = /** @type {string} */ (firstArg.value);
const isExisting = varName in context.variables;
if (!isExisting) {
context.variables[varName] = {
name: varName,
type: expression.type,
};
}
let result = uniformNameForVariable(varName);
if (isType(expression.type, BooleanType)) {
result = `(${result} > 0.0)`;
}
return result;
},
[Ops.Has]: (context, expression) => {
const firstArg = /** @type {LiteralExpression} */ (expression.args[0]);
const propName = /** @type {string} */ (firstArg.value);
const isExisting = propName in context.properties;
if (!isExisting) {
context.properties[propName] = {
name: propName,
type: expression.type,
};
}
return `(a_prop_${propName} != ${numberToGlsl(UNDEFINED_PROP_VALUE)})`;
},
[Ops.Resolution]: () => 'u_resolution',
[Ops.Zoom]: () => 'u_zoom',
[Ops.Time]: () => 'u_time',
[Ops.Any]: createCompiler((compiledArgs) => `(${compiledArgs.join(` || `)})`),
[Ops.All]: createCompiler((compiledArgs) => `(${compiledArgs.join(` && `)})`),
[Ops.Not]: createCompiler(([value]) => `(!${value})`),
[Ops.Equal]: createCompiler(
([firstValue, secondValue]) => `(${firstValue} == ${secondValue})`,
),
[Ops.NotEqual]: createCompiler(
([firstValue, secondValue]) => `(${firstValue} != ${secondValue})`,
),
[Ops.GreaterThan]: createCompiler(
([firstValue, secondValue]) => `(${firstValue} > ${secondValue})`,
),
[Ops.GreaterThanOrEqualTo]: createCompiler(
([firstValue, secondValue]) => `(${firstValue} >= ${secondValue})`,
),
[Ops.LessThan]: createCompiler(
([firstValue, secondValue]) => `(${firstValue} < ${secondValue})`,
),
[Ops.LessThanOrEqualTo]: createCompiler(
([firstValue, secondValue]) => `(${firstValue} <= ${secondValue})`,
),
[Ops.Multiply]: createCompiler(
(compiledArgs) => `(${compiledArgs.join(' * ')})`,
),
[Ops.Divide]: createCompiler(
([firstValue, secondValue]) => `(${firstValue} / ${secondValue})`,
),
[Ops.Add]: createCompiler((compiledArgs) => `(${compiledArgs.join(' + ')})`),
[Ops.Subtract]: createCompiler(
([firstValue, secondValue]) => `(${firstValue} - ${secondValue})`,
),
[Ops.Clamp]: createCompiler(
([value, min, max]) => `clamp(${value}, ${min}, ${max})`,
),
[Ops.Mod]: createCompiler(([value, modulo]) => `mod(${value}, ${modulo})`),
[Ops.Pow]: createCompiler(([value, power]) => `pow(${value}, ${power})`),
[Ops.Abs]: createCompiler(([value]) => `abs(${value})`),
[Ops.Floor]: createCompiler(([value]) => `floor(${value})`),
[Ops.Ceil]: createCompiler(([value]) => `ceil(${value})`),
[Ops.Round]: createCompiler(([value]) => `floor(${value} + 0.5)`),
[Ops.Sin]: createCompiler(([value]) => `sin(${value})`),
[Ops.Cos]: createCompiler(([value]) => `cos(${value})`),
[Ops.Atan]: createCompiler(([firstValue, secondValue]) => {
return secondValue !== undefined
? `atan(${firstValue}, ${secondValue})`
: `atan(${firstValue})`;
}),
[Ops.Sqrt]: createCompiler(([value]) => `sqrt(${value})`),
[Ops.Match]: createCompiler((compiledArgs) => {
const input = compiledArgs[0];
const fallback = compiledArgs[compiledArgs.length - 1];
let result = null;
for (let i = compiledArgs.length - 3; i >= 1; i -= 2) {
const match = compiledArgs[i];
const output = compiledArgs[i + 1];
result = `(${input} == ${match} ? ${output} : ${result || fallback})`;
}
return result;
}),
[Ops.Between]: createCompiler(
([value, min, max]) => `(${value} >= ${min} && ${value} <= ${max})`,
),
[Ops.Interpolate]: createCompiler(([exponent, input, ...compiledArgs]) => {
let result = '';
for (let i = 0; i < compiledArgs.length - 2; i += 2) {
const stop1 = compiledArgs[i];
const output1 = result || compiledArgs[i + 1];
const stop2 = compiledArgs[i + 2];
const output2 = compiledArgs[i + 3];
let ratio;
if (exponent === numberToGlsl(1)) {
ratio = `(${input} - ${stop1}) / (${stop2} - ${stop1})`;
} else {
ratio = `(pow(${exponent}, (${input} - ${stop1})) - 1.0) / (pow(${exponent}, (${stop2} - ${stop1})) - 1.0)`;
}
result = `mix(${output1}, ${output2}, clamp(${ratio}, 0.0, 1.0))`;
}
return result;
}),
[Ops.Case]: createCompiler((compiledArgs) => {
const fallback = compiledArgs[compiledArgs.length - 1];
let result = null;
for (let i = compiledArgs.length - 3; i >= 0; i -= 2) {
const condition = compiledArgs[i];
const output = compiledArgs[i + 1];
result = `(${condition} ? ${output} : ${result || fallback})`;
}
return result;
}),
[Ops.In]: createCompiler(([needle, ...haystack], context) => {
const funcName = computeOperatorFunctionName('in', context);
const tests = [];
for (let i = 0; i < haystack.length; i += 1) {
tests.push(` if (inputValue == ${haystack[i]}) { return true; }`);
}
context.functions[funcName] = `bool ${funcName}(float inputValue) {
${tests.join('\n')}
return false;
}`;
return `${funcName}(${needle})`;
}),
[Ops.Array]: createCompiler(
(args) => `vec${args.length}(${args.join(', ')})`,
),
[Ops.Color]: createCompiler((compiledArgs) => {
if (compiledArgs.length === 1) {
//grayscale
return `vec4(vec3(${compiledArgs[0]} / 255.0), 1.0)`;
}
if (compiledArgs.length === 2) {
//grayscale with alpha
return `vec4(vec3(${compiledArgs[0]} / 255.0), ${compiledArgs[1]})`;
}
const rgb = compiledArgs.slice(0, 3).map((color) => `${color} / 255.0`);
if (compiledArgs.length === 3) {
return `vec4(${rgb.join(', ')}, 1.0)`;
}
const alpha = compiledArgs[3];
return `vec4(${rgb.join(', ')}, ${alpha})`;
}),
[Ops.Band]: createCompiler(([band, xOffset, yOffset], context) => {
if (!(GET_BAND_VALUE_FUNC in context.functions)) {
let ifBlocks = '';
const bandCount = context.bandCount || 1;
for (let i = 0; i < bandCount; i++) {
const colorIndex = Math.floor(i / 4);
let bandIndex = i % 4;
if (i === bandCount - 1 && bandIndex === 1) {
// LUMINANCE_ALPHA - band 1 assigned to rgb and band 2 assigned to alpha
bandIndex = 3;
}
const textureName = `${Uniforms.TILE_TEXTURE_ARRAY}[${colorIndex}]`;
ifBlocks += ` if (band == ${i + 1}.0) {
return texture2D(${textureName}, v_textureCoord + vec2(dx, dy))[${bandIndex}];
}
`;
}
context.functions[GET_BAND_VALUE_FUNC] =
`float getBandValue(float band, float xOffset, float yOffset) {
float dx = xOffset / ${Uniforms.TEXTURE_PIXEL_WIDTH};
float dy = yOffset / ${Uniforms.TEXTURE_PIXEL_HEIGHT};
${ifBlocks}
}`;
}
return `${GET_BAND_VALUE_FUNC}(${band}, ${xOffset ?? '0.0'}, ${
yOffset ?? '0.0'
})`;
}),
[Ops.Palette]: (context, expression) => {
const [index, ...colors] = expression.args;
const numColors = colors.length;
const palette = new Uint8Array(numColors * 4);
for (let i = 0; i < colors.length; i++) {
const parsedValue = /** @type {string | Array<number>} */ (
/** @type {LiteralExpression} */ (colors[i]).value
);
const color = asArray(parsedValue);
const offset = i * 4;
palette[offset] = color[0];
palette[offset + 1] = color[1];
palette[offset + 2] = color[2];
palette[offset + 3] = color[3] * 255;
}
if (!context.paletteTextures) {
context.paletteTextures = [];
}
const paletteName = `${PALETTE_TEXTURE_ARRAY}[${context.paletteTextures.length}]`;
const paletteTexture = new PaletteTexture(paletteName, palette);
context.paletteTextures.push(paletteTexture);
const compiledIndex = compile(index, NumberType, context);
return `texture2D(${paletteName}, vec2((${compiledIndex} + 0.5) / ${numColors}.0, 0.5))`;
},
// TODO: unimplemented
// Ops.Number
// Ops.String
// Ops.Coalesce
// Ops.Concat
// Ops.ToString
};
/**
* @param {Expression} expression The expression.
* @param {number} returnType The expected return type.
* @param {CompilationContext} context The compilation context.
* @return {CompiledExpression} The compiled expression
*/
function compile(expression, returnType, context) {
// operator
if (expression instanceof CallExpression) {
const compiler = compilers[expression.operator];
if (compiler === undefined) {
throw new Error(
`No compiler defined for this operator: ${JSON.stringify(
expression.operator,
)}`,
);
}
return compiler(context, expression, returnType);
}
if ((expression.type & NumberType) > 0) {
return numberToGlsl(/** @type {number} */ (expression.value));
}
if ((expression.type & BooleanType) > 0) {
return expression.value.toString();
}
if ((expression.type & StringType) > 0) {
return stringToGlsl(expression.value.toString());
}
if ((expression.type & ColorType) > 0) {
return colorToGlsl(
/** @type {Array<number> | string} */ (expression.value),
);
}
if ((expression.type & NumberArrayType) > 0) {
return arrayToGlsl(/** @type {Array<number>} */ (expression.value));
}
if ((expression.type & SizeType) > 0) {
return sizeToGlsl(
/** @type {number|import('../size.js').Size} */ (expression.value),
);
}
throw new Error(
`Unexpected expression ${expression.value} (expected type ${typeName(
returnType,
)})`,
);
}