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 @danvk - Thanks for this awesome gist.

Just a heads up for anyone using this, the createFilter() method was modified recently on master to add support for the within expression and now it returns an object with the filter method attached instead of the methods directly.

It's not broken on the latest published release but may break soon, let's hope we get a public API soon!

Changes to the API can be seen on this commit

@danvk
Copy link
Author

danvk 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.

@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