diff --git a/src/style-spec/function/convert.js b/src/style-spec/function/convert.js index 9340001443f..95bc031c032 100644 --- a/src/style-spec/function/convert.js +++ b/src/style-spec/function/convert.js @@ -37,6 +37,12 @@ function convertFunction(parameters, propertySpec) { } else { expression = convertPropertyFunction(parameters, propertySpec); } + + if (expression[0] === 'curve' && expression[1][0] === 'step' && expression.length === 4) { + // degenerate step curve (i.e. a constant function): add a noop stop + expression.push(0); + expression.push(expression[3]); + } } else { // identity function expression = annotateValue(['get', parameters.property], propertySpec); @@ -115,16 +121,17 @@ function convertZoomAndPropertyFunction(parameters, propertySpec) { // otherwise. const functionType = getFunctionType({}, propertySpec); let interpolationType; + let isStep = false; if (functionType === 'exponential') { interpolationType = ['linear']; } else { interpolationType = ['step']; + isStep = true; } const expression = ['curve', interpolationType, ['zoom']]; for (const z of zoomStops) { - expression.push(z); - expression.push(convertPropertyFunction(featureFunctions[z], propertySpec)); + appendStopPair(expression, z, convertPropertyFunction(featureFunctions[z], propertySpec), isStep); } return expression; @@ -143,6 +150,7 @@ function convertPropertyFunction(parameters, propertySpec) { let input = [inputType, ['get', parameters.property]]; let expression; + let isStep = false; if (type === 'categorical' && inputType === 'boolean') { assert(parameters.stops.length > 0 && parameters.stops.length <= 2); if (parameters.stops[0][0] === false) { @@ -159,6 +167,7 @@ function convertPropertyFunction(parameters, propertySpec) { expression = ['match', input]; } else if (type === 'interval') { expression = ['curve', ['step'], input]; + isStep = true; } else if (type === 'exponential') { const base = parameters.base !== undefined ? parameters.base : 1; expression = ['curve', ['exponential', base], input]; @@ -167,8 +176,7 @@ function convertPropertyFunction(parameters, propertySpec) { } for (const stop of parameters.stops) { - expression.push(stop[0]); - expression.push(stop[1]); + appendStopPair(expression, stop[0], stop[1], isStep); } if (expression[0] === 'match') { @@ -181,8 +189,10 @@ function convertPropertyFunction(parameters, propertySpec) { function convertZoomFunction(parameters, propertySpec) { const type = getFunctionType(parameters, propertySpec); let expression; + let isStep = false; if (type === 'interval') { expression = ['curve', ['step'], ['zoom']]; + isStep = true; } else if (type === 'exponential') { const base = parameters.base !== undefined ? parameters.base : 1; expression = ['curve', ['exponential', base], ['zoom']]; @@ -191,13 +201,20 @@ function convertZoomFunction(parameters, propertySpec) { } for (const stop of parameters.stops) { - expression.push(stop[0]); - expression.push(stop[1]); + appendStopPair(expression, stop[0], stop[1], isStep); } return expression; } +function appendStopPair(curve, input, output, isStep) { + // step curves don't get the first input value, as it is redundant. + if (!(isStep && curve.length === 3)) { + curve.push(input); + } + curve.push(output); +} + function getFunctionType (parameters, propertySpec) { if (parameters.type) { return parameters.type; diff --git a/src/style-spec/function/definitions/curve.js b/src/style-spec/function/definitions/curve.js index aa245dd828c..b6f135bae03 100644 --- a/src/style-spec/function/definitions/curve.js +++ b/src/style-spec/function/definitions/curve.js @@ -56,13 +56,6 @@ class Curve implements Expression { } static parse(args: Array, context: ParsingContext) { - if (args.length < 5) - return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`); - - if (args.length % 2 !== 1) { - return context.error("Expected an even number of arguments."); - } - let [ , interpolation, input, ...rest] = args; if (!Array.isArray(interpolation) || interpolation.length === 0) { @@ -98,6 +91,17 @@ class Curve implements Expression { return context.error(`Unknown interpolation type ${String(interpolation[0])}`, 1, 0); } + const isStep = interpolation.name === 'step'; + + const minArgs = isStep ? 5 : 4; + if (args.length - 1 < minArgs) + return context.error(`Expected at least ${minArgs} arguments, but found only ${args.length - 1}.`); + + const parity = minArgs % 2; + if ((args.length - 1) % 2 !== parity) { + return context.error(`Expected an ${parity === 0 ? 'even' : 'odd'} number of arguments.`); + } + input = parseExpression(input, context.concat(2, NumberType)); if (!input) return null; @@ -107,19 +111,27 @@ class Curve implements Expression { if (context.expectedType && context.expectedType.kind !== 'Value') { outputType = context.expectedType; } + + if (isStep) { + rest.unshift(-Infinity); + } + for (let i = 0; i < rest.length; i += 2) { const label = rest[i]; const value = rest[i + 1]; + const labelKey = isStep ? i + 4 : i + 3; + const valueKey = isStep ? i + 5 : i + 4; + if (typeof label !== 'number') { - return context.error('Input/output pairs for "curve" expressions must be defined using literal numeric values (not computed expressions) for the input values.', i + 3); + return context.error('Input/output pairs for "curve" expressions must be defined using literal numeric values (not computed expressions) for the input values.', labelKey); } if (stops.length && stops[stops.length - 1][0] > label) { - return context.error('Input/output pairs for "curve" expressions must be arranged with input values in strictly ascending order.', i + 3); + return context.error('Input/output pairs for "curve" expressions must be arranged with input values in strictly ascending order.', labelKey); } - const parsed = parseExpression(value, context.concat(i + 4, outputType)); + const parsed = parseExpression(value, context.concat(valueKey, outputType)); if (!parsed) return null; outputType = outputType || parsed.type; stops.push([label, parsed]); diff --git a/test/integration/expression-tests/curve/infer-array-type/test.json b/test/integration/expression-tests/curve/infer-array-type/test.json index d5b381167a8..1c5b216e99f 100644 --- a/test/integration/expression-tests/curve/infer-array-type/test.json +++ b/test/integration/expression-tests/curve/infer-array-type/test.json @@ -4,7 +4,6 @@ "curve", ["step"], ["number", ["get", "x"]], - 0, ["literal", ["one"]], 10, ["literal", ["one", "two"]] diff --git a/test/integration/expression-tests/curve/step/test.json b/test/integration/expression-tests/curve/step/test.json index 2e38c459bcf..e213ad48b54 100644 --- a/test/integration/expression-tests/curve/step/test.json +++ b/test/integration/expression-tests/curve/step/test.json @@ -2,7 +2,7 @@ "expectExpressionType": null, "expression": [ "number", - ["curve", ["step"], ["number", ["get", "x"]], -1, 11, 0, 111, 1, 1111] + ["curve", ["step"], ["number", ["get", "x"]], 11, 0, 111, 1, 1111] ], "inputs": [ [{}, {"properties": {"x": -1.5}}], diff --git a/test/integration/render-tests/line-opacity/step-curve/style.json b/test/integration/render-tests/line-opacity/step-curve/style.json index 5cf69ba1477..5bb36fc91b2 100644 --- a/test/integration/render-tests/line-opacity/step-curve/style.json +++ b/test/integration/render-tests/line-opacity/step-curve/style.json @@ -71,7 +71,6 @@ "line-opacity": { "expression": [ "curve", ["step"], ["zoom"], - 0, ["match", ["string", ["get", "class"]], "motorway", 1, "trunk", 0.25, diff --git a/test/integration/render-tests/text-font/camera-function/style.json b/test/integration/render-tests/text-font/camera-function/style.json index b4bee113d40..379736fe3c9 100644 --- a/test/integration/render-tests/text-font/camera-function/style.json +++ b/test/integration/render-tests/text-font/camera-function/style.json @@ -42,7 +42,6 @@ "text-font": { "expression": [ "curve", ["step"], ["zoom"], - 0, [ "literal", [ "Open Sans Semibold", "Arial Unicode MS Bold" ]], 1, ["literal", [ "Open Sans Semibold", "Arial Unicode MS Bold" ]]