Skip to content

Instantly share code, notes, and snippets.

@danvk
Created October 28, 2019 15:33
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danvk/4378b6936f9cd634fc8c9f69c4f18b81 to your computer and use it in GitHub Desktop.
Save danvk/4378b6936f9cd634fc8c9f69c4f18b81 to your computer and use it in GitHub Desktop.
TypeScript API for Mapbox GL Style Expressions
import {Expression} from './expression';
describe('Expression', () => {
it('should work with constants', () => {
expect(Expression.parse(0).evaluate(null!)).toEqual(0);
expect(Expression.parse(10).evaluate(null!)).toEqual(10);
});
it('should add', () => {
expect(Expression.parse(['+', 1, 2]).evaluate(null!)).toEqual(3);
});
it('should handle simple accessors', () => {
expect(
Expression.parse(['get', 'height']).evaluate({
type: 'Feature',
geometry: null!,
properties: {
height: 10,
},
}),
).toEqual(10);
});
it('should handle match expressions', () => {
const matchExpr = Expression.parse([
'match',
['get', 'floor_type'],
'residential',
'#ffd37b',
['loft1', 'loft2', 'loft3'],
'#00a9d8',
'stoa',
'purple',
'white',
]);
const f = (type: string): Feature => ({
type: 'Feature',
geometry: null!,
properties: {
floor_type: type,
},
});
expect(matchExpr.evaluate(f('residential'))).toEqual('#ffd37b');
expect(matchExpr.evaluate(f('loft2'))).toEqual('#00a9d8');
expect(matchExpr.evaluate(f('stoa'))).toEqual('purple');
expect(matchExpr.evaluate(f('loft7'))).toEqual('white');
});
// Helper to construct Mapbox-style RGB objects.
const rgb = (r: number, g: number, b: number) => ({r, g, b, a: 1});
it('should evaluate colors', () => {
expect(Expression.parse('black', 'color').evaluate(null!)).toEqual(rgb(0, 0, 0));
expect(Expression.parse('white', 'color').evaluate(null!)).toEqual(rgb(1, 1, 1));
});
it('should interpolate colors', () => {
const evalExpr = (x: number) =>
Expression.parse(
['interpolate', ['linear'], x, 0, 'rgb(0, 0, 0)', 1, 'rgb(255, 0, 255)'],
'color',
).evaluate(null!);
expect(evalExpr(0)).toEqual(rgb(0, 0, 0));
expect(evalExpr(1)).toEqual(rgb(1, 0, 1));
expect(evalExpr(0.5)).toEqual(rgb(0.5, 0, 0.5));
});
it('should reject expressions which return the wrong type of value', () => {
expect(() => Expression.parse('black', 'number')).toThrow(/Expected number/);
expect(() => Expression.parse(0, 'string')).toThrow(/Expected string/);
expect(() => Expression.parse(0, 'color')).toThrow(/Expected color/);
expect(() => Expression.parse('non-color', 'color')).toThrow(/Could not parse color/);
});
});
import {Feature} from 'geojson';
import {Expression as MapboxExpression, StyleFunction} from 'mapbox-gl';
import {expression} from 'mapbox-gl/dist/style-spec';
// TODO(danvk): pass down the real zoom level.
const expressionGlobals = {
zoom: 14,
};
/** A color as returned by a Mapbox style expression. All values are in [0, 1] */
export interface RGBA {
r: number;
g: number;
b: number;
a: number;
}
interface TypeMap {
string: string;
number: number;
color: RGBA;
boolean: boolean;
[other: string]: any;
}
/**
* Class for working with Mapbox style expressions.
*
* See https://docs.mapbox.com/mapbox-gl-js/style-spec/#expressions
*/
export class Expression<T> {
/**
* Parse a Mapbox style expression.
*
* Pass an expected type to get tigher error checking and more precise types.
*/
static parse<T extends expression.StylePropertyType>(
expr: number | string | Readonly<StyleFunction> | Readonly<MapboxExpression> | undefined,
expectedType?: T,
): Expression<TypeMap[T]> {
// For details on use of this private API and plans to publicize it, see
// https://github.com/mapbox/mapbox-gl-js/issues/7670
let parseResult: expression.ParseResult;
if (expectedType) {
parseResult = expression.createExpression(expr, {type: expectedType});
if (parseResult.result === 'success') {
return new Expression<TypeMap[T]>(parseResult.value);
}
} else {
parseResult = expression.createExpression(expr);
if (parseResult.result === 'success') {
return new Expression<any>(parseResult.value);
}
}
throw parseResult.value[0];
}
constructor(public parsedExpression: expression.StyleExpression) {}
evaluate(feature: Feature): T {
return this.parsedExpression.evaluate(expressionGlobals, feature);
}
}
declare module 'mapbox-gl/dist/style-spec' {
import {Feature} from 'geojson';
export namespace expression {
export type FeatureState = {[key: string]: any};
export type GlobalProperties = Readonly<{
zoom: number;
heatmapDensity?: number;
lineProgress?: number;
isSupportedScript?: (script: string) => boolean;
accumulated?: any;
}>;
interface StyleExpression {
expression: any;
evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState): any;
evaluateWithoutErrorHandling(
globals: GlobalProperties,
feature?: Feature,
featureState?: FeatureState,
): any;
}
export interface ParseResultSuccess {
result: 'success';
value: StyleExpression;
}
export interface ParsingError extends Error {
key: string;
message: string;
}
export interface ParseResultError {
result: 'error';
value: ParsingError[];
}
export type ParseResult = ParseResultSuccess | ParseResultError;
export type StylePropertyType =
| 'color'
| 'string'
| 'number'
| 'enum'
| 'boolean'
| 'formatted'
| 'image';
export interface StylePropertySpecification {
type: StylePropertyType;
}
export function createExpression(
expr: any,
propertySpec?: StylePropertySpecification,
): ParseResult;
}
}
@lbutler
Copy link

lbutler commented Mar 23, 2020

Hi @lbutler, glad you like it! Does that change affect this gist? createFilter doesn't appear in it — it only calls createExpression.

Apologies! I had used your code in this Gist as a base and then added some extra lines to include featureFilter and didn't notice that it was my own work that was falling over...

I'll update my previous comment, thanks again @danvk

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment