diff --git a/bench/benchmarks/filter_evaluate.js b/bench/benchmarks/filter_evaluate.js index 689a9bf84fe..e07c270c7e0 100644 --- a/bench/benchmarks/filter_evaluate.js +++ b/bench/benchmarks/filter_evaluate.js @@ -39,7 +39,7 @@ module.exports = class FilterEvaluate extends Benchmark { for (const layer of this.layers) { for (const filter of layer.filters) { for (const feature of layer.features) { - if (typeof filter(feature) !== 'boolean') { + if (typeof filter({zoom: 0}, feature) !== 'boolean') { assert(false, 'Expected boolean result from filter'); } } diff --git a/src/data/bucket/circle_bucket.js b/src/data/bucket/circle_bucket.js index ac8768a697e..79ed7136250 100644 --- a/src/data/bucket/circle_bucket.js +++ b/src/data/bucket/circle_bucket.js @@ -80,7 +80,7 @@ class CircleBucket implements Bucket { populate(features: Array, options: PopulateParameters) { for (const {feature, index, sourceLayerIndex} of features) { - if (this.layers[0].filter(feature)) { + if (this.layers[0]._featureFilter({zoom: this.zoom}, feature)) { const geometry = loadGeometry(feature); this.addFeature(feature, geometry); options.featureIndex.insert(feature, geometry, index, sourceLayerIndex, this.index); diff --git a/src/data/bucket/fill_bucket.js b/src/data/bucket/fill_bucket.js index e6ca37a4b28..a266b59b867 100644 --- a/src/data/bucket/fill_bucket.js +++ b/src/data/bucket/fill_bucket.js @@ -72,7 +72,7 @@ class FillBucket implements Bucket { populate(features: Array, options: PopulateParameters) { for (const {feature, index, sourceLayerIndex} of features) { - if (this.layers[0].filter(feature)) { + if (this.layers[0]._featureFilter({zoom: this.zoom}, feature)) { const geometry = loadGeometry(feature); this.addFeature(feature, geometry); options.featureIndex.insert(feature, geometry, index, sourceLayerIndex, this.index); diff --git a/src/data/bucket/fill_extrusion_bucket.js b/src/data/bucket/fill_extrusion_bucket.js index 57e6c885526..67d800550f3 100644 --- a/src/data/bucket/fill_extrusion_bucket.js +++ b/src/data/bucket/fill_extrusion_bucket.js @@ -85,7 +85,7 @@ class FillExtrusionBucket implements Bucket { populate(features: Array, options: PopulateParameters) { for (const {feature, index, sourceLayerIndex} of features) { - if (this.layers[0].filter(feature)) { + if (this.layers[0]._featureFilter({zoom: this.zoom}, feature)) { const geometry = loadGeometry(feature); this.addFeature(feature, geometry); options.featureIndex.insert(feature, geometry, index, sourceLayerIndex, this.index); diff --git a/src/data/bucket/line_bucket.js b/src/data/bucket/line_bucket.js index f13a993cd88..ef40d1bcfa3 100644 --- a/src/data/bucket/line_bucket.js +++ b/src/data/bucket/line_bucket.js @@ -129,7 +129,7 @@ class LineBucket implements Bucket { populate(features: Array, options: PopulateParameters) { for (const {feature, index, sourceLayerIndex} of features) { - if (this.layers[0].filter(feature)) { + if (this.layers[0]._featureFilter({zoom: this.zoom}, feature)) { const geometry = loadGeometry(feature); this.addFeature(feature, geometry); options.featureIndex.insert(feature, geometry, index, sourceLayerIndex, this.index); diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index b8b57294ca0..8c401bdf958 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -396,7 +396,7 @@ class SymbolBucket implements Bucket { const globalProperties = {zoom: this.zoom}; for (const {feature, index, sourceLayerIndex} of features) { - if (!layer.filter(feature)) { + if (!layer._featureFilter(globalProperties, feature)) { continue; } diff --git a/src/data/feature_index.js b/src/data/feature_index.js index c5df6ec46b9..aa10d50652e 100644 --- a/src/data/feature_index.js +++ b/src/data/feature_index.js @@ -16,6 +16,7 @@ import type CollisionTile from '../symbol/collision_tile'; import type TileCoord from '../source/tile_coord'; import type StyleLayer from '../style/style_layer'; import type {SerializedStructArray} from '../util/struct_array'; +import type {FeatureFilter} from '../style-spec/feature_filter'; const FeatureIndexArray = createStructArrayType({ members: [ @@ -178,7 +179,7 @@ class FeatureIndex { matching: Array, array: any, queryGeometry: Array>, - filter: any, + filter: FeatureFilter, filterLayerIDs: Array, styleLayers: {[string]: StyleLayer}, bearing: number, @@ -201,7 +202,7 @@ class FeatureIndex { const sourceLayer = this.vtLayers[sourceLayerName]; const feature = sourceLayer.feature(match.featureIndex); - if (!filter(feature)) continue; + if (!filter({zoom: this.coord.z}, feature)) continue; let geometry = null; diff --git a/src/source/tile.js b/src/source/tile.js index 28bd00c1bc6..f2ac9ab59ae 100644 --- a/src/source/tile.js +++ b/src/source/tile.js @@ -357,7 +357,7 @@ class Tile { for (let i = 0; i < layer.length; i++) { const feature = layer.feature(i); - if (filter(feature)) { + if (filter({zoom: this.coord.z}, feature)) { const geojsonFeature = new GeoJSONFeature(feature, this.coord.z, this.coord.x, this.coord.y); (geojsonFeature: any).tile = coord; result.push(geojsonFeature); diff --git a/src/style-spec/expression/compound_expression.js b/src/style-spec/expression/compound_expression.js index faf4b74f646..411deeecf24 100644 --- a/src/style-spec/expression/compound_expression.js +++ b/src/style-spec/expression/compound_expression.js @@ -11,7 +11,7 @@ import type { Value } from './values'; type Varargs = {| type: Type |}; type Signature = Array | Varargs; -type Evaluate = (EvaluationContext, Array) => Value; +type Evaluate = (EvaluationContext) => Value; type Definition = [Type, Signature, Evaluate] | {|type: Type, overloads: Array<[Signature, Evaluate]>|}; @@ -19,7 +19,7 @@ class CompoundExpression implements Expression { key: string; name: string; type: Type; - _evaluate: Evaluate; + evaluate: (ctx: EvaluationContext) => any; args: Array; static definitions: { [string]: Definition }; @@ -28,14 +28,10 @@ class CompoundExpression implements Expression { this.key = key; this.name = name; this.type = type; - this._evaluate = evaluate; + this.evaluate = evaluate; this.args = args; } - evaluate(ctx: EvaluationContext) { - return this._evaluate(ctx, this.args); - } - serialize() { const name = this.name; const args = this.args.map(e => e.serialize()); diff --git a/src/style-spec/expression/definitions/index.js b/src/style-spec/expression/definitions/index.js index 01d031b81e5..5b018e79977 100644 --- a/src/style-spec/expression/definitions/index.js +++ b/src/style-spec/expression/definitions/index.js @@ -49,7 +49,8 @@ const expressions: { [string]: Class } = { 'curve': Curve, }; -function rgba(ctx, [r, g, b, a]) { +function rgba(ctx) { + let [r, g, b, a] = this.args; r = r.evaluate(ctx); g = g.evaluate(ctx); b = b.evaluate(ctx); @@ -60,8 +61,7 @@ function rgba(ctx, [r, g, b, a]) { } function has(key, obj) { - const v = obj[key]; - return typeof v !== 'undefined'; + return key in obj; } function get(key, obj) { @@ -69,33 +69,48 @@ function get(key, obj) { return typeof v === 'undefined' ? null : v; } -function length(ctx, [v]) { +function length(ctx) { + const [v] = this.args; return v.evaluate(ctx).length; } -function eq(ctx, [a, b]) { return a.evaluate(ctx) === b.evaluate(ctx); } -function ne(ctx, [a, b]) { return a.evaluate(ctx) !== b.evaluate(ctx); } -function lt(ctx, [a, b]) { return a.evaluate(ctx) < b.evaluate(ctx); } -function gt(ctx, [a, b]) { return a.evaluate(ctx) > b.evaluate(ctx); } -function lteq(ctx, [a, b]) { return a.evaluate(ctx) <= b.evaluate(ctx); } -function gteq(ctx, [a, b]) { return a.evaluate(ctx) >= b.evaluate(ctx); } +function eq(ctx) { const [a, b] = this.args; return a.evaluate(ctx) === b.evaluate(ctx); } +function ne(ctx) { const [a, b] = this.args; return a.evaluate(ctx) !== b.evaluate(ctx); } +function lt(ctx) { const [a, b] = this.args; return a.evaluate(ctx) < b.evaluate(ctx); } +function gt(ctx) { const [a, b] = this.args; return a.evaluate(ctx) > b.evaluate(ctx); } +function lteq(ctx) { const [a, b] = this.args; return a.evaluate(ctx) <= b.evaluate(ctx); } +function gteq(ctx) { const [a, b] = this.args; return a.evaluate(ctx) >= b.evaluate(ctx); } + +function binarySearch(v, a, i, j) { + while (i <= j) { + const m = (i + j) >> 1; + if (a[m] === v) + return true; + if (a[m] > v) + j = m - 1; + else + i = m + 1; + } + return false; +} + CompoundExpression.register(expressions, { 'error': [ ErrorType, [StringType], - (ctx, [v]) => { throw new RuntimeError(v.evaluate(ctx)); } + function (ctx) { throw new RuntimeError(this.args[0].evaluate(ctx)); } ], 'typeof': [ StringType, [ValueType], - (ctx, [v]) => toString(typeOf(v.evaluate(ctx))) + function (ctx) { return toString(typeOf(this.args[0].evaluate(ctx))); } ], 'to-string': [ StringType, [ValueType], - (ctx, [v]) => { - v = v.evaluate(ctx); + function (ctx) { + const v = this.args[0].evaluate(ctx); const type = typeof v; if (v === null || type === 'string' || type === 'number' || type === 'boolean') { return String(v); @@ -110,12 +125,12 @@ CompoundExpression.register(expressions, { 'to-boolean': [ BooleanType, [ValueType], - (ctx, [v]) => Boolean(v.evaluate(ctx)) + function (ctx) { return Boolean(this.args[0].evaluate(ctx)); } ], 'to-rgba': [ array(NumberType, 4), [ColorType], - (ctx, [v]) => v.evaluate(ctx).value + function (ctx) { return this.args[0].evaluate(ctx).value; } ], 'rgb': [ ColorType, @@ -144,10 +159,10 @@ CompoundExpression.register(expressions, { overloads: [ [ [StringType], - (ctx, [key]) => has(key.evaluate(ctx), ctx.properties()) + function (ctx) { return has(this.args[0].evaluate(ctx), ctx.properties()); } ], [ [StringType, ObjectType], - (ctx, [key, obj]) => has(key.evaluate(ctx), obj.evaluate(ctx)) + function (ctx) { return has(this.args[0].evaluate(ctx), this.args[1].evaluate(ctx)); } ] ] }, @@ -156,10 +171,10 @@ CompoundExpression.register(expressions, { overloads: [ [ [StringType], - (ctx, [key]) => get(key.evaluate(ctx), ctx.properties()) + function (ctx) { return get(this.args[0].evaluate(ctx), ctx.properties()); } ], [ [StringType, ObjectType], - (ctx, [key, obj]) => get(key.evaluate(ctx), obj.evaluate(ctx)) + function (ctx) { return get(this.args[0].evaluate(ctx), this.args[1].evaluate(ctx)); } ] ] }, @@ -191,34 +206,46 @@ CompoundExpression.register(expressions, { '+': [ NumberType, varargs(NumberType), - (ctx, args) => args.reduce((a, b) => a + b.evaluate(ctx), 0) + function (ctx) { + let result = 0; + for (const arg of this.args) { + result += arg.evaluate(ctx); + } + return result; + } ], '*': [ NumberType, varargs(NumberType), - (ctx, args) => args.reduce((a, b) => a * b.evaluate(ctx), 1) + function (ctx) { + let result = 1; + for (const arg of this.args) { + result *= arg.evaluate(ctx); + } + return result; + } ], '-': { type: NumberType, overloads: [ [ [NumberType, NumberType], - (ctx, [a, b]) => a.evaluate(ctx) - b.evaluate(ctx) + function (ctx) { return this.args[0].evaluate(ctx) - this.args[1].evaluate(ctx); } ], [ [NumberType], - (ctx, [a]) => -a.evaluate(ctx) + function (ctx) { return -this.args[0].evaluate(ctx); } ] ] }, '/': [ NumberType, [NumberType, NumberType], - (ctx, [a, b]) => a.evaluate(ctx) / b.evaluate(ctx) + function (ctx) { return this.args[0].evaluate(ctx) / this.args[1].evaluate(ctx); } ], '%': [ NumberType, [NumberType, NumberType], - (ctx, [a, b]) => a.evaluate(ctx) % b.evaluate(ctx) + function (ctx) { return this.args[0].evaluate(ctx) % this.args[1].evaluate(ctx); } ], 'ln2': [ NumberType, @@ -238,62 +265,62 @@ CompoundExpression.register(expressions, { '^': [ NumberType, [NumberType, NumberType], - (ctx, [b, e]) => Math.pow(b.evaluate(ctx), e.evaluate(ctx)) + function (ctx) { return Math.pow(this.args[0].evaluate(ctx), this.args[1].evaluate(ctx)); } ], 'log10': [ NumberType, [NumberType], - (ctx, [n]) => Math.log10(n.evaluate(ctx)) + function (ctx) { return Math.log10(this.args[0].evaluate(ctx)); } ], 'ln': [ NumberType, [NumberType], - (ctx, [n]) => Math.log(n.evaluate(ctx)) + function (ctx) { return Math.log(this.args[0].evaluate(ctx)); } ], 'log2': [ NumberType, [NumberType], - (ctx, [n]) => Math.log2(n.evaluate(ctx)) + function (ctx) { return Math.log2(this.args[0].evaluate(ctx)); } ], 'sin': [ NumberType, [NumberType], - (ctx, [n]) => Math.sin(n.evaluate(ctx)) + function (ctx) { return Math.sin(this.args[0].evaluate(ctx)); } ], 'cos': [ NumberType, [NumberType], - (ctx, [n]) => Math.cos(n.evaluate(ctx)) + function (ctx) { return Math.cos(this.args[0].evaluate(ctx)); } ], 'tan': [ NumberType, [NumberType], - (ctx, [n]) => Math.tan(n.evaluate(ctx)) + function (ctx) { return Math.tan(this.args[0].evaluate(ctx)); } ], 'asin': [ NumberType, [NumberType], - (ctx, [n]) => Math.asin(n.evaluate(ctx)) + function (ctx) { return Math.asin(this.args[0].evaluate(ctx)); } ], 'acos': [ NumberType, [NumberType], - (ctx, [n]) => Math.acos(n.evaluate(ctx)) + function (ctx) { return Math.acos(this.args[0].evaluate(ctx)); } ], 'atan': [ NumberType, [NumberType], - (ctx, [n]) => Math.atan(n.evaluate(ctx)) + function (ctx) { return Math.atan(this.args[0].evaluate(ctx)); } ], 'min': [ NumberType, varargs(NumberType), - (ctx, args) => Math.min(...args.map(arg => arg.evaluate(ctx))) + function (ctx) { return Math.min(...this.args.map(arg => arg.evaluate(ctx))); } ], 'max': [ NumberType, varargs(NumberType), - (ctx, args) => Math.max(...args.map(arg => arg.evaluate(ctx))) + function (ctx) { return Math.max(...this.args.map(arg => arg.evaluate(ctx))); } ], '==': { type: BooleanType, @@ -304,6 +331,140 @@ CompoundExpression.register(expressions, { [[NullType, NullType], eq] ] }, + 'filter-==': [ + BooleanType, + [StringType, ValueType], + function (ctx) { + return ctx.properties()[this.args[0].value] === this.args[1].value; + } + ], + 'filter-id-==': [ + BooleanType, + [ValueType], + function (ctx) { + return ctx.id() === this.args[0].value; + } + ], + 'filter-type-==': [ + BooleanType, + [StringType], + function (ctx) { + return ctx.geometryType() === this.args[0].value; + } + ], + 'filter-<': [ + BooleanType, + [StringType, ValueType], + function (ctx) { + const v = ctx.properties()[this.args[0].value]; + const b = this.args[1]; + return typeof v === typeof b.value && v < b.value; + } + ], + 'filter-id-<': [ + BooleanType, + [ValueType], + function (ctx) { + const v = ctx.id(); + const b = this.args[0]; + return typeof v === typeof b.value && v < b.value; + } + ], + 'filter->': [ + BooleanType, + [StringType, ValueType], + function (ctx) { + const v = ctx.properties()[this.args[0].value]; + const b = this.args[1]; + return typeof v === typeof b.value && v > b.value; + } + ], + 'filter-id->': [ + BooleanType, + [ValueType], + function (ctx) { + const v = ctx.id(); + const b = this.args[0]; + return typeof v === typeof b.value && v > b.value; + } + ], + 'filter-<=': [ + BooleanType, + [StringType, ValueType], + function (ctx) { + const v = ctx.properties()[this.args[0].value]; + const b = this.args[1]; + return typeof v === typeof b.value && v <= b.value; + } + ], + 'filter-id-<=': [ + BooleanType, + [ValueType], + function (ctx) { + const v = ctx.id(); + const b = this.args[0]; + return typeof v === typeof b.value && v <= b.value; + } + ], + 'filter->=': [ + BooleanType, + [StringType, ValueType], + function (ctx) { + const v = ctx.properties()[this.args[0].value]; + const b = this.args[1]; + return typeof v === typeof b.value && v >= b.value; + } + ], + 'filter-id->=': [ + BooleanType, + [ValueType], + function (ctx) { + const v = ctx.id(); + const b = this.args[0]; + return typeof v === typeof b.value && v >= b.value; + } + ], + 'filter-has': [ + BooleanType, + [ValueType], + function (ctx) { return this.args[0].value in ctx.properties(); } + ], + 'filter-has-id': [ + BooleanType, + [], + (ctx) => ctx.id() !== null + ], + 'filter-type-in': [ + BooleanType, + [array(StringType)], + function (ctx) { return this.args[0].value.indexOf(ctx.geometryType()) >= 0; } + ], + 'filter-id-in': [ + BooleanType, + [array(ValueType)], + function (ctx) { return this.args[0].value.indexOf(ctx.id()) >= 0; } + ], + 'filter-in-small': [ + BooleanType, + [StringType, array(ValueType)], + function (ctx) { + // assumes this.args[1] is an array Literal + const value = ctx.properties()[this.args[0].value]; + const array = this.args[1].value; + return array.indexOf(value) >= 0; + } + ], + 'filter-in-large': [ + BooleanType, + [StringType, array(ValueType)], + function (ctx) { + // assumes values is a array Literal with values + // sorted in ascending order and of a single type + const value = ctx.properties()[this.args[0].value]; + const array = this.args[1].value; + return binarySearch(value, array, 0, array.length - 1); + } + ], '!=': { type: BooleanType, overloads: [ @@ -341,35 +502,63 @@ CompoundExpression.register(expressions, { [[StringType, StringType], lteq] ] }, - 'all': [ - BooleanType, - varargs(BooleanType), - (ctx, args) => args.reduce((a, b) => a && b.evaluate(ctx), true) - ], - 'any': [ - BooleanType, - varargs(BooleanType), - (ctx, args) => args.reduce((a, b) => a || b.evaluate(ctx), false) - ], + 'all': { + type: BooleanType, + overloads: [ + [ + [BooleanType, BooleanType], + function (ctx) { return this.args[0].evaluate(ctx) && this.args[1].evaluate(ctx); } + ], + [ + varargs(BooleanType), + function (ctx) { + for (const arg of this.args) { + if (!arg.evaluate(ctx)) + return false; + } + return true; + } + ] + ] + }, + 'any': { + type: BooleanType, + overloads: [ + [ + [BooleanType, BooleanType], + function (ctx) { return this.args[0].evaluate(ctx) || this.args[1].evaluate(ctx); } + ], + [ + varargs(BooleanType), + function (ctx) { + for (const arg of this.args) { + if (arg.evaluate(ctx)) + return true; + } + return false; + } + ] + ] + }, '!': [ BooleanType, [BooleanType], - (ctx, [b]) => !b.evaluate(ctx) + function (ctx) { return !this.args[0].evaluate(ctx); } ], 'upcase': [ StringType, [StringType], - (ctx, [s]) => s.evaluate(ctx).toUpperCase() + function (ctx) { return this.args[0].evaluate(ctx).toUpperCase(); } ], 'downcase': [ StringType, [StringType], - (ctx, [s]) => s.evaluate(ctx).toLowerCase() + function (ctx) { return this.args[0].evaluate(ctx).toLowerCase(); } ], 'concat': [ StringType, varargs(StringType), - (ctx, args) => args.map(arg => arg.evaluate(ctx)).join('') + function (ctx) { return this.args.map(arg => arg.evaluate(ctx)).join(''); } ] }); diff --git a/src/style-spec/expression/index.js b/src/style-spec/expression/index.js index 5216b47f7b4..9a9cda455e8 100644 --- a/src/style-spec/expression/index.js +++ b/src/style-spec/expression/index.js @@ -306,5 +306,5 @@ function getDefaultValue(spec: StylePropertySpecification): Value | null { assert(Array.isArray(c)); return new Color(c[0], c[1], c[2], c[3]); } - return defaultValue || null; + return defaultValue === undefined ? null : defaultValue; } diff --git a/src/style-spec/expression/is_constant.js b/src/style-spec/expression/is_constant.js index 08c2484afea..c33ea372ccd 100644 --- a/src/style-spec/expression/is_constant.js +++ b/src/style-spec/expression/is_constant.js @@ -13,7 +13,8 @@ function isFeatureConstant(e: Expression) { } else if ( e.name === 'properties' || e.name === 'geometry-type' || - e.name === 'id' + e.name === 'id' || + /^filter-/.test(e.name) ) { return false; } diff --git a/src/style-spec/feature_filter/index.js b/src/style-spec/feature_filter/index.js index 82c6679bea7..5bdf23edc71 100644 --- a/src/style-spec/feature_filter/index.js +++ b/src/style-spec/feature_filter/index.js @@ -1,6 +1,55 @@ +// @flow + +const {createExpression} = require('../expression'); + +import type {GlobalProperties} from '../expression'; +export type FeatureFilter = (globalProperties: GlobalProperties, feature: VectorTileFeature) => boolean; + module.exports = createFilter; +module.exports.isExpressionFilter = isExpressionFilter; + +function isExpressionFilter(filter) { + if (!Array.isArray(filter) || filter.length === 0) { + return false; + } + switch (filter[0]) { + case 'has': + return filter.length >= 2 && filter[1] !== '$id' && filter[1] !== '$type'; + + case 'in': + case '!in': + case '!has': + case 'none': + return false; + + case '==': + case '!=': + case '>': + case '>=': + case '<': + case '<=': + return filter.length === 3 && (Array.isArray(filter[1]) || Array.isArray(filter[2])); + + case 'any': + case 'all': + for (const f of filter.slice(1)) { + if (!isExpressionFilter(f)) { + return false; + } + } + return true; + + default: + return true; + } +} -const types = ['Unknown', 'Point', 'LineString', 'Polygon']; +const filterSpec = { + 'type': 'boolean', + 'default': false, + 'function': true, + 'property-function': true +}; /** * Given a filter expressed as nested arrays, return a new function @@ -11,74 +60,87 @@ const types = ['Unknown', 'Point', 'LineString', 'Polygon']; * @param {Array} filter mapbox gl filter * @returns {Function} filter-evaluating function */ -function createFilter(filter) { - return new Function('f', `var p = (f && f.properties || {}); return ${compile(filter)}`); +function createFilter(filter: any): FeatureFilter { + if (!filter) { + return () => true; + } + + const isExpression = isExpressionFilter(filter); + const expression = isExpression ? filter : convertFilter(filter); + const compiled = createExpression(expression, filterSpec, 'filter', {handleErrors: isExpression}); + + if (compiled.result === 'success') { + return compiled.evaluate; + } else { + throw new Error(compiled.errors.map(err => `${err.key}: ${err.message}`).join(', ')); + } } -function compile(filter) { - if (!filter) return 'true'; +function convertFilter(filter: ?Array): mixed { + if (!filter) return true; const op = filter[0]; - if (filter.length <= 1) return op === 'any' ? 'false' : 'true'; - const str = - op === '==' ? compileComparisonOp(filter[1], filter[2], '===', false) : - op === '!=' ? compileComparisonOp(filter[1], filter[2], '!==', false) : + if (filter.length <= 1) return (op !== 'any'); + const converted = + op === '==' ? compileComparisonOp(filter[1], filter[2], '==') : + op === '!=' ? compileNegation(compileComparisonOp(filter[1], filter[2], '==')) : op === '<' || op === '>' || op === '<=' || - op === '>=' ? compileComparisonOp(filter[1], filter[2], op, true) : - op === 'any' ? compileLogicalOp(filter.slice(1), '||') : - op === 'all' ? compileLogicalOp(filter.slice(1), '&&') : - op === 'none' ? compileNegation(compileLogicalOp(filter.slice(1), '||')) : + op === '>=' ? compileComparisonOp(filter[1], filter[2], op) : + op === 'any' ? compileDisjunctionOp(filter.slice(1)) : + op === 'all' ? ['all'].concat(filter.slice(1).map(convertFilter)) : + op === 'none' ? ['all'].concat(filter.slice(1).map(convertFilter).map(compileNegation)) : op === 'in' ? compileInOp(filter[1], filter.slice(2)) : op === '!in' ? compileNegation(compileInOp(filter[1], filter.slice(2))) : op === 'has' ? compileHasOp(filter[1]) : op === '!has' ? compileNegation(compileHasOp(filter[1])) : - 'true'; - return `(${str})`; -} - -function compilePropertyReference(property) { - const ref = - property === '$type' ? 'f.type' : - property === '$id' ? 'f.id' : `p[${JSON.stringify(property)}]`; - return ref; + true; + return converted; } -function compileComparisonOp(property, value, op, checkType) { - const left = compilePropertyReference(property); - const right = property === '$type' ? types.indexOf(value) : JSON.stringify(value); - return (checkType ? `typeof ${left}=== typeof ${right}&&` : '') + left + op + right; +function compileComparisonOp(property: string, value: any, op: string) { + switch (property) { + case '$type': + return [`filter-type-${op}`, value]; + case '$id': + return [`filter-id-${op}`, value]; + default: + return [`filter-${op}`, property, value]; + } } -function compileLogicalOp(expressions, op) { - return expressions.map(compile).join(op); +function compileDisjunctionOp(filters: Array>) { + return ['any'].concat(filters.map(convertFilter)); } -function compileInOp(property, values) { - if (property === '$type') values = values.map((value) => { - return types.indexOf(value); - }); - const left = JSON.stringify(values.sort(compare)); - const right = compilePropertyReference(property); - - if (values.length <= 200) return `${left}.indexOf(${right}) !== -1`; - - return `${'function(v, a, i, j) {' + - 'while (i <= j) { var m = (i + j) >> 1;' + - ' if (a[m] === v) return true; if (a[m] > v) j = m - 1; else i = m + 1;' + - '}' + - 'return false; }('}${right}, ${left},0,${values.length - 1})`; +function compileInOp(property: string, values: Array) { + if (values.length === 0) { return false; } + switch (property) { + case '$type': + return [`filter-type-in`, ['literal', values]]; + case '$id': + return [`filter-id-in`, ['literal', values]]; + default: + if (values.length > 200 && !values.some(v => typeof v !== typeof values[0])) { + return ['filter-in-large', property, ['literal', values.sort((a, b) => a < b ? -1 : a > b ? 1 : 0)]]; + } else { + return ['filter-in-small', property, ['literal', values]]; + } + } } -function compileHasOp(property) { - return property === '$id' ? '"id" in f' : `${JSON.stringify(property)} in p`; +function compileHasOp(property: string) { + switch (property) { + case '$type': + return true; + case '$id': + return [`filter-has-id`]; + default: + return [`filter-has`, property]; + } } -function compileNegation(expression) { - return `!(${expression})`; +function compileNegation(filter: mixed) { + return ['!', filter]; } -// Comparison function to sort numbers and strings -function compare(a, b) { - return a < b ? -1 : a > b ? 1 : 0; -} diff --git a/src/style-spec/validate/validate_filter.js b/src/style-spec/validate/validate_filter.js index b87ab3eb829..ddc4a5dc6cf 100644 --- a/src/style-spec/validate/validate_filter.js +++ b/src/style-spec/validate/validate_filter.js @@ -1,8 +1,11 @@ const ValidationError = require('../error/validation_error'); +const validateExpression = require('./validate_expression'); const validateEnum = require('./validate_enum'); const getType = require('../util/get_type'); const unbundle = require('../util/unbundle_jsonlint'); +const extend = require('../util/extend'); +const {isExpressionFilter} = require('../feature_filter'); module.exports = function validateFilter(options) { const value = options.value; @@ -16,6 +19,13 @@ module.exports = function validateFilter(options) { return [new ValidationError(key, value, 'array expected, %s found', getType(value))]; } + if (isExpressionFilter(unbundle.deep(value))) { + return validateExpression(extend({}, options, { + expressionContext: 'filter', + valueSpec: { value: 'boolean' } + })); + } + if (value.length < 1) { return [new ValidationError(key, value, 'filter array must have at least 1 element')]; } diff --git a/src/style/style_layer.js b/src/style/style_layer.js index 3f8fb507a9f..35ee7a9ad7e 100644 --- a/src/style/style_layer.js +++ b/src/style/style_layer.js @@ -13,6 +13,7 @@ import type Point from '@mapbox/point-geometry'; import type {Feature, GlobalProperties} from '../style-spec/expression'; import type RenderTexture from '../render/render_texture'; import type AnimationLoop from './animation_loop'; +import type {FeatureFilter} from '../style-spec/feature_filter'; const TRANSITION_SUFFIX = '-transition'; @@ -26,11 +27,12 @@ class StyleLayer extends Evented { sourceLayer: ?string; minzoom: ?number; maxzoom: ?number; - filter: any; + filter: mixed; paint: { [string]: any }; layout: { [string]: any }; viewportFrame: ?RenderTexture; + _featureFilter: FeatureFilter; _paintSpecifications: any; _layoutSpecifications: any; @@ -67,6 +69,8 @@ class StyleLayer extends Evented { this.paint = {}; this.layout = {}; + this._featureFilter = () => true; + this._paintSpecifications = styleSpec[`paint_${this.type}`]; this._layoutSpecifications = styleSpec[`layout_${this.type}`]; diff --git a/src/style/style_layer_index.js b/src/style/style_layer_index.js index 45dee0d2136..2297938112a 100644 --- a/src/style/style_layer_index.js +++ b/src/style/style_layer_index.js @@ -38,7 +38,7 @@ class StyleLayerIndex { const layer = this._layers[layerConfig.id] = StyleLayer.create(layerConfig); layer.updatePaintTransitions({transition: false}); - layer.filter = featureFilter(layer.filter); + layer._featureFilter = featureFilter(layer.filter); } for (const id of removedIds) { delete this._layerConfigs[id]; diff --git a/test/integration/expression-tests/coalesce/null/test.json b/test/integration/expression-tests/coalesce/null/test.json new file mode 100644 index 00000000000..0a8a7198115 --- /dev/null +++ b/test/integration/expression-tests/coalesce/null/test.json @@ -0,0 +1,21 @@ +{ + "expectExpressionType": null, + "expression": [ + "coalesce", + ["get", "z"], + 0 + ], + "inputs": [ + [{}, {"properties": {"z": 1}}], + [{}, {"properties": {"z": null}}] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "Value" + }, + "outputs": [1, 0] + } +} diff --git a/test/unit/style-spec/feature_filter.test.js b/test/unit/style-spec/feature_filter.test.js index 3077bf90c8f..fc03035b841 100644 --- a/test/unit/style-spec/feature_filter.test.js +++ b/test/unit/style-spec/feature_filter.test.js @@ -3,6 +3,45 @@ const test = require('mapbox-gl-js-test').test; const filter = require('../../../src/style-spec').featureFilter; +test('expression, zoom', (t) => { + const f = filter(['>=', ['number', ['get', 'x']], ['zoom']]); + t.equal(f({zoom: 1}, {properties: {x: 0}}), false); + t.equal(f({zoom: 1}, {properties: {x: 1.5}}), true); + t.equal(f({zoom: 1}, {properties: {x: 2.5}}), true); + t.equal(f({zoom: 2}, {properties: {x: 0}}), false); + t.equal(f({zoom: 2}, {properties: {x: 1.5}}), false); + t.equal(f({zoom: 2}, {properties: {x: 2.5}}), true); + t.end(); +}); + +test('expression, compare two properties', (t) => { + t.stub(console, 'warn'); + const f = filter(['==', ['string', ['get', 'x']], ['string', ['get', 'y']]]); + t.equal(f({zoom: 0}, {properties: {x: 1, y: 1}}), false); + t.equal(f({zoom: 0}, {properties: {x: '1', y: '1'}}), true); + t.equal(f({zoom: 0}, {properties: {x: 'same', y: 'same'}}), true); + t.equal(f({zoom: 0}, {properties: {x: null}}), false); + t.equal(f({zoom: 0}, {properties: {x: undefined}}), false); + t.end(); +}); + +test('expression, type error', (t) => { + t.throws(() => { + filter(['==', ['number', ['get', 'x']], ['string', ['get', 'y']]]); + }); + + t.throws(() => { + filter(['number', ['get', 'x']]); + }); + + t.doesNotThrow(() => { + filter(['boolean', ['get', 'x']]); + }); + + t.end(); +}); + + test('degenerate', (t) => { t.equal(filter()(), true); t.equal(filter(undefined)(), true); @@ -12,418 +51,434 @@ test('degenerate', (t) => { test('==, string', (t) => { const f = filter(['==', 'foo', 'bar']); - t.equal(f({properties: {foo: 'bar'}}), true); - t.equal(f({properties: {foo: 'baz'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 'bar'}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 'baz'}}), false); t.end(); }); test('==, number', (t) => { const f = filter(['==', 'foo', 0]); - t.equal(f({properties: {foo: 0}}), true); - t.equal(f({properties: {foo: 1}}), false); - t.equal(f({properties: {foo: '0'}}), false); - t.equal(f({properties: {foo: true}}), false); - t.equal(f({properties: {foo: false}}), false); - t.equal(f({properties: {foo: null}}), false); - t.equal(f({properties: {foo: undefined}}), false); - t.equal(f({properties: {}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 0}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 1}}), false); + t.equal(f({zoom: 0}, {properties: {foo: '0'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: true}}), false); + t.equal(f({zoom: 0}, {properties: {foo: false}}), false); + t.equal(f({zoom: 0}, {properties: {foo: null}}), false); + t.equal(f({zoom: 0}, {properties: {foo: undefined}}), false); + t.equal(f({zoom: 0}, {properties: {}}), false); t.end(); }); test('==, null', (t) => { const f = filter(['==', 'foo', null]); - t.equal(f({properties: {foo: 0}}), false); - t.equal(f({properties: {foo: 1}}), false); - t.equal(f({properties: {foo: '0'}}), false); - t.equal(f({properties: {foo: true}}), false); - t.equal(f({properties: {foo: false}}), false); - t.equal(f({properties: {foo: null}}), true); - t.equal(f({properties: {foo: undefined}}), false); - t.equal(f({properties: {}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 1}}), false); + t.equal(f({zoom: 0}, {properties: {foo: '0'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: true}}), false); + t.equal(f({zoom: 0}, {properties: {foo: false}}), false); + t.equal(f({zoom: 0}, {properties: {foo: null}}), true); + t.equal(f({zoom: 0}, {properties: {foo: undefined}}), false); + t.equal(f({zoom: 0}, {properties: {}}), false); t.end(); }); test('==, $type', (t) => { const f = filter(['==', '$type', 'LineString']); - t.equal(f({type: 1}), false); - t.equal(f({type: 2}), true); + t.equal(f({zoom: 0}, {type: 1}), false); + t.equal(f({zoom: 0}, {type: 2}), true); t.end(); }); test('==, $id', (t) => { const f = filter(['==', '$id', 1234]); - t.equal(f({id: 1234}), true); - t.equal(f({id: '1234'}), false); - t.equal(f({properties: {id: 1234}}), false); + t.equal(f({zoom: 0}, {id: 1234}), true); + t.equal(f({zoom: 0}, {id: '1234'}), false); + t.equal(f({zoom: 0}, {properties: {id: 1234}}), false); t.end(); }); test('!=, string', (t) => { const f = filter(['!=', 'foo', 'bar']); - t.equal(f({properties: {foo: 'bar'}}), false); - t.equal(f({properties: {foo: 'baz'}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 'bar'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 'baz'}}), true); t.end(); }); test('!=, number', (t) => { const f = filter(['!=', 'foo', 0]); - t.equal(f({properties: {foo: 0}}), false); - t.equal(f({properties: {foo: 1}}), true); - t.equal(f({properties: {foo: '0'}}), true); - t.equal(f({properties: {foo: true}}), true); - t.equal(f({properties: {foo: false}}), true); - t.equal(f({properties: {foo: null}}), true); - t.equal(f({properties: {foo: undefined}}), true); - t.equal(f({properties: {}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 1}}), true); + t.equal(f({zoom: 0}, {properties: {foo: '0'}}), true); + t.equal(f({zoom: 0}, {properties: {foo: true}}), true); + t.equal(f({zoom: 0}, {properties: {foo: false}}), true); + t.equal(f({zoom: 0}, {properties: {foo: null}}), true); + t.equal(f({zoom: 0}, {properties: {foo: undefined}}), true); + t.equal(f({zoom: 0}, {properties: {}}), true); t.end(); }); test('!=, null', (t) => { const f = filter(['!=', 'foo', null]); - t.equal(f({properties: {foo: 0}}), true); - t.equal(f({properties: {foo: 1}}), true); - t.equal(f({properties: {foo: '0'}}), true); - t.equal(f({properties: {foo: true}}), true); - t.equal(f({properties: {foo: false}}), true); - t.equal(f({properties: {foo: null}}), false); - t.equal(f({properties: {foo: undefined}}), true); - t.equal(f({properties: {}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 0}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 1}}), true); + t.equal(f({zoom: 0}, {properties: {foo: '0'}}), true); + t.equal(f({zoom: 0}, {properties: {foo: true}}), true); + t.equal(f({zoom: 0}, {properties: {foo: false}}), true); + t.equal(f({zoom: 0}, {properties: {foo: null}}), false); + t.equal(f({zoom: 0}, {properties: {foo: undefined}}), true); + t.equal(f({zoom: 0}, {properties: {}}), true); t.end(); }); test('!=, $type', (t) => { const f = filter(['!=', '$type', 'LineString']); - t.equal(f({type: 1}), true); - t.equal(f({type: 2}), false); + t.equal(f({zoom: 0}, {type: 1}), true); + t.equal(f({zoom: 0}, {type: 2}), false); t.end(); }); test('<, number', (t) => { const f = filter(['<', 'foo', 0]); - t.equal(f({properties: {foo: 1}}), false); - t.equal(f({properties: {foo: 0}}), false); - t.equal(f({properties: {foo: -1}}), true); - t.equal(f({properties: {foo: '1'}}), false); - t.equal(f({properties: {foo: '0'}}), false); - t.equal(f({properties: {foo: '-1'}}), false); - t.equal(f({properties: {foo: true}}), false); - t.equal(f({properties: {foo: false}}), false); - t.equal(f({properties: {foo: null}}), false); - t.equal(f({properties: {foo: undefined}}), false); - t.equal(f({properties: {}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 1}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); + t.equal(f({zoom: 0}, {properties: {foo: -1}}), true); + t.equal(f({zoom: 0}, {properties: {foo: '1'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: '0'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: '-1'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: true}}), false); + t.equal(f({zoom: 0}, {properties: {foo: false}}), false); + t.equal(f({zoom: 0}, {properties: {foo: null}}), false); + t.equal(f({zoom: 0}, {properties: {foo: undefined}}), false); + t.equal(f({zoom: 0}, {properties: {}}), false); t.end(); }); test('<, string', (t) => { const f = filter(['<', 'foo', '0']); - t.equal(f({properties: {foo: -1}}), false); - t.equal(f({properties: {foo: 0}}), false); - t.equal(f({properties: {foo: 1}}), false); - t.equal(f({properties: {foo: '1'}}), false); - t.equal(f({properties: {foo: '0'}}), false); - t.equal(f({properties: {foo: '-1'}}), true); - t.equal(f({properties: {foo: true}}), false); - t.equal(f({properties: {foo: false}}), false); - t.equal(f({properties: {foo: null}}), false); - t.equal(f({properties: {foo: undefined}}), false); + t.equal(f({zoom: 0}, {properties: {foo: -1}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 1}}), false); + t.equal(f({zoom: 0}, {properties: {foo: '1'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: '0'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: '-1'}}), true); + t.equal(f({zoom: 0}, {properties: {foo: true}}), false); + t.equal(f({zoom: 0}, {properties: {foo: false}}), false); + t.equal(f({zoom: 0}, {properties: {foo: null}}), false); + t.equal(f({zoom: 0}, {properties: {foo: undefined}}), false); t.end(); }); test('<=, number', (t) => { const f = filter(['<=', 'foo', 0]); - t.equal(f({properties: {foo: 1}}), false); - t.equal(f({properties: {foo: 0}}), true); - t.equal(f({properties: {foo: -1}}), true); - t.equal(f({properties: {foo: '1'}}), false); - t.equal(f({properties: {foo: '0'}}), false); - t.equal(f({properties: {foo: '-1'}}), false); - t.equal(f({properties: {foo: true}}), false); - t.equal(f({properties: {foo: false}}), false); - t.equal(f({properties: {foo: null}}), false); - t.equal(f({properties: {foo: undefined}}), false); - t.equal(f({properties: {}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 1}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 0}}), true); + t.equal(f({zoom: 0}, {properties: {foo: -1}}), true); + t.equal(f({zoom: 0}, {properties: {foo: '1'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: '0'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: '-1'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: true}}), false); + t.equal(f({zoom: 0}, {properties: {foo: false}}), false); + t.equal(f({zoom: 0}, {properties: {foo: null}}), false); + t.equal(f({zoom: 0}, {properties: {foo: undefined}}), false); + t.equal(f({zoom: 0}, {properties: {}}), false); t.end(); }); test('<=, string', (t) => { const f = filter(['<=', 'foo', '0']); - t.equal(f({properties: {foo: -1}}), false); - t.equal(f({properties: {foo: 0}}), false); - t.equal(f({properties: {foo: 1}}), false); - t.equal(f({properties: {foo: '1'}}), false); - t.equal(f({properties: {foo: '0'}}), true); - t.equal(f({properties: {foo: '-1'}}), true); - t.equal(f({properties: {foo: true}}), false); - t.equal(f({properties: {foo: false}}), false); - t.equal(f({properties: {foo: null}}), false); - t.equal(f({properties: {foo: undefined}}), false); + t.equal(f({zoom: 0}, {properties: {foo: -1}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 1}}), false); + t.equal(f({zoom: 0}, {properties: {foo: '1'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: '0'}}), true); + t.equal(f({zoom: 0}, {properties: {foo: '-1'}}), true); + t.equal(f({zoom: 0}, {properties: {foo: true}}), false); + t.equal(f({zoom: 0}, {properties: {foo: false}}), false); + t.equal(f({zoom: 0}, {properties: {foo: null}}), false); + t.equal(f({zoom: 0}, {properties: {foo: undefined}}), false); t.end(); }); test('>, number', (t) => { const f = filter(['>', 'foo', 0]); - t.equal(f({properties: {foo: 1}}), true); - t.equal(f({properties: {foo: 0}}), false); - t.equal(f({properties: {foo: -1}}), false); - t.equal(f({properties: {foo: '1'}}), false); - t.equal(f({properties: {foo: '0'}}), false); - t.equal(f({properties: {foo: '-1'}}), false); - t.equal(f({properties: {foo: true}}), false); - t.equal(f({properties: {foo: false}}), false); - t.equal(f({properties: {foo: null}}), false); - t.equal(f({properties: {foo: undefined}}), false); - t.equal(f({properties: {}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 1}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); + t.equal(f({zoom: 0}, {properties: {foo: -1}}), false); + t.equal(f({zoom: 0}, {properties: {foo: '1'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: '0'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: '-1'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: true}}), false); + t.equal(f({zoom: 0}, {properties: {foo: false}}), false); + t.equal(f({zoom: 0}, {properties: {foo: null}}), false); + t.equal(f({zoom: 0}, {properties: {foo: undefined}}), false); + t.equal(f({zoom: 0}, {properties: {}}), false); t.end(); }); test('>, string', (t) => { const f = filter(['>', 'foo', '0']); - t.equal(f({properties: {foo: -1}}), false); - t.equal(f({properties: {foo: 0}}), false); - t.equal(f({properties: {foo: 1}}), false); - t.equal(f({properties: {foo: '1'}}), true); - t.equal(f({properties: {foo: '0'}}), false); - t.equal(f({properties: {foo: '-1'}}), false); - t.equal(f({properties: {foo: true}}), false); - t.equal(f({properties: {foo: false}}), false); - t.equal(f({properties: {foo: null}}), false); - t.equal(f({properties: {foo: undefined}}), false); + t.equal(f({zoom: 0}, {properties: {foo: -1}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 1}}), false); + t.equal(f({zoom: 0}, {properties: {foo: '1'}}), true); + t.equal(f({zoom: 0}, {properties: {foo: '0'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: '-1'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: true}}), false); + t.equal(f({zoom: 0}, {properties: {foo: false}}), false); + t.equal(f({zoom: 0}, {properties: {foo: null}}), false); + t.equal(f({zoom: 0}, {properties: {foo: undefined}}), false); t.end(); }); test('>=, number', (t) => { const f = filter(['>=', 'foo', 0]); - t.equal(f({properties: {foo: 1}}), true); - t.equal(f({properties: {foo: 0}}), true); - t.equal(f({properties: {foo: -1}}), false); - t.equal(f({properties: {foo: '1'}}), false); - t.equal(f({properties: {foo: '0'}}), false); - t.equal(f({properties: {foo: '-1'}}), false); - t.equal(f({properties: {foo: true}}), false); - t.equal(f({properties: {foo: false}}), false); - t.equal(f({properties: {foo: null}}), false); - t.equal(f({properties: {foo: undefined}}), false); - t.equal(f({properties: {}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 1}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 0}}), true); + t.equal(f({zoom: 0}, {properties: {foo: -1}}), false); + t.equal(f({zoom: 0}, {properties: {foo: '1'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: '0'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: '-1'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: true}}), false); + t.equal(f({zoom: 0}, {properties: {foo: false}}), false); + t.equal(f({zoom: 0}, {properties: {foo: null}}), false); + t.equal(f({zoom: 0}, {properties: {foo: undefined}}), false); + t.equal(f({zoom: 0}, {properties: {}}), false); t.end(); }); test('>=, string', (t) => { const f = filter(['>=', 'foo', '0']); - t.equal(f({properties: {foo: -1}}), false); - t.equal(f({properties: {foo: 0}}), false); - t.equal(f({properties: {foo: 1}}), false); - t.equal(f({properties: {foo: '1'}}), true); - t.equal(f({properties: {foo: '0'}}), true); - t.equal(f({properties: {foo: '-1'}}), false); - t.equal(f({properties: {foo: true}}), false); - t.equal(f({properties: {foo: false}}), false); - t.equal(f({properties: {foo: null}}), false); - t.equal(f({properties: {foo: undefined}}), false); + t.equal(f({zoom: 0}, {properties: {foo: -1}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 1}}), false); + t.equal(f({zoom: 0}, {properties: {foo: '1'}}), true); + t.equal(f({zoom: 0}, {properties: {foo: '0'}}), true); + t.equal(f({zoom: 0}, {properties: {foo: '-1'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: true}}), false); + t.equal(f({zoom: 0}, {properties: {foo: false}}), false); + t.equal(f({zoom: 0}, {properties: {foo: null}}), false); + t.equal(f({zoom: 0}, {properties: {foo: undefined}}), false); t.end(); }); test('in, degenerate', (t) => { const f = filter(['in', 'foo']); - t.equal(f({properties: {foo: 1}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 1}}), false); t.end(); }); test('in, string', (t) => { const f = filter(['in', 'foo', '0']); - t.equal(f({properties: {foo: 0}}), false); - t.equal(f({properties: {foo: '0'}}), true); - t.equal(f({properties: {foo: true}}), false); - t.equal(f({properties: {foo: false}}), false); - t.equal(f({properties: {foo: null}}), false); - t.equal(f({properties: {foo: undefined}}), false); - t.equal(f({properties: {}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); + t.equal(f({zoom: 0}, {properties: {foo: '0'}}), true); + t.equal(f({zoom: 0}, {properties: {foo: true}}), false); + t.equal(f({zoom: 0}, {properties: {foo: false}}), false); + t.equal(f({zoom: 0}, {properties: {foo: null}}), false); + t.equal(f({zoom: 0}, {properties: {foo: undefined}}), false); + t.equal(f({zoom: 0}, {properties: {}}), false); t.end(); }); test('in, number', (t) => { const f = filter(['in', 'foo', 0]); - t.equal(f({properties: {foo: 0}}), true); - t.equal(f({properties: {foo: '0'}}), false); - t.equal(f({properties: {foo: true}}), false); - t.equal(f({properties: {foo: false}}), false); - t.equal(f({properties: {foo: null}}), false); - t.equal(f({properties: {foo: undefined}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 0}}), true); + t.equal(f({zoom: 0}, {properties: {foo: '0'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: true}}), false); + t.equal(f({zoom: 0}, {properties: {foo: false}}), false); + t.equal(f({zoom: 0}, {properties: {foo: null}}), false); + t.equal(f({zoom: 0}, {properties: {foo: undefined}}), false); t.end(); }); test('in, null', (t) => { const f = filter(['in', 'foo', null]); - t.equal(f({properties: {foo: 0}}), false); - t.equal(f({properties: {foo: '0'}}), false); - t.equal(f({properties: {foo: true}}), false); - t.equal(f({properties: {foo: false}}), false); - t.equal(f({properties: {foo: null}}), true); - t.equal(f({properties: {foo: undefined}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); + t.equal(f({zoom: 0}, {properties: {foo: '0'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: true}}), false); + t.equal(f({zoom: 0}, {properties: {foo: false}}), false); + t.equal(f({zoom: 0}, {properties: {foo: null}}), true); + t.equal(f({zoom: 0}, {properties: {foo: undefined}}), false); t.end(); }); test('in, multiple', (t) => { const f = filter(['in', 'foo', 0, 1]); - t.equal(f({properties: {foo: 0}}), true); - t.equal(f({properties: {foo: 1}}), true); - t.equal(f({properties: {foo: 3}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 0}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 1}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 3}}), false); t.end(); }); test('in, large_multiple', (t) => { - const f = filter(['in', 'foo'].concat(Array.apply(null, {length: 2000}).map(Number.call, Number))); - t.equal(f({properties: {foo: 0}}), true); - t.equal(f({properties: {foo: 1}}), true); - t.equal(f({properties: {foo: 1999}}), true); - t.equal(f({properties: {foo: 2000}}), false); + const values = Array.apply(null, {length: 2000}).map(Number.call, Number); + values.reverse(); + const f = filter(['in', 'foo'].concat(values)); + t.equal(f({zoom: 0}, {properties: {foo: 0}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 1}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 1999}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 2000}}), false); + t.end(); +}); + +test('in, large_multiple, heterogeneous', (t) => { + const values = Array.apply(null, {length: 2000}).map(Number.call, Number); + values.push('a'); + values.unshift('b'); + const f = filter(['in', 'foo'].concat(values)); + t.equal(f({zoom: 0}, {properties: {foo: 'b'}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 'a'}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 0}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 1}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 1999}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 2000}}), false); t.end(); }); test('in, $type', (t) => { const f = filter(['in', '$type', 'LineString', 'Polygon']); - t.equal(f({type: 1}), false); - t.equal(f({type: 2}), true); - t.equal(f({type: 3}), true); + t.equal(f({zoom: 0}, {type: 1}), false); + t.equal(f({zoom: 0}, {type: 2}), true); + t.equal(f({zoom: 0}, {type: 3}), true); const f1 = filter(['in', '$type', 'Polygon', 'LineString', 'Point']); - t.equal(f1({type: 1}), true); - t.equal(f1({type: 2}), true); - t.equal(f1({type: 3}), true); + t.equal(f1({zoom: 0}, {type: 1}), true); + t.equal(f1({zoom: 0}, {type: 2}), true); + t.equal(f1({zoom: 0}, {type: 3}), true); t.end(); }); test('!in, degenerate', (t) => { const f = filter(['!in', 'foo']); - t.equal(f({properties: {foo: 1}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 1}}), true); t.end(); }); test('!in, string', (t) => { const f = filter(['!in', 'foo', '0']); - t.equal(f({properties: {foo: 0}}), true); - t.equal(f({properties: {foo: '0'}}), false); - t.equal(f({properties: {foo: null}}), true); - t.equal(f({properties: {foo: undefined}}), true); - t.equal(f({properties: {}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 0}}), true); + t.equal(f({zoom: 0}, {properties: {foo: '0'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: null}}), true); + t.equal(f({zoom: 0}, {properties: {foo: undefined}}), true); + t.equal(f({zoom: 0}, {properties: {}}), true); t.end(); }); test('!in, number', (t) => { const f = filter(['!in', 'foo', 0]); - t.equal(f({properties: {foo: 0}}), false); - t.equal(f({properties: {foo: '0'}}), true); - t.equal(f({properties: {foo: null}}), true); - t.equal(f({properties: {foo: undefined}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); + t.equal(f({zoom: 0}, {properties: {foo: '0'}}), true); + t.equal(f({zoom: 0}, {properties: {foo: null}}), true); + t.equal(f({zoom: 0}, {properties: {foo: undefined}}), true); t.end(); }); test('!in, null', (t) => { const f = filter(['!in', 'foo', null]); - t.equal(f({properties: {foo: 0}}), true); - t.equal(f({properties: {foo: '0'}}), true); - t.equal(f({properties: {foo: null}}), false); - t.equal(f({properties: {foo: undefined}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 0}}), true); + t.equal(f({zoom: 0}, {properties: {foo: '0'}}), true); + t.equal(f({zoom: 0}, {properties: {foo: null}}), false); + t.equal(f({zoom: 0}, {properties: {foo: undefined}}), true); t.end(); }); test('!in, multiple', (t) => { const f = filter(['!in', 'foo', 0, 1]); - t.equal(f({properties: {foo: 0}}), false); - t.equal(f({properties: {foo: 1}}), false); - t.equal(f({properties: {foo: 3}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 1}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 3}}), true); t.end(); }); test('!in, large_multiple', (t) => { const f = filter(['!in', 'foo'].concat(Array.apply(null, {length: 2000}).map(Number.call, Number))); - t.equal(f({properties: {foo: 0}}), false); - t.equal(f({properties: {foo: 1}}), false); - t.equal(f({properties: {foo: 1999}}), false); - t.equal(f({properties: {foo: 2000}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 1}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 1999}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 2000}}), true); t.end(); }); test('!in, $type', (t) => { const f = filter(['!in', '$type', 'LineString', 'Polygon']); - t.equal(f({type: 1}), true); - t.equal(f({type: 2}), false); - t.equal(f({type: 3}), false); + t.equal(f({zoom: 0}, {type: 1}), true); + t.equal(f({zoom: 0}, {type: 2}), false); + t.equal(f({zoom: 0}, {type: 3}), false); t.end(); }); test('any', (t) => { const f1 = filter(['any']); - t.equal(f1({properties: {foo: 1}}), false); + t.equal(f1({zoom: 0}, {properties: {foo: 1}}), false); const f2 = filter(['any', ['==', 'foo', 1]]); - t.equal(f2({properties: {foo: 1}}), true); + t.equal(f2({zoom: 0}, {properties: {foo: 1}}), true); const f3 = filter(['any', ['==', 'foo', 0]]); - t.equal(f3({properties: {foo: 1}}), false); + t.equal(f3({zoom: 0}, {properties: {foo: 1}}), false); const f4 = filter(['any', ['==', 'foo', 0], ['==', 'foo', 1]]); - t.equal(f4({properties: {foo: 1}}), true); + t.equal(f4({zoom: 0}, {properties: {foo: 1}}), true); t.end(); }); test('all', (t) => { const f1 = filter(['all']); - t.equal(f1({properties: {foo: 1}}), true); + t.equal(f1({zoom: 0}, {properties: {foo: 1}}), true); const f2 = filter(['all', ['==', 'foo', 1]]); - t.equal(f2({properties: {foo: 1}}), true); + t.equal(f2({zoom: 0}, {properties: {foo: 1}}), true); const f3 = filter(['all', ['==', 'foo', 0]]); - t.equal(f3({properties: {foo: 1}}), false); + t.equal(f3({zoom: 0}, {properties: {foo: 1}}), false); const f4 = filter(['all', ['==', 'foo', 0], ['==', 'foo', 1]]); - t.equal(f4({properties: {foo: 1}}), false); + t.equal(f4({zoom: 0}, {properties: {foo: 1}}), false); t.end(); }); test('none', (t) => { const f1 = filter(['none']); - t.equal(f1({properties: {foo: 1}}), true); + t.equal(f1({zoom: 0}, {properties: {foo: 1}}), true); const f2 = filter(['none', ['==', 'foo', 1]]); - t.equal(f2({properties: {foo: 1}}), false); + t.equal(f2({zoom: 0}, {properties: {foo: 1}}), false); const f3 = filter(['none', ['==', 'foo', 0]]); - t.equal(f3({properties: {foo: 1}}), true); + t.equal(f3({zoom: 0}, {properties: {foo: 1}}), true); const f4 = filter(['none', ['==', 'foo', 0], ['==', 'foo', 1]]); - t.equal(f4({properties: {foo: 1}}), false); + t.equal(f4({zoom: 0}, {properties: {foo: 1}}), false); t.end(); }); test('has', (t) => { const f = filter(['has', 'foo']); - t.equal(f({properties: {foo: 0}}), true); - t.equal(f({properties: {foo: 1}}), true); - t.equal(f({properties: {foo: '0'}}), true); - t.equal(f({properties: {foo: true}}), true); - t.equal(f({properties: {foo: false}}), true); - t.equal(f({properties: {foo: null}}), true); - t.equal(f({properties: {foo: undefined}}), true); - t.equal(f({properties: {}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 0}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 1}}), true); + t.equal(f({zoom: 0}, {properties: {foo: '0'}}), true); + t.equal(f({zoom: 0}, {properties: {foo: true}}), true); + t.equal(f({zoom: 0}, {properties: {foo: false}}), true); + t.equal(f({zoom: 0}, {properties: {foo: null}}), true); + t.equal(f({zoom: 0}, {properties: {foo: undefined}}), true); + t.equal(f({zoom: 0}, {properties: {}}), false); t.end(); }); test('!has', (t) => { const f = filter(['!has', 'foo']); - t.equal(f({properties: {foo: 0}}), false); - t.equal(f({properties: {foo: 1}}), false); - t.equal(f({properties: {foo: '0'}}), false); - t.equal(f({properties: {foo: false}}), false); - t.equal(f({properties: {foo: false}}), false); - t.equal(f({properties: {foo: null}}), false); - t.equal(f({properties: {foo: undefined}}), false); - t.equal(f({properties: {}}), true); + t.equal(f({zoom: 0}, {properties: {foo: 0}}), false); + t.equal(f({zoom: 0}, {properties: {foo: 1}}), false); + t.equal(f({zoom: 0}, {properties: {foo: '0'}}), false); + t.equal(f({zoom: 0}, {properties: {foo: false}}), false); + t.equal(f({zoom: 0}, {properties: {foo: false}}), false); + t.equal(f({zoom: 0}, {properties: {foo: null}}), false); + t.equal(f({zoom: 0}, {properties: {foo: undefined}}), false); + t.equal(f({zoom: 0}, {properties: {}}), true); t.end(); }); diff --git a/test/unit/style-spec/fixture/filters.output.json b/test/unit/style-spec/fixture/filters.output.json index a27efe71294..0b0e0b1b5bf 100644 --- a/test/unit/style-spec/fixture/filters.output.json +++ b/test/unit/style-spec/fixture/filters.output.json @@ -8,8 +8,8 @@ "line": 22 }, { - "message": "layers[2].filter[0]: expected one of [==, !=, >, >=, <, <=, in, !in, all, any, none, has, !has], \"=\" found", - "line": 30 + "message": "layers[2].filter[0]: Unknown expression \"=\". If you wanted a literal array, use [\"literal\", [...]].", + "line": 29 }, { "message": "layers[3].filter: filter array for operator \"==\" must have 3 elements", @@ -24,8 +24,8 @@ "line": 59 }, { - "message": "layers[6].filter[2]: string, number, or boolean expected, array found", - "line": 71 + "message": "layers[6].filter[2]: Expected an array with at least one element. If you wanted a literal array, use [\"literal\", []].", + "line": 68 }, { "message": "layers[7].filter[2]: expected one of [Point, LineString, Polygon], \"value\" found", @@ -44,11 +44,11 @@ "line": 103 }, { - "message": "layers[12].filter: filter array for \"has\" operator must have 2 elements", + "message": "layers[12].filter: Expected arguments of type (String) | (String, Object), but found (String, String) instead.", "line": 131 }, { - "message": "layers[13].filter[1]: string expected, object found", - "line": 144 + "message": "layers[13].filter[1]: Bare objects invalid. Use [\"literal\", {...}] instead.", + "line": 142 } ] \ No newline at end of file