diff --git a/src/style-spec/expression/definitions/interpolate.js b/src/style-spec/expression/definitions/interpolate.js index 05622bfd7f1..f64f72bd769 100644 --- a/src/style-spec/expression/definitions/interpolate.js +++ b/src/style-spec/expression/definitions/interpolate.js @@ -116,7 +116,7 @@ class Interpolate implements Expression { return context.error('Input/output pairs for "interpolate" 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) { + if (stops.length && stops[stops.length - 1][0] >= label) { return context.error('Input/output pairs for "interpolate" expressions must be arranged with input values in strictly ascending order.', labelKey); } diff --git a/src/style-spec/expression/definitions/step.js b/src/style-spec/expression/definitions/step.js index d52c37d5a27..70869b0fafe 100644 --- a/src/style-spec/expression/definitions/step.js +++ b/src/style-spec/expression/definitions/step.js @@ -64,7 +64,7 @@ class Step implements Expression { return context.error('Input/output pairs for "step" 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) { + if (stops.length && stops[stops.length - 1][0] >= label) { return context.error('Input/output pairs for "step" expressions must be arranged with input values in strictly ascending order.', labelKey); } diff --git a/src/style-spec/function/convert.js b/src/style-spec/function/convert.js index 7ba8d4b711b..66cd919ea8e 100644 --- a/src/style-spec/function/convert.js +++ b/src/style-spec/function/convert.js @@ -220,6 +220,11 @@ function fixupDegenerateStepCurve(expression) { } function appendStopPair(curve, input, output, isStep) { + // Skip duplicate stop values. They were not validated for functions, but they are for expressions. + // https://github.com/mapbox/mapbox-gl-js/issues/4107 + if (curve.length > 3 && input === curve[curve.length - 2]) { + return; + } // step curves don't get the first input value, as it is redundant. if (!(isStep && curve.length === 2)) { curve.push(input); diff --git a/test/integration/expression-tests/interpolate/duplicate_stops/test.json b/test/integration/expression-tests/interpolate/duplicate_stops/test.json new file mode 100644 index 00000000000..00888d77569 --- /dev/null +++ b/test/integration/expression-tests/interpolate/duplicate_stops/test.json @@ -0,0 +1,15 @@ +{ + "expression": ["interpolate", ["linear"], 0, 0, 1, 0, 2], + "inputs": [[{}, {}]], + "expected": { + "compiled": { + "result": "error", + "errors": [ + { + "key": "[5]", + "error": "Input/output pairs for \"interpolate\" expressions must be arranged with input values in strictly ascending order." + } + ] + } + } +} diff --git a/test/integration/expression-tests/step/duplicate_stops/test.json b/test/integration/expression-tests/step/duplicate_stops/test.json new file mode 100644 index 00000000000..de52123697e --- /dev/null +++ b/test/integration/expression-tests/step/duplicate_stops/test.json @@ -0,0 +1,15 @@ +{ + "expression": ["step", 0, "a", 0, "b", 0, "c"], + "inputs": [[{}, {}]], + "expected": { + "compiled": { + "result": "error", + "errors": [ + { + "key": "[5]", + "error": "Input/output pairs for \"step\" expressions must be arranged with input values in strictly ascending order." + } + ] + } + } +} diff --git a/test/unit/style-spec/convert_function.test.js b/test/unit/style-spec/convert_function.test.js index 31176d82e2e..12dd3cfcbcc 100644 --- a/test/unit/style-spec/convert_function.test.js +++ b/test/unit/style-spec/convert_function.test.js @@ -42,5 +42,61 @@ test('convertFunction', (t) => { t.end(); }); + t.test('duplicate step function stops', (t) => { + const functionValue = { + stops: [ + [0, 'a'], + [1, 'b'], + [1, 'c'], + [2, 'd'] + ] + }; + + const expression = convertFunction(functionValue, { + type: 'string', + function: 'piecewise-constant' + }, 'text-field'); + t.deepEqual(expression, [ + 'step', + ['zoom'], + 'a', + 1, + 'b', + 2, + 'd' + ]); + + t.end(); + }); + + t.test('duplicate interpolate function stops', (t) => { + const functionValue = { + stops: [ + [0, 'a'], + [1, 'b'], + [1, 'c'], + [2, 'd'] + ] + }; + + const expression = convertFunction(functionValue, { + type: 'number', + function: 'interpolated' + }, 'text-size'); + t.deepEqual(expression, [ + 'interpolate', + ['exponential', 1], + ['zoom'], + 0, + 'a', + 1, + 'b', + 2, + 'd' + ]); + + t.end(); + }); + t.end(); });