@maplibre/maplibre-gl-style-spec
Version:
a specification for maplibre styles
656 lines (610 loc) • 21.6 kB
text/typescript
import {
typeToString,
NumberType,
StringType,
BooleanType,
ColorType,
ObjectType,
ValueType,
ErrorType,
CollatorType,
array
} from './types';
import {ParsingContext} from './parsing_context';
import {EvaluationContext} from './evaluation_context';
import {expressions} from './definitions';
import {CollatorExpression} from './definitions/collator';
import {Within} from './definitions/within';
import {Literal} from './definitions/literal';
import {Assertion} from './definitions/assertion';
import {Coercion} from './definitions/coercion';
import {Var} from './definitions/var';
import {Distance} from './definitions/distance';
import {GlobalState} from './definitions/global_state';
import type {Expression, ExpressionRegistry} from './expression';
import type {Value} from './values';
import type {Type} from './types';
import {typeOf, validateRGBA, valueToString} from './values';
import {RuntimeError} from './runtime_error';
import {Color} from './types/color';
export type Varargs = {
type: Type;
};
type Signature = Array<Type> | Varargs;
type Evaluate = (b: EvaluationContext, a: Array<Expression>) => Value;
type Definition =
| [Type, Signature, Evaluate]
| {
type: Type;
overloads: Array<[Signature, Evaluate]>;
};
export class CompoundExpression implements Expression {
name: string;
type: Type;
_evaluate: Evaluate;
args: Array<Expression>;
static definitions: {[_: string]: Definition};
constructor(name: string, type: Type, evaluate: Evaluate, args: Array<Expression>) {
this.name = name;
this.type = type;
this._evaluate = evaluate;
this.args = args;
}
evaluate(ctx: EvaluationContext) {
return this._evaluate(ctx, this.args);
}
eachChild(fn: (_: Expression) => void) {
this.args.forEach(fn);
}
outputDefined() {
return false;
}
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
const op: string = args[0] as any;
const definition = CompoundExpression.definitions[op];
if (!definition) {
return context.error(
`Unknown expression "${op}". If you wanted a literal array, use ["literal", [...]].`,
0
) as null;
}
// Now check argument types against each signature
const type = Array.isArray(definition) ? definition[0] : definition.type;
const availableOverloads = Array.isArray(definition)
? [[definition[1], definition[2]]]
: definition.overloads;
const overloads = availableOverloads.filter(
([signature]) =>
!Array.isArray(signature) || // varags
signature.length === args.length - 1 // correct param count
);
let signatureContext: ParsingContext = null;
for (const [params, evaluate] of overloads) {
// Use a fresh context for each attempted signature so that, if
// we eventually succeed, we haven't polluted `context.errors`.
signatureContext = new ParsingContext(
context.registry,
isExpressionConstant,
context.path,
null,
context.scope
);
// First parse all the args, potentially coercing to the
// types expected by this overload.
const parsedArgs: Array<Expression> = [];
let argParseFailed = false;
for (let i = 1; i < args.length; i++) {
const arg = args[i];
const expectedType = Array.isArray(params)
? params[i - 1]
: (params as Varargs).type;
const parsed = signatureContext.parse(arg, 1 + parsedArgs.length, expectedType);
if (!parsed) {
argParseFailed = true;
break;
}
parsedArgs.push(parsed);
}
if (argParseFailed) {
// Couldn't coerce args of this overload to expected type, move
// on to next one.
continue;
}
if (Array.isArray(params)) {
if (params.length !== parsedArgs.length) {
signatureContext.error(
`Expected ${params.length} arguments, but found ${parsedArgs.length} instead.`
);
continue;
}
}
for (let i = 0; i < parsedArgs.length; i++) {
const expected = Array.isArray(params) ? params[i] : (params as Varargs).type;
const arg = parsedArgs[i];
signatureContext.concat(i + 1).checkSubtype(expected, arg.type);
}
if (signatureContext.errors.length === 0) {
return new CompoundExpression(op, type, evaluate as Evaluate, parsedArgs);
}
}
if (overloads.length === 1) {
context.errors.push(...signatureContext.errors);
} else {
const expected = overloads.length ? overloads : availableOverloads;
const signatures = expected
.map(([params]) => stringifySignature(params as Signature))
.join(' | ');
const actualTypes = [];
// For error message, re-parse arguments without trying to
// apply any coercions
for (let i = 1; i < args.length; i++) {
const parsed = context.parse(args[i], 1 + actualTypes.length);
if (!parsed) return null;
actualTypes.push(typeToString(parsed.type));
}
context.error(
`Expected arguments of type ${signatures}, but found (${actualTypes.join(', ')}) instead.`
);
}
return null;
}
static register(registry: ExpressionRegistry, definitions: {[_: string]: Definition}) {
CompoundExpression.definitions = definitions;
for (const name in definitions) {
registry[name] = CompoundExpression;
}
}
}
function rgba(ctx, [r, g, b, a]) {
r = r.evaluate(ctx);
g = g.evaluate(ctx);
b = b.evaluate(ctx);
const alpha = a ? a.evaluate(ctx) : 1;
const error = validateRGBA(r, g, b, alpha);
if (error) throw new RuntimeError(error);
return new Color(r / 255, g / 255, b / 255, alpha, false);
}
function has(key, obj) {
return key in obj && obj[key] !== undefined;
}
function get(key, obj) {
const v = obj[key];
return typeof v === 'undefined' ? null : v;
}
function binarySearch(v, a, i, j) {
while (i <= j) {
const m = (i + j) >> 1;
if (a[m] === v) return true;
if (a[m] > v) j = m - 1;
else i = m + 1;
}
return false;
}
function varargs(type: Type): Varargs {
return {type};
}
CompoundExpression.register(expressions, {
error: [
ErrorType,
[StringType],
(ctx, [v]) => {
throw new RuntimeError(v.evaluate(ctx));
}
],
typeof: [StringType, [ValueType], (ctx, [v]) => typeToString(typeOf(v.evaluate(ctx)))],
'to-rgba': [
array(NumberType, 4),
[ColorType],
(ctx, [v]) => {
const [r, g, b, a] = v.evaluate(ctx).rgb;
return [r * 255, g * 255, b * 255, a];
}
],
rgb: [ColorType, [NumberType, NumberType, NumberType], rgba],
rgba: [ColorType, [NumberType, NumberType, NumberType, NumberType], rgba],
has: {
type: BooleanType,
overloads: [
[[StringType], (ctx, [key]) => has(key.evaluate(ctx), ctx.properties())],
[
[StringType, ObjectType],
(ctx, [key, obj]) => has(key.evaluate(ctx), obj.evaluate(ctx))
]
]
},
get: {
type: ValueType,
overloads: [
[[StringType], (ctx, [key]) => get(key.evaluate(ctx), ctx.properties())],
[
[StringType, ObjectType],
(ctx, [key, obj]) => get(key.evaluate(ctx), obj.evaluate(ctx))
]
]
},
'feature-state': [
ValueType,
[StringType],
(ctx, [key]) => get(key.evaluate(ctx), ctx.featureState || {})
],
properties: [ObjectType, [], (ctx) => ctx.properties()],
'geometry-type': [StringType, [], (ctx) => ctx.geometryType()],
id: [ValueType, [], (ctx) => ctx.id()],
zoom: [NumberType, [], (ctx) => ctx.globals.zoom],
'heatmap-density': [NumberType, [], (ctx) => ctx.globals.heatmapDensity || 0],
elevation: [NumberType, [], (ctx) => ctx.globals.elevation || 0],
'line-progress': [NumberType, [], (ctx) => ctx.globals.lineProgress || 0],
accumulated: [
ValueType,
[],
(ctx) => (ctx.globals.accumulated === undefined ? null : ctx.globals.accumulated)
],
'+': [
NumberType,
varargs(NumberType),
(ctx, args) => {
let result = 0;
for (const arg of args) {
result += arg.evaluate(ctx);
}
return result;
}
],
'*': [
NumberType,
varargs(NumberType),
(ctx, args) => {
let result = 1;
for (const arg of args) {
result *= arg.evaluate(ctx);
}
return result;
}
],
'-': {
type: NumberType,
overloads: [
[[NumberType, NumberType], (ctx, [a, b]) => a.evaluate(ctx) - b.evaluate(ctx)],
[[NumberType], (ctx, [a]) => -a.evaluate(ctx)]
]
},
'/': [NumberType, [NumberType, NumberType], (ctx, [a, b]) => a.evaluate(ctx) / b.evaluate(ctx)],
'%': [NumberType, [NumberType, NumberType], (ctx, [a, b]) => a.evaluate(ctx) % b.evaluate(ctx)],
ln2: [NumberType, [], () => Math.LN2],
pi: [NumberType, [], () => Math.PI],
e: [NumberType, [], () => Math.E],
'^': [
NumberType,
[NumberType, NumberType],
(ctx, [b, e]) => Math.pow(b.evaluate(ctx), e.evaluate(ctx))
],
sqrt: [NumberType, [NumberType], (ctx, [x]) => Math.sqrt(x.evaluate(ctx))],
log10: [NumberType, [NumberType], (ctx, [n]) => Math.log(n.evaluate(ctx)) / Math.LN10],
ln: [NumberType, [NumberType], (ctx, [n]) => Math.log(n.evaluate(ctx))],
log2: [NumberType, [NumberType], (ctx, [n]) => Math.log(n.evaluate(ctx)) / Math.LN2],
sin: [NumberType, [NumberType], (ctx, [n]) => Math.sin(n.evaluate(ctx))],
cos: [NumberType, [NumberType], (ctx, [n]) => Math.cos(n.evaluate(ctx))],
tan: [NumberType, [NumberType], (ctx, [n]) => Math.tan(n.evaluate(ctx))],
asin: [NumberType, [NumberType], (ctx, [n]) => Math.asin(n.evaluate(ctx))],
acos: [NumberType, [NumberType], (ctx, [n]) => Math.acos(n.evaluate(ctx))],
atan: [NumberType, [NumberType], (ctx, [n]) => Math.atan(n.evaluate(ctx))],
min: [
NumberType,
varargs(NumberType),
(ctx, args) => Math.min(...args.map((arg) => arg.evaluate(ctx)))
],
max: [
NumberType,
varargs(NumberType),
(ctx, args) => Math.max(...args.map((arg) => arg.evaluate(ctx)))
],
abs: [NumberType, [NumberType], (ctx, [n]) => Math.abs(n.evaluate(ctx))],
round: [
NumberType,
[NumberType],
(ctx, [n]) => {
const v = n.evaluate(ctx);
// Javascript's Math.round() rounds towards +Infinity for halfway
// values, even when they're negative. It's more common to round
// away from 0 (e.g., this is what python and C++ do)
return v < 0 ? -Math.round(-v) : Math.round(v);
}
],
floor: [NumberType, [NumberType], (ctx, [n]) => Math.floor(n.evaluate(ctx))],
ceil: [NumberType, [NumberType], (ctx, [n]) => Math.ceil(n.evaluate(ctx))],
'filter-==': [
BooleanType,
[StringType, ValueType],
(ctx, [k, v]) => ctx.properties()[(k as any).value] === (v as any).value
],
'filter-id-==': [BooleanType, [ValueType], (ctx, [v]) => ctx.id() === (v as any).value],
'filter-type-==': [
BooleanType,
[StringType],
(ctx, [v]) => ctx.geometryType() === (v as any).value
],
'filter-<': [
BooleanType,
[StringType, ValueType],
(ctx, [k, v]) => {
const a = ctx.properties()[(k as any).value];
const b = (v as any).value;
return typeof a === typeof b && a < b;
}
],
'filter-id-<': [
BooleanType,
[ValueType],
(ctx, [v]) => {
const a = ctx.id();
const b = (v as any).value;
return typeof a === typeof b && a < b;
}
],
'filter->': [
BooleanType,
[StringType, ValueType],
(ctx, [k, v]) => {
const a = ctx.properties()[(k as any).value];
const b = (v as any).value;
return typeof a === typeof b && a > b;
}
],
'filter-id->': [
BooleanType,
[ValueType],
(ctx, [v]) => {
const a = ctx.id();
const b = (v as any).value;
return typeof a === typeof b && a > b;
}
],
'filter-<=': [
BooleanType,
[StringType, ValueType],
(ctx, [k, v]) => {
const a = ctx.properties()[(k as any).value];
const b = (v as any).value;
return typeof a === typeof b && a <= b;
}
],
'filter-id-<=': [
BooleanType,
[ValueType],
(ctx, [v]) => {
const a = ctx.id();
const b = (v as any).value;
return typeof a === typeof b && a <= b;
}
],
'filter->=': [
BooleanType,
[StringType, ValueType],
(ctx, [k, v]) => {
const a = ctx.properties()[(k as any).value];
const b = (v as any).value;
return typeof a === typeof b && a >= b;
}
],
'filter-id->=': [
BooleanType,
[ValueType],
(ctx, [v]) => {
const a = ctx.id();
const b = (v as any).value;
return typeof a === typeof b && a >= b;
}
],
'filter-has': [
BooleanType,
[ValueType],
(ctx, [k]) => {
const key = (k as any).value;
const props = ctx.properties();
return key in props && props[key] !== undefined;
}
],
'filter-has-id': [BooleanType, [], (ctx) => ctx.id() !== null && ctx.id() !== undefined],
'filter-type-in': [
BooleanType,
[array(StringType)],
(ctx, [v]) => (v as any).value.indexOf(ctx.geometryType()) >= 0
],
'filter-id-in': [
BooleanType,
[array(ValueType)],
(ctx, [v]) => (v as any).value.indexOf(ctx.id()) >= 0
],
'filter-in-small': [
BooleanType,
[StringType, array(ValueType)],
// assumes v is an array literal
(ctx, [k, v]) => (v as any).value.indexOf(ctx.properties()[(k as any).value]) >= 0
],
'filter-in-large': [
BooleanType,
[StringType, array(ValueType)],
// assumes v is a array literal with values sorted in ascending order and of a single type
(ctx, [k, v]) =>
binarySearch(
ctx.properties()[(k as any).value],
(v as any).value,
0,
(v as any).value.length - 1
)
],
all: {
type: BooleanType,
overloads: [
[[BooleanType, BooleanType], (ctx, [a, b]) => a.evaluate(ctx) && b.evaluate(ctx)],
[
varargs(BooleanType),
(ctx, args) => {
for (const arg of args) {
if (!arg.evaluate(ctx)) return false;
}
return true;
}
]
]
},
any: {
type: BooleanType,
overloads: [
[[BooleanType, BooleanType], (ctx, [a, b]) => a.evaluate(ctx) || b.evaluate(ctx)],
[
varargs(BooleanType),
(ctx, args) => {
for (const arg of args) {
if (arg.evaluate(ctx)) return true;
}
return false;
}
]
]
},
'!': [BooleanType, [BooleanType], (ctx, [b]) => !b.evaluate(ctx)],
'is-supported-script': [
BooleanType,
[StringType],
// At parse time this will always return true, so we need to exclude this expression with isGlobalPropertyConstant
(ctx, [s]) => {
const isSupportedScript = ctx.globals && ctx.globals.isSupportedScript;
if (isSupportedScript) {
return isSupportedScript(s.evaluate(ctx));
}
return true;
}
],
upcase: [StringType, [StringType], (ctx, [s]) => s.evaluate(ctx).toUpperCase()],
downcase: [StringType, [StringType], (ctx, [s]) => s.evaluate(ctx).toLowerCase()],
concat: [
StringType,
varargs(ValueType),
(ctx, args) => args.map((arg) => valueToString(arg.evaluate(ctx))).join('')
],
split: [
array(StringType),
[StringType, StringType],
(ctx, [s, delim]) => s.evaluate(ctx).split(delim.evaluate(ctx))
],
join: [
StringType,
[array(StringType), StringType],
(ctx, [arr, delim]) => arr.evaluate(ctx).join(delim.evaluate(ctx))
],
'resolved-locale': [
StringType,
[CollatorType],
(ctx, [collator]) => collator.evaluate(ctx).resolvedLocale()
]
});
function stringifySignature(signature: Signature): string {
if (Array.isArray(signature)) {
return `(${signature.map(typeToString).join(', ')})`;
} else {
return `(${typeToString(signature.type)}...)`;
}
}
function isExpressionConstant(expression: Expression) {
if (expression instanceof Var) {
return isExpressionConstant(expression.boundExpression);
} else if (expression instanceof CompoundExpression && expression.name === 'error') {
return false;
} else if (expression instanceof CollatorExpression) {
// Although the results of a Collator expression with fixed arguments
// generally shouldn't change between executions, we can't serialize them
// as constant expressions because results change based on environment.
return false;
} else if (expression instanceof Within) {
return false;
} else if (expression instanceof Distance) {
return false;
} else if (expression instanceof GlobalState) {
return false;
}
const isTypeAnnotation = expression instanceof Coercion || expression instanceof Assertion;
let childrenConstant = true;
expression.eachChild((child) => {
// We can _almost_ assume that if `expressions` children are constant,
// they would already have been evaluated to Literal values when they
// were parsed. Type annotations are the exception, because they might
// have been inferred and added after a child was parsed.
// So we recurse into isConstant() for the children of type annotations,
// but otherwise simply check whether they are Literals.
if (isTypeAnnotation) {
childrenConstant = childrenConstant && isExpressionConstant(child);
} else {
childrenConstant = childrenConstant && child instanceof Literal;
}
});
if (!childrenConstant) {
return false;
}
return (
isFeatureConstant(expression) &&
isGlobalPropertyConstant(expression, [
'zoom',
'heatmap-density',
'elevation',
'line-progress',
'accumulated',
'is-supported-script'
])
);
}
function isFeatureConstant(e: Expression) {
if (e instanceof CompoundExpression) {
if (e.name === 'get' && e.args.length === 1) {
return false;
} else if (e.name === 'feature-state') {
return false;
} else if (e.name === 'has' && e.args.length === 1) {
return false;
} else if (e.name === 'properties' || e.name === 'geometry-type' || e.name === 'id') {
return false;
} else if (/^filter-/.test(e.name)) {
return false;
}
}
if (e instanceof Within) {
return false;
}
if (e instanceof Distance) {
return false;
}
let result = true;
e.eachChild((arg) => {
if (result && !isFeatureConstant(arg)) {
result = false;
}
});
return result;
}
function isStateConstant(e: Expression) {
if (e instanceof CompoundExpression) {
if (e.name === 'feature-state') {
return false;
}
}
let result = true;
e.eachChild((arg) => {
if (result && !isStateConstant(arg)) {
result = false;
}
});
return result;
}
function isGlobalPropertyConstant(e: Expression, properties: Array<string>) {
if (e instanceof CompoundExpression && properties.indexOf(e.name) >= 0) {
return false;
}
let result = true;
e.eachChild((arg) => {
if (result && !isGlobalPropertyConstant(arg, properties)) {
result = false;
}
});
return result;
}
export {isFeatureConstant, isGlobalPropertyConstant, isStateConstant, isExpressionConstant};