diff --git a/src/style-spec/expression/definitions/match.js b/src/style-spec/expression/definitions/match.js index d4f015f5328..86d22dca2d2 100644 --- a/src/style-spec/expression/definitions/match.js +++ b/src/style-spec/expression/definitions/match.js @@ -3,11 +3,11 @@ import assert from 'assert'; import { typeOf } from '../values'; +import { ValueType, type Type } from '../types'; import type { Expression } from '../expression'; import type ParsingContext from '../parsing_context'; import type EvaluationContext from '../evaluation_context'; -import type { Type } from '../types'; // Map input label values to output expression index type Cases = {[number | string]: number}; @@ -84,19 +84,25 @@ class Match implements Expression { outputs.push(result); } - const input = context.parse(args[1], 1, inputType); + const input = context.parse(args[1], 1, ValueType); if (!input) return null; const otherwise = context.parse(args[args.length - 1], args.length - 1, outputType); if (!otherwise) return null; assert(inputType && outputType); + + if (input.type.kind !== 'value' && context.concat(1).checkSubtype((inputType: any), input.type)) { + return null; + } + return new Match((inputType: any), (outputType: any), input, cases, outputs, otherwise); } evaluate(ctx: EvaluationContext) { const input = (this.input.evaluate(ctx): any); - return (this.outputs[this.cases[input]] || this.otherwise).evaluate(ctx); + const output = (typeOf(input) === this.inputType && this.outputs[this.cases[input]]) || this.otherwise; + return output.evaluate(ctx); } eachChild(fn: (Expression) => void) { @@ -134,7 +140,7 @@ class Match implements Expression { } } - const coerceLabel = (label) => this.input.type.kind === 'number' ? Number(label) : label; + const coerceLabel = (label) => this.inputType.kind === 'number' ? Number(label) : label; for (const [outputIndex, labels] of groupedByOutput) { if (labels.length === 1) { diff --git a/src/style-spec/function/convert.js b/src/style-spec/function/convert.js index 1fd944418f4..3fa43c170b5 100644 --- a/src/style-spec/function/convert.js +++ b/src/style-spec/function/convert.js @@ -141,37 +141,24 @@ function convertZoomAndPropertyFunction(parameters, propertySpec, stops, default function convertPropertyFunction(parameters, propertySpec, stops, defaultExpression) { const type = getFunctionType(parameters, propertySpec); - const inputType = typeof stops[0][0]; - assert( - inputType === 'string' || - inputType === 'number' || - inputType === 'boolean' - ); - - let input = [inputType, ['get', parameters.property]]; - let expression; let isStep = false; - if (type === 'categorical' && inputType === 'boolean') { + if (type === 'categorical' && typeof stops[0][0] === 'boolean') { assert(parameters.stops.length > 0 && parameters.stops.length <= 2); - if (parameters.stops[0][0] === false) { - input = ['!', input]; - } - expression = [ 'case', input, parameters.stops[0][1] ]; - if (parameters.stops.length > 1) { - expression.push(parameters.stops[1][1]); - } else { - expression.push(defaultExpression); + expression = ['case']; + for (const stop of stops) { + expression.push(['==', ['get', parameters.property], stop[0]], stop[1]); } + expression.push(defaultExpression); return expression; } else if (type === 'categorical') { - expression = ['match', input]; + expression = ['match', ['get', parameters.property]]; } else if (type === 'interval') { - expression = ['step', input]; + expression = ['step', ['number', ['get', parameters.property]]]; isStep = true; } else if (type === 'exponential') { const base = parameters.base !== undefined ? parameters.base : 1; - expression = ['interpolate', ['exponential', base], input]; + expression = ['interpolate', ['exponential', base], ['number', ['get', parameters.property]]]; } else { throw new Error(`Unknown property function type ${type}`); } diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json index a732a542517..ce51ed31b3e 100644 --- a/src/style-spec/reference/v8.json +++ b/src/style-spec/reference/v8.json @@ -2321,7 +2321,7 @@ } }, "match": { - "doc": "Selects the output whose label value matches the input value, or the fallback value if no match is found. The `input` can be any string or number expression (e.g. `[\"get\", \"building_type\"]`). Each label can either be a single literal value or an array of values.", + "doc": "Selects the output whose label value matches the input value, or the fallback value if no match is found. The input can be any expression (e.g. `[\"get\", \"building_type\"]`). Each label must either be a single literal value or an array of literal values (e.g. `\"a\"` or `[\"c\", \"b\"]`), and those values must be all strings or all numbers. (The values `\"1\"` and `1` cannot both be labels in the same match expression.) If the input type does not match the type of the labels, the result will be the fallback value.", "group": "Decision", "sdk-support": { "basic functionality": { diff --git a/test/integration/expression-tests/match/basic/test.json b/test/integration/expression-tests/match/basic/test.json index 3e9d1736f85..1178d0e80a6 100644 --- a/test/integration/expression-tests/match/basic/test.json +++ b/test/integration/expression-tests/match/basic/test.json @@ -4,7 +4,8 @@ [{}, {"properties": {"x": "a"}}], [{}, {"properties": {"x": "b"}}], [{}, {"properties": {"x": "c"}}], - [{}, {"properties": {"x": 0}}] + [{}, {"properties": {"x": 0}}], + [{}, {"properties": {}}] ], "expected": { "compiled": { @@ -17,13 +18,12 @@ "Apple", "Banana", "Kumquat", - { - "error": "Expected value to be of type string, but found number instead." - } + "Kumquat", + "Kumquat" ], "serialized": [ "match", - ["string", ["get", "x"]], + ["get", "x"], "a", "Apple", "b", diff --git a/test/integration/expression-tests/match/label-number/test.json b/test/integration/expression-tests/match/label-number/test.json index 1ee106a7723..2e1bad800e8 100644 --- a/test/integration/expression-tests/match/label-number/test.json +++ b/test/integration/expression-tests/match/label-number/test.json @@ -20,15 +20,11 @@ "match", "otherwise", "otherwise", - { - "error": "Expected value to be of type number, but found string instead." - }, - { - "error": "Expected value to be of type number, but found boolean instead." - }, - {"error": "Expected value to be of type number, but found null instead."}, - {"error": "Expected value to be of type number, but found null instead."} + "otherwise", + "otherwise", + "otherwise", + "otherwise" ], - "serialized": ["match", ["number", ["get", "x"]], 0, "match", "otherwise"] + "serialized": ["match", ["get", "x"], 0, "match", "otherwise"] } } diff --git a/test/unit/style-spec/convert_function.test.js b/test/unit/style-spec/convert_function.test.js index 9100a135dab..5b489c676fe 100644 --- a/test/unit/style-spec/convert_function.test.js +++ b/test/unit/style-spec/convert_function.test.js @@ -2,6 +2,51 @@ import { test } from 'mapbox-gl-js-test'; import convertFunction from '../../../src/style-spec/function/convert'; test('convertFunction', (t) => { + t.test('boolean categorical', (t) => { + const fn = { + type: 'categorical', + property: 'p', + stops: [ + [true, 'true'], + [false, 'false'] + ], + default: 'default' + }; + + t.deepEqual(convertFunction(fn, {}), [ + 'case', + ['==', ['get', 'p'], true], + 'true', + ['==', ['get', 'p'], false], + 'false', + 'default' + ]); + + t.end(); + }); + + t.test('numeric categorical', (t) => { + const fn = { + type: 'categorical', + property: 'p', + stops: [ + [0, '0'], + [1, '1'] + ], + default: 'default' + }; + + t.deepEqual(convertFunction(fn, {}), [ + 'match', + ['get', 'p'], + 0, '0', + 1, '1', + 'default' + ]); + + t.end(); + }); + t.test('feature-constant text-field with token replacement', (t) => { const functionValue = { stops: [