From 9853755f1efe8db35c5b627bb7439c88e40ab494 Mon Sep 17 00:00:00 2001 From: Harel M Date: Fri, 5 Aug 2022 06:36:11 +0300 Subject: [PATCH] Improve filter specification typings (#1390) Fixes #1380 * Add typing for expressions and fix filter expressions. (#1383) improve filter conversion by adding typing and helping typescript to guess types. add tests for filters mentioned in issue #1380. * Change else-if to switch case * lint * Update CHANGELOG.md * Added some tests, fixed failing test * Fix failing test * Fix lint * Fix lint * Fix test * Fix code and added tests * Remove links in changelog * Fix lint Co-authored-by: cns-solutions-admin <59872815+cns-solutions-admin@users.noreply.github.com> --- CHANGELOG.md | 5 +- build/generate-style-spec.ts | 182 ++++++++++++++---- src/data/bucket/fill_bucket.test.ts | 1 + src/style-spec/feature_filter/convert.ts | 130 ++++++------- .../feature_filter/feature_filter.test.ts | 47 ++++- src/style-spec/feature_filter/index.ts | 3 +- src/style-spec/migrate/expressions.ts | 2 +- src/style/light.test.ts | 2 +- 8 files changed, 265 insertions(+), 107 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0f019d83b..75a70464a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,13 +3,14 @@ ### ✨ Features and improvements - *...Add new stuff here...* -- Update `icon-padding` symbol layout property to support asymmetric padding [#1289](https://github.com/maplibre/maplibre-gl-js/pull/1289) +- Update `icon-padding` symbol layout property to support asymmetric padding (#1289) - Added `cooperativeGestures` option when instantiating map to prevent inadvertent scrolling/panning when navigating a page where map is embedded inline (#234) +- Improve filter specification typings (#1390) ### 🐞 Bug fixes - *...Add new stuff here...* -- Fix compact attribution style when using global CSS that sets `box-sizing: border-box;` ([#1250](https://github.com/maplibre/maplibre-gl-js/pull/1250)) +- Fix compact attribution style when using global CSS that sets `box-sizing: border-box;` (#1250) ## 2.2.0-pre.3 diff --git a/build/generate-style-spec.ts b/build/generate-style-spec.ts index ad27889bd4..61756ef563 100644 --- a/build/generate-style-spec.ts +++ b/build/generate-style-spec.ts @@ -48,7 +48,7 @@ function propertyType(property) { } else if (properties.supportsZoomExpression(property)) { return `PropertyValueSpecification<${baseType}>`; } else if (property.expression) { - return 'ExpressionSpecificationArray'; + return 'ExpressionSpecification'; } else { return baseType; } @@ -128,36 +128,152 @@ export type ResolvedImageSpecification = string; export type PromoteIdSpecification = {[_: string]: string} | string; -export type FilterSpecificationInputType = string | number | boolean; -export type FilterSpecification = - // Lookup - | ['at', number, (number |string)[]] - | ['get', string, Record?] - | ['has', string, Record?] - | ['in', ...FilterSpecificationInputType[], FilterSpecificationInputType | FilterSpecificationInputType[]] - | ['index-of', FilterSpecificationInputType, FilterSpecificationInputType | FilterSpecificationInputType[]] - | ['length', string | string[]] - | ['slice', string | string[], number] +export type ExpressionInputType = string | number | boolean; + +export type CollatorExpressionSpecification = + ['collator', { + 'case-sensitive'?: boolean | ExpressionSpecification, + 'diacritic-sensitive'?: boolean | ExpressionSpecification, + locale?: string | ExpressionSpecification} + ]; // collator + +export type InterpolationSpecification = + | ['linear'] + | ['exponential', number | ExpressionSpecification] + | ['cubic-bezier', number | ExpressionSpecification, number | ExpressionSpecification, number | ExpressionSpecification, number | ExpressionSpecification] + +export type ExpressionSpecification = + // types + | ['array', unknown | ExpressionSpecification] // array + | ['array', ExpressionInputType | ExpressionSpecification, unknown | ExpressionSpecification] // array + | ['array', ExpressionInputType | ExpressionSpecification, number | ExpressionSpecification, unknown | ExpressionSpecification] // array + | ['boolean', ...(unknown | ExpressionSpecification)[], unknown | ExpressionSpecification] // boolean + | CollatorExpressionSpecification + | ['format', ...(string | ['image', ExpressionSpecification] | ExpressionSpecification | {'font-scale'?: number | ExpressionSpecification, 'text-font'?: string[] | ExpressionSpecification, 'text-color': ColorSpecification | ExpressionSpecification})[]] // string + | ['image', unknown | ExpressionSpecification] // image + | ['literal', unknown] + | ['number', unknown | ExpressionSpecification, ...(unknown | ExpressionSpecification)[]] // number + | ['number-format', number | ExpressionSpecification, {'locale'?: string | ExpressionSpecification, 'currency'?: string | ExpressionSpecification, 'min-fraction-digits'?: number | ExpressionSpecification, 'max-fraction-digits'?: number | ExpressionSpecification}] // string + | ['object', unknown | ExpressionSpecification, ...(unknown | ExpressionSpecification)[]] // object + | ['string', unknown | ExpressionSpecification, ...(unknown | ExpressionSpecification)[]] // string + | ['to-boolean', unknown | ExpressionSpecification] // boolean + | ['to-color', unknown | ExpressionSpecification, ...(unknown | ExpressionSpecification)[]] // color + | ['to-number', unknown | ExpressionSpecification, ...(unknown | ExpressionSpecification)[]] // number + | ['to-string', unknown | ExpressionSpecification] // string + // feature data + | ['accumulated'] + | ['feature-state', string] + | ['geometry-type'] // string + | ['id'] + | ['line-progress'] // number + | ['properties'] // object + // lookup + | ['at', number | ExpressionSpecification, ExpressionSpecification] + | ['get', string | ExpressionSpecification, (Record | ExpressionSpecification)?] + | ['has', string | ExpressionSpecification, (Record | ExpressionSpecification)?] + | ['in', ExpressionInputType | ExpressionSpecification, ExpressionInputType | ExpressionSpecification] + | ['index-of', ExpressionInputType | ExpressionSpecification, ExpressionInputType | ExpressionSpecification] // number + | ['length', string | ExpressionSpecification] + | ['slice', string | ExpressionSpecification, number | ExpressionSpecification] // Decision - | ['!', FilterSpecification] - | ['!=', string | FilterSpecification, FilterSpecificationInputType] - | ['<', string | FilterSpecification, FilterSpecificationInputType] - | ['<=', string | FilterSpecification, FilterSpecificationInputType] - | ['==', string | FilterSpecification, FilterSpecificationInputType] - | ['>', string | FilterSpecification, FilterSpecificationInputType] - | ['>=', string | FilterSpecification, FilterSpecificationInputType] - | ["all", ...FilterSpecification[], FilterSpecificationInputType] - | ["any", ...FilterSpecification[], FilterSpecificationInputType] - | ["case", ...FilterSpecification[], FilterSpecificationInputType] - | ["coalesce", ...FilterSpecification[], FilterSpecificationInputType] - | ["match", ...FilterSpecification[], FilterSpecificationInputType] - | ["within", ...FilterSpecification[], FilterSpecificationInputType] - // Used in convert.ts - | ["!in", ...FilterSpecification[], FilterSpecificationInputType] - | ["!has", ...FilterSpecification[], FilterSpecificationInputType] - | ["none", ...FilterSpecification[], FilterSpecificationInputType] - // Fallbak for others - | Array + | ['!', boolean | ExpressionSpecification] // boolean + | ['!=', ExpressionInputType | ExpressionSpecification, ExpressionInputType | ExpressionSpecification, CollatorExpressionSpecification?] // boolean + | ['<', ExpressionInputType | ExpressionSpecification, ExpressionInputType | ExpressionSpecification, CollatorExpressionSpecification?] // boolean + | ['<=', ExpressionInputType | ExpressionSpecification, ExpressionInputType | ExpressionSpecification, CollatorExpressionSpecification?] // boolean + | ['==', ExpressionInputType | ExpressionSpecification, ExpressionInputType | ExpressionSpecification, CollatorExpressionSpecification?] // boolean + | ['>', ExpressionInputType | ExpressionSpecification, ExpressionInputType | ExpressionSpecification, CollatorExpressionSpecification?] // boolean + | ['>=', ExpressionInputType | ExpressionSpecification, ExpressionInputType | ExpressionSpecification, CollatorExpressionSpecification?] // boolean + | ['all', ...(boolean | ExpressionSpecification)[]] // boolean + | ['any', ...(boolean | ExpressionSpecification)[]] // boolean + | ['case', boolean | ExpressionSpecification, ExpressionInputType | ExpressionSpecification, + ...(boolean | ExpressionInputType | ExpressionSpecification)[], ExpressionInputType | ExpressionSpecification] + | ['coalesce', ExpressionInputType | ExpressionSpecification, ExpressionInputType | ExpressionSpecification, + ...(ExpressionInputType | ExpressionSpecification)[]] + | ['match', ExpressionInputType | ExpressionSpecification, + ExpressionInputType | ExpressionInputType[], ExpressionInputType | ExpressionSpecification, + ...(ExpressionInputType | ExpressionInputType[] | ExpressionSpecification)[], // repeated as above + ExpressionInputType] + | ['within', unknown | ExpressionSpecification] + // Ramps, scales, curves + | ['interpolate', InterpolationSpecification, + number | ExpressionSpecification, number | ExpressionSpecification, ExpressionInputType | ExpressionSpecification, + ...(number | ExpressionInputType | ExpressionSpecification)[]] + | ['interpolate-hcl', InterpolationSpecification, + number | ExpressionSpecification, number | ExpressionSpecification, ExpressionInputType | ExpressionSpecification, + ...(number | ColorSpecification | ExpressionSpecification)[]] + | ['interpolate-lab', InterpolationSpecification, + number | ExpressionSpecification, number | ExpressionSpecification, ExpressionInputType | ExpressionSpecification, + ...(number | ColorSpecification | ExpressionSpecification)[]] + | ['step', number | ExpressionSpecification, number | ExpressionSpecification, ExpressionInputType | ExpressionSpecification, + ...(number | ExpressionInputType | ExpressionSpecification)[]] + // Variable binding + | ['let', string, ExpressionInputType | ExpressionSpecification, ...(string | ExpressionInputType | ExpressionSpecification)[]] + | ['var', string] + // String + | ['concat', ExpressionInputType | ExpressionSpecification, ExpressionInputType | ExpressionSpecification, + ...(ExpressionInputType | ExpressionSpecification)[]] // string + | ['downcase', string | ExpressionSpecification] // string + | ['is-supported-script', string | ExpressionSpecification] // boolean + | ['resolved-locale', CollatorExpressionSpecification] // string + | ['upcase', string | ExpressionSpecification] // string + // Color + | ['rgb', number | ExpressionSpecification, number | ExpressionSpecification, number | ExpressionSpecification] // color + | ['rgba', number | ExpressionSpecification, number | ExpressionSpecification, number | ExpressionSpecification, number | ExpressionSpecification] + | ['to-rgba', ColorSpecification | ExpressionSpecification] + // Math + | ['-', number | ExpressionSpecification, (number | ExpressionSpecification)?] // number + | ['*', number | ExpressionSpecification, number | ExpressionSpecification, ...(number | ExpressionSpecification)[]] // number + | ['/', number | ExpressionSpecification, number | ExpressionSpecification] // number + | ['%', number | ExpressionSpecification, number | ExpressionSpecification] // number + | ['^', number | ExpressionSpecification, number | ExpressionSpecification] // number + | ['+', number | ExpressionSpecification, number | ExpressionSpecification, ...(number | ExpressionSpecification)[]] // number + | ['abs', number | ExpressionSpecification] // number + | ['acos', number | ExpressionSpecification] // number + | ['asin', number | ExpressionSpecification] // number + | ['atan', number | ExpressionSpecification] // number + | ['ceil', number | ExpressionSpecification] // number + | ['cos', number | ExpressionSpecification] // number + | ['distance', Record | ExpressionSpecification] // number + | ['ExpressionSpecification'] // number + | ['floor', number | ExpressionSpecification] // number + | ['ln', number | ExpressionSpecification] // number + | ['ln2'] // number + | ['log10', number | ExpressionSpecification] // number + | ['log2', number | ExpressionSpecification] // number + | ['max', number | ExpressionSpecification, ...(number | ExpressionSpecification)[]] // number + | ['min', number | ExpressionSpecification, ...(number | ExpressionSpecification)[]] // number + | ['pi'] // number + | ['round', number | ExpressionSpecification] // number + | ['sin', number | ExpressionSpecification] // number + | ['sqrt', number | ExpressionSpecification] // number + | ['tan', number | ExpressionSpecification] // number + // Zoom + | ['zoom'] // number + // Heatmap + | ['heatmap-density'] // number + +export type ExpressionFilterSpecification = boolean | ExpressionSpecification + +export type LegacyFilterSpecification = + // Existential + | ['has', string] + | ['!has', string] + // Comparison + | ['==', string, string | number | boolean] + | ['!=', string, string | number | boolean] + | ['>', string, string | number | boolean] + | ['>=', string, string | number | boolean] + | ['<', string, string | number | boolean] + | ['<=', string, string | number | boolean] + // Set membership + | ['in', string, ...(string | number | boolean)[]] + | ['!in', string, ...(string | number | boolean)[]] + // Combining + | ['all', ...LegacyFilterSpecification[]] + | ['any', ...LegacyFilterSpecification[]] + | ['none', ...LegacyFilterSpecification[]] + +export type FilterSpecification = ExpressionFilterSpecification | LegacyFilterSpecification export type TransitionSpecification = { duration?: number, @@ -181,19 +297,17 @@ export type CompositeFunctionSpecification = | { type: 'interval', stops: Array<[{zoom: number, value: number}, T]>, property: string, default?: T } | { type: 'categorical', stops: Array<[{zoom: number, value: string | number | boolean}, T]>, property: string, default?: T }; -export type ExpressionSpecificationArray = Array; - export type PropertyValueSpecification = T | CameraFunctionSpecification - | ExpressionSpecificationArray; + | ExpressionSpecification; export type DataDrivenPropertyValueSpecification = T | CameraFunctionSpecification | SourceFunctionSpecification | CompositeFunctionSpecification - | ExpressionSpecificationArray; + | ExpressionSpecification; ${objectDeclaration('StyleSpecification', spec.$root)} diff --git a/src/data/bucket/fill_bucket.test.ts b/src/data/bucket/fill_bucket.test.ts index 74883a87b3..fbe51798ef 100644 --- a/src/data/bucket/fill_bucket.test.ts +++ b/src/data/bucket/fill_bucket.test.ts @@ -55,6 +55,7 @@ test('FillBucket segmentation', () => { id: 'test', type: 'fill', layout: {}, + source: 'source', paint: { 'fill-color': ['to-color', ['get', 'foo'], '#000'] } diff --git a/src/style-spec/feature_filter/convert.ts b/src/style-spec/feature_filter/convert.ts index bb697f0029..5fffae04fd 100644 --- a/src/style-spec/feature_filter/convert.ts +++ b/src/style-spec/feature_filter/convert.ts @@ -1,17 +1,8 @@ import {isExpressionFilter} from './index'; -import type {FilterSpecification} from '../types.g'; +import type {ExpressionFilterSpecification, ExpressionInputType, ExpressionSpecification, FilterSpecification, LegacyFilterSpecification} from '../types.g'; -type ExpectedTypes = {[_: string]: 'string' | 'number' | 'boolean'}; - -/** - * Convert the given legacy filter to (the JSON representation of) an - * equivalent expression - * @private - */ -export default function convertFilter(filter: FilterSpecification): unknown { - return _convertFilter(filter, {}); -} +type ExpectedTypes = {[_: string]: ExpressionInputType}; /* * Convert the given filter to an expression, storing the expected types for @@ -61,51 +52,58 @@ export default function convertFilter(filter: FilterSpecification): unknown { * false (legacy filter semantics) are equivalent: they cause the filter to * produce a `false` result. */ -function _convertFilter(filter: FilterSpecification, expectedTypes: ExpectedTypes): unknown { - if (isExpressionFilter(filter)) { return filter; } - +export default function convertFilter(filter: FilterSpecification, expectedTypes: ExpectedTypes = {}): ExpressionFilterSpecification { + if (isExpressionFilter(filter)) return filter; if (!filter) return true; - const op = filter[0]; - if (filter.length <= 1) return (op !== 'any'); - - let converted; - - if ( - op === '==' || - op === '!=' || - op === '<' || - op === '>' || - op === '<=' || - op === '>=' - ) { - const [, property, value] = filter; - converted = convertComparisonOp(property as string, value, op, expectedTypes); - } else if (op === 'any') { - const children = (filter).slice(1).map((f: FilterSpecification) => { - const types = {}; - const child = _convertFilter(f, types); - const typechecks = runtimeTypeChecks(types); - return typechecks === true ? child : ['case', typechecks, child, false]; - }); - return ['any'].concat(children as string[]); - } else if (op === 'all') { - const children = (filter).slice(1).map(f => _convertFilter(f as FilterSpecification, expectedTypes)); - return children.length > 1 ? ['all'].concat(children as string[]) : [].concat(...children); - } else if (op === 'none') { - return ['!', _convertFilter(['any'].concat(filter.slice(1) as string[]) as string[], {})]; - } else if (op === 'in') { - converted = convertInOp(filter[1] as string, filter.slice(2)); - } else if (op === '!in') { - converted = convertInOp(filter[1] as string, filter.slice(2), true); - } else if (op === 'has') { - converted = convertHasOp(filter[1] as string); - } else if (op === '!has') { - converted = ['!', convertHasOp(filter[1] as string)]; - } else { - converted = true; - } - return converted; + const legacyFilter = filter as LegacyFilterSpecification; + const legacyOp = legacyFilter[0]; + if (filter.length <= 1) return (legacyOp !== 'any'); + + switch (legacyOp) { + case '==': + case '!=': + case '<': + case '>': + case '<=': + case '>=': { + const [, property, value] = filter; + return convertComparisonOp(property as string, value, legacyOp, expectedTypes); + } + case 'any': { + const [, ...conditions] = legacyFilter; + const children = conditions.map((f: LegacyFilterSpecification) => { + const types = {}; + const child = convertFilter(f, types); + const typechecks = runtimeTypeChecks(types); + return typechecks === true ? child : ['case', typechecks, child, false] as ExpressionSpecification; + }); + return ['any', ...children]; + } + case 'all': { + const [, ...conditions] = legacyFilter; + const children = conditions.map(f => convertFilter(f, expectedTypes)); + return children.length > 1 ? ['all', ...children] : children[0]; + } + case 'none': { + const [, ...conditions] = legacyFilter; + return ['!', convertFilter(['any', ...conditions], {})]; + } + case 'in': { + const [, property, ...values] = legacyFilter; + return convertInOp(property, values); + } + case '!in': { + const [, property, ...values] = legacyFilter; + return convertInOp(property, values, true); + } + case 'has': + return convertHasOp(legacyFilter[1]); + case '!has': + return ['!', convertHasOp(legacyFilter[1])]; + default: + return true; + } } // Given a set of feature properties and an expected type for each one, @@ -116,7 +114,7 @@ function _convertFilter(filter: FilterSpecification, expectedTypes: ExpectedType // ['==', ['typeof', ['get', 'name'], 'string']], // ['==', ['typeof', ['get', 'population'], 'number]] // ] -function runtimeTypeChecks(expectedTypes: ExpectedTypes) { +function runtimeTypeChecks(expectedTypes: ExpectedTypes): ExpressionFilterSpecification { const conditions = []; for (const property in expectedTypes) { const get = property === '$id' ? ['id'] : ['get', property]; @@ -124,13 +122,13 @@ function runtimeTypeChecks(expectedTypes: ExpectedTypes) { } if (conditions.length === 0) return true; if (conditions.length === 1) return conditions[0]; - return ['all'].concat(conditions); + return ['all', ...conditions]; } -function convertComparisonOp(property: string, value: any, op: string, expectedTypes?: ExpectedTypes | null) { +function convertComparisonOp(property: string, value: any, op: string, expectedTypes?: ExpectedTypes | null): ExpressionFilterSpecification { let get; if (property === '$type') { - return [op, ['geometry-type'], value]; + return [op, ['geometry-type'], value] as ExpressionFilterSpecification; } else if (property === '$id') { get = ['id']; } else { @@ -156,13 +154,13 @@ function convertComparisonOp(property: string, value: any, op: string, expectedT ]; } - return [op, get, value]; + return [op, get, value] as ExpressionFilterSpecification; } -function convertInOp(property: string, values: Array, negate = false) { +function convertInOp(property: string, values: Array, negate = false): ExpressionFilterSpecification { if (values.length === 0) return negate; - let get; + let get: ExpressionSpecification; if (property === '$type') { get = ['geometry-type']; } else if (property === '$id') { @@ -190,12 +188,14 @@ function convertInOp(property: string, values: Array, negate = false) { return ['match', get, uniqueValues, !negate, negate]; } - return [ negate ? 'all' : 'any' as any].concat( - values.map(v => [negate ? '!=' : '==', get, v]) - ); + if (negate) { + return ['all', ...values.map(v => ['!=', get, v] as ExpressionSpecification)]; + } else { + return ['any', ...values.map(v => ['==', get, v] as ExpressionSpecification)]; + } } -function convertHasOp(property: string) { +function convertHasOp(property: string): ExpressionFilterSpecification { if (property === '$type') { return true; } else if (property === '$id') { diff --git a/src/style-spec/feature_filter/feature_filter.test.ts b/src/style-spec/feature_filter/feature_filter.test.ts index ce220a14a1..99b599be0a 100644 --- a/src/style-spec/feature_filter/feature_filter.test.ts +++ b/src/style-spec/feature_filter/feature_filter.test.ts @@ -5,10 +5,39 @@ import Point from '@mapbox/point-geometry'; import MercatorCoordinate from '../../geo/mercator_coordinate'; import EXTENT from '../../data/extent'; import {CanonicalTileID} from '../../source/tile_id'; -import {FilterSpecification} from '../types.g'; +import {ExpressionFilterSpecification, FilterSpecification} from '../types.g'; import {Feature} from '../expression'; describe('filter', () => { + test('exprssions transpilation test', () => { + function compileTimeCheck(_: ExpressionFilterSpecification) { + expect(true).toBeTruthy(); + } + compileTimeCheck(['any']); + compileTimeCheck(['at', 2, ['array', 1, 2, 3]]); + compileTimeCheck(['case', ['has', 'color'], ['get', 'color'], 'white']); + compileTimeCheck(['case', ['all', ['has', 'point_count'], ['<', ['get', 'point_count'], 3]], ['get', 'cluster_routes'], '']); + compileTimeCheck(['interpolate', ['linear'], ['get', 'point_count'], 2, 18.0, 10, 24.0]); + compileTimeCheck(['case', ['has', 'point_count'], ['interpolate', ['linear'], ['get', 'point_count'], 2, 18.0, 10, 24.0], 12.0]); + compileTimeCheck([ + 'case', + ['has', 'point_count'], ['interpolate', ['linear'], ['get', 'point_count'], 2, '#ccc', 10, '#444'], + ['has', 'priorityValue'], ['interpolate', ['linear'], ['get', 'priorityValue'], 0, '#ff9', 1, '#f66'], + '#fcaf3e' + ]); + compileTimeCheck([ + 'case', + ['==', ['get', 'CAPITAL'], 1], 'city-capital', + ['>=', ['get', 'POPULATION'], 1000000], 'city-1M', + ['>=', ['get', 'POPULATION'], 500000], 'city-500k', + ['>=', ['get', 'POPULATION'], 100000], 'city-100k', + 'city' + ]); + compileTimeCheck(['match', ['get', 'TYPE'], ['TARGETPOINT:HOSPITAL'], true, false]); + compileTimeCheck(['match', ['get', 'TYPE'], ['ADIZ', 'AMA', 'AWY', 'CLASS', 'NO-FIR', 'OCA', 'OTA', 'P', 'RAS', 'RCA', 'UTA', 'UTA-P'], true, false]); + compileTimeCheck(['==', ['get', 'MILITARYAIRPORT'], 1]); + }); + test('expression, zoom', () => { const f = createFilter(['>=', ['number', ['get', 'x']], ['zoom']]).filter; expect(f({zoom: 1}, {properties: {x: 0}} as any as Feature)).toBe(false); @@ -52,6 +81,18 @@ describe('filter', () => { expect(createFilter(['any', false, false]).filter(undefined, undefined)).toBe(false); }); + test('expression, literal', () => { + expect(createFilter(['literal', true]).filter(undefined, undefined)).toBe(true); + expect(createFilter(['literal', false]).filter(undefined, undefined)).toBe(false); + }); + + test('expression, match', () => { + const match = createFilter(['match', ['get', 'x'], ['a', 'b', 'c'], true, false]).filter; + expect(match(undefined, {properties: {x: 'a'}} as any as Feature)).toBe(true); + expect(match(undefined, {properties: {x: 'c'}} as any as Feature)).toBe(true); + expect(match(undefined, {properties: {x: 'd'}} as any as Feature)).toBe(false); + }); + test('expression, type error', () => { expect(() => { createFilter(['==', ['number', ['get', 'x']], ['string', ['get', 'y']]]); @@ -179,14 +220,14 @@ describe('convert legacy filters to expressions', () => { ['LineString', 'Point', 'Polygon'], true, false - ] as FilterSpecification, + ], [ 'match', ['get', 'type'], ['island'], true, false - ] as FilterSpecification + ] ]; const converted = convertFilter(filter); diff --git a/src/style-spec/feature_filter/index.ts b/src/style-spec/feature_filter/index.ts index 50dc65f77d..bc6fd2aa81 100644 --- a/src/style-spec/feature_filter/index.ts +++ b/src/style-spec/feature_filter/index.ts @@ -2,6 +2,7 @@ import {createExpression} from '../expression'; import type {GlobalProperties, Feature} from '../expression'; import type {CanonicalTileID} from '../../source/tile_id'; import {StylePropertySpecification} from '../style-spec'; +import {ExpressionFilterSpecification} from '../types.g'; type FilterExpression = ( globalProperties: GlobalProperties, @@ -17,7 +18,7 @@ export type FeatureFilter = { export default createFilter; export {isExpressionFilter}; -function isExpressionFilter(filter: any) { +function isExpressionFilter(filter: any): filter is ExpressionFilterSpecification { if (filter === true || filter === false) { return true; } diff --git a/src/style-spec/migrate/expressions.ts b/src/style-spec/migrate/expressions.ts index abbd14ca8f..95a5686eb0 100644 --- a/src/style-spec/migrate/expressions.ts +++ b/src/style-spec/migrate/expressions.ts @@ -15,7 +15,7 @@ export default function expressions(style: StyleSpecification) { eachLayer(style, (layer: LayerSpecification & { filter?: FilterSpecification }) => { if (layer.filter) { - layer.filter = (convertFilter(layer.filter) as any); + layer.filter = convertFilter(layer.filter); } }); diff --git a/src/style/light.test.ts b/src/style/light.test.ts index f3235ed367..c934832869 100644 --- a/src/style/light.test.ts +++ b/src/style/light.test.ts @@ -77,7 +77,7 @@ describe('Light#setLight', () => { const light = new Light({}); const lightSpy = jest.spyOn(light, '_validate'); - light.setLight({color: [999]}, {validate: false}); + light.setLight({color: [999]} as any, {validate: false}); light.updateTransitions({transition: false} as any as TransitionParameters); light.recalculate({zoom: 16, zoomHistory: {}, now: 10} as EvaluationParameters);