Created
October 28, 2019 15:33
-
-
Save danvk/4378b6936f9cd634fc8c9f69c4f18b81 to your computer and use it in GitHub Desktop.
TypeScript API for Mapbox GL Style Expressions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/); | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
Hi @lbutler, glad you like it! Does that change affect this gist? createFilter
doesn't appear in it — it only calls createExpression
.
Hi @lbutler, glad you like it! Does that change affect this gist?
createFilter
doesn't appear in it — it only callscreateExpression
.
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
Hi @danvk - Thanks for this awesome gist.
Just a heads up for anyone using this, thecreateFilter()
method was modified recently on master to add support for thewithin
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