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 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 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 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; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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