From 90a5d3dc1a92e3d99ff5bee90df77476a022384c Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Thu, 26 Jan 2017 16:26:02 -0800 Subject: [PATCH] Add "default" property for functions It is used in the following circumstances: * In categorical functions, when the feature value does not match any of the stop domain values. * In property and zoom-and-property functions, when a feature does not contain a value for the specified property. * In identity functions, when the feature value is not valid for the style property (for example, if the function is being used for a `circle-color` property but the feature property value is not a string or not a valid color). * In interval or exponential property and zoom-and-property functions, when the feature value is not numeric. If no default is provided, the style property's default is used in these circumstances. Implementing this required breaking changes to the function API in mapbox-gl-style-spec. The export is now a function which accepts the function definition and the spec JSON for a style property. It handles color parsing and validation of feature values internally. --- docs/style-spec/_generate/index.html | 11 + js/style-spec/CHANGELOG.md | 9 + js/style-spec/function/index.js | 85 ++- js/style-spec/reference/v8.json | 5 + js/style-spec/util/parse_color.js | 25 + js/style-spec/validate/validate_function.js | 14 +- js/style/parse_color.js | 44 -- js/style/style_declaration.js | 24 +- js/style/style_layer.js | 2 +- package.json | 1 - .../mapbox-gl-js#4124/expected.png | Bin 0 -> 362 bytes .../regressions/mapbox-gl-js#4124/style.json | 59 +++ .../mapbox-gl-js#4144/expected.png | Bin 0 -> 362 bytes .../regressions/mapbox-gl-js#4144/style.json | 73 +++ .../mapbox-gl-js#4146/expected.png | Bin 0 -> 362 bytes .../regressions/mapbox-gl-js#4146/style.json | 73 +++ .../mapbox-gl-js#4150/expected.png | Bin 0 -> 572 bytes .../regressions/mapbox-gl-js#4150/style.json | 126 +++++ test/js/data/fill_bucket.test.js | 3 + .../style-spec/fixture/functions.input.json | 39 ++ .../style-spec/fixture/functions.output.json | 14 +- test/js/style-spec/function.test.js | 487 ++++++++++++++++-- test/js/style-spec/parse_color.js | 13 + test/js/style/style_declaration.test.js | 18 - 24 files changed, 964 insertions(+), 161 deletions(-) create mode 100644 js/style-spec/util/parse_color.js delete mode 100644 js/style/parse_color.js create mode 100644 test/integration/render-tests/regressions/mapbox-gl-js#4124/expected.png create mode 100644 test/integration/render-tests/regressions/mapbox-gl-js#4124/style.json create mode 100644 test/integration/render-tests/regressions/mapbox-gl-js#4144/expected.png create mode 100644 test/integration/render-tests/regressions/mapbox-gl-js#4144/style.json create mode 100644 test/integration/render-tests/regressions/mapbox-gl-js#4146/expected.png create mode 100644 test/integration/render-tests/regressions/mapbox-gl-js#4146/style.json create mode 100644 test/integration/render-tests/regressions/mapbox-gl-js#4150/expected.png create mode 100644 test/integration/render-tests/regressions/mapbox-gl-js#4150/style.json create mode 100644 test/js/style-spec/parse_color.js diff --git a/docs/style-spec/_generate/index.html b/docs/style-spec/_generate/index.html index bcbe2c1e630..e108e797a28 100755 --- a/docs/style-spec/_generate/index.html +++ b/docs/style-spec/_generate/index.html @@ -856,6 +856,17 @@

Fun
functions return the output value of the stop equal to the function input.
+
+
default
+
A value to serve as a fallback function result when a value isn't otherwise available. It is used in the following circumstances:
+
    +
  • In categorical functions, when the feature value does not match any of the stop domain values.
  • +
  • In property and zoom-and-property functions, when a feature does not contain a value for the specified property.
  • +
  • In identity functions, when the feature value is not valid for the style property (for example, if the function is being used for a circle-color property but the feature property value is not a string or not a valid color).
  • +
  • In interval or exponential property and zoom-and-property functions, when the feature value is not numeric.
  • +
+
If no default is provided, the style property's default is used in these circumstances.
+
colorSpace
Optional enum. One of rgb, lab, hcl.
diff --git a/js/style-spec/CHANGELOG.md b/js/style-spec/CHANGELOG.md index 6d5330ddc6c..e6e33ffd751 100644 --- a/js/style-spec/CHANGELOG.md +++ b/js/style-spec/CHANGELOG.md @@ -1,3 +1,12 @@ +## master + +* BREAKING CHANGE: the API for the `function` module has changed. The `interpolated` and `piecewise-constant` exports +were replaced with a single unnamed export, a function which accepts an object conforming to the style spec "function" +definition, and an object defining a style spec property. It handles color parsing and validation of feature values +internally. +* Functions now support a "default" property. +* `parseColor` was promoted from gl-js. + ## 8.11.0 * Merge `feature-filter` repository into this repository #639 diff --git a/js/style-spec/function/index.js b/js/style-spec/function/index.js index 999c73ecb08..1c461baf3af 100644 --- a/js/style-spec/function/index.js +++ b/js/style-spec/function/index.js @@ -1,15 +1,23 @@ 'use strict'; const colorSpaces = require('./color_spaces'); +const parseColor = require('../util/parse_color'); +const extend = require('../util/extend'); +const getType = require('../util/get_type'); function identityFunction(x) { return x; } -function createFunction(parameters, defaultType) { +function createFunction(parameters, propertySpec) { + const isColor = propertySpec.type === 'color'; + let fun; if (!isFunctionDefinition(parameters)) { + if (isColor && parameters) { + parameters = parseColor(parameters); + } fun = function() { return parameters; }; @@ -20,7 +28,23 @@ function createFunction(parameters, defaultType) { const zoomAndFeatureDependent = parameters.stops && typeof parameters.stops[0][0] === 'object'; const featureDependent = zoomAndFeatureDependent || parameters.property !== undefined; const zoomDependent = zoomAndFeatureDependent || !featureDependent; - const type = parameters.type || defaultType || 'exponential'; + const type = parameters.type || (propertySpec.function === 'interpolated' ? 'exponential' : 'interval'); + + if (isColor) { + parameters = extend({}, parameters); + + if (parameters.stops) { + parameters.stops = parameters.stops.map((stop) => { + return [stop[0], parseColor(stop[1])]; + }); + } + + if (parameters.default) { + parameters.default = parseColor(parameters.default); + } else { + parameters.default = parseColor(propertySpec.default); + } + } let innerFun; let hashedStops; @@ -84,26 +108,30 @@ function createFunction(parameters, defaultType) { } for (const z in featureFunctions) { - featureFunctionStops.push([featureFunctions[z].zoom, createFunction(featureFunctions[z])]); + featureFunctionStops.push([featureFunctions[z].zoom, createFunction(featureFunctions[z], propertySpec)]); } fun = function(zoom, feature) { return outputFunction(evaluateExponentialFunction({ stops: featureFunctionStops, base: parameters.base - }, zoom)(zoom, feature)); + }, propertySpec, zoom)(zoom, feature)); }; fun.isFeatureConstant = false; fun.isZoomConstant = false; } else if (zoomDependent) { fun = function(zoom) { - return outputFunction(innerFun(parameters, zoom, hashedStops)); + return outputFunction(innerFun(parameters, propertySpec, zoom, hashedStops)); }; fun.isFeatureConstant = true; fun.isZoomConstant = false; } else { fun = function(zoom, feature) { - return outputFunction(innerFun(parameters, feature[parameters.property], hashedStops)); + const value = feature[parameters.property]; + if (value === undefined) { + return coalesce(parameters.default, propertySpec.default); + } + return outputFunction(innerFun(parameters, propertySpec, value, hashedStops)); }; fun.isFeatureConstant = false; fun.isZoomConstant = true; @@ -113,21 +141,21 @@ function createFunction(parameters, defaultType) { return fun; } -function evaluateCategoricalFunction(parameters, input, hashedStops) { - const value = hashedStops[input]; - if (value === undefined) { - // If the input is not found, return the first value from the original array by default - return parameters.stops[0][1]; - } +function coalesce(a, b, c) { + if (a !== undefined) return a; + if (b !== undefined) return b; + if (c !== undefined) return c; +} - return value; +function evaluateCategoricalFunction(parameters, propertySpec, input, hashedStops) { + return coalesce(hashedStops[input], parameters.default, propertySpec.default); } -function evaluateIntervalFunction(parameters, input) { +function evaluateIntervalFunction(parameters, propertySpec, input) { // Edge cases + if (getType(input) !== 'number') return coalesce(parameters.default, propertySpec.default); const n = parameters.stops.length; if (n === 1) return parameters.stops[0][1]; - if (input === undefined || input === null) return parameters.stops[n - 1][1]; if (input <= parameters.stops[0][0]) return parameters.stops[0][1]; if (input >= parameters.stops[n - 1][0]) return parameters.stops[n - 1][1]; @@ -136,13 +164,13 @@ function evaluateIntervalFunction(parameters, input) { return parameters.stops[index][1]; } -function evaluateExponentialFunction(parameters, input) { +function evaluateExponentialFunction(parameters, propertySpec, input) { const base = parameters.base !== undefined ? parameters.base : 1; // Edge cases + if (getType(input) !== 'number') return coalesce(parameters.default, propertySpec.default); const n = parameters.stops.length; if (n === 1) return parameters.stops[0][1]; - if (input === undefined || input === null) return parameters.stops[n - 1][1]; if (input <= parameters.stops[0][0]) return parameters.stops[0][1]; if (input >= parameters.stops[n - 1][0]) return parameters.stops[n - 1][1]; @@ -158,8 +186,13 @@ function evaluateExponentialFunction(parameters, input) { ); } -function evaluateIdentityFunction(parameters, input) { - return input; +function evaluateIdentityFunction(parameters, propertySpec, input) { + if (propertySpec.type === 'color') { + input = parseColor(input); + } else if (getType(input) !== propertySpec.type) { + input = undefined; + } + return coalesce(input, parameters.default, propertySpec.default); } function binarySearchForIndex(stops, input) { @@ -190,6 +223,10 @@ function interpolate(input, base, inputLower, inputUpper, outputLower, outputUpp return function() { const evaluatedLower = outputLower.apply(undefined, arguments); const evaluatedUpper = outputUpper.apply(undefined, arguments); + // Special case for fill-outline-color, which has no spec default. + if (evaluatedLower === undefined || evaluatedUpper === undefined) { + return undefined; + } return interpolate(input, base, inputLower, inputUpper, evaluatedLower, evaluatedUpper); }; } else if (outputLower.length) { @@ -225,13 +262,5 @@ function isFunctionDefinition(value) { return typeof value === 'object' && (value.stops || value.type === 'identity'); } - +module.exports = createFunction; module.exports.isFunctionDefinition = isFunctionDefinition; - -module.exports.interpolated = function(parameters) { - return createFunction(parameters, 'exponential'); -}; - -module.exports['piecewise-constant'] = function(parameters) { - return createFunction(parameters, 'interval'); -}; diff --git a/js/style-spec/reference/v8.json b/js/style-spec/reference/v8.json index 7a00c305b04..b50c398565a 100644 --- a/js/style-spec/reference/v8.json +++ b/js/style-spec/reference/v8.json @@ -1643,6 +1643,11 @@ }, "doc": "The color space in which colors interpolated. Interpolating colors in perceptual color spaces like LAB and HCL tend to produce color ramps that look more consistent and produce colors that can be differentiated more easily than those interpolated in RGB space.", "default": "rgb" + }, + "default": { + "type": "*", + "required": false, + "doc": "A value to serve as a fallback function result when a value isn't otherwise available. It is used in the following circumstances:\n* In categorical functions, when the feature value does not match any of the stop domain values.\n* In property and zoom-and-property functions, when a feature does not contain a value for the specified property.\n* In identity functions, when the feature value is not valid for the style property (for example, if the function is being used for a `circle-color` property but the feature property value is not a string or not a valid color).\n* In interval or exponential property and zoom-and-property functions, when the feature value is not numeric.\nIf no default is provided, the style property's default is used in these circumstances." } }, "function_stop": { diff --git a/js/style-spec/util/parse_color.js b/js/style-spec/util/parse_color.js new file mode 100644 index 00000000000..8e7e401f0bf --- /dev/null +++ b/js/style-spec/util/parse_color.js @@ -0,0 +1,25 @@ +'use strict'; + +const parseColorString = require('csscolorparser').parseCSSColor; + +module.exports = function parseColor(input) { + if (typeof input === 'string') { + const rgba = parseColorString(input); + if (!rgba) { return undefined; } + + // GL expects all components to be in the range [0, 1] and to be + // multipled by the alpha value. + return [ + rgba[0] / 255 * rgba[3], + rgba[1] / 255 * rgba[3], + rgba[2] / 255 * rgba[3], + rgba[3] + ]; + + } else if (Array.isArray(input)) { + return input; + + } else { + return undefined; + } +}; diff --git a/js/style-spec/validate/validate_function.js b/js/style-spec/validate/validate_function.js index eaa444c7a3d..56256c8cbd7 100644 --- a/js/style-spec/validate/validate_function.js +++ b/js/style-spec/validate/validate_function.js @@ -29,7 +29,10 @@ module.exports = function validateFunction(options) { valueSpec: options.styleSpec.function, style: options.style, styleSpec: options.styleSpec, - objectElementValidators: { stops: validateFunctionStops } + objectElementValidators: { + stops: validateFunctionStops, + default: validateFunctionDefault + } }); if (functionType !== 'identity' && !options.value.stops) { @@ -184,4 +187,13 @@ module.exports = function validateFunction(options) { return []; } + function validateFunctionDefault(options) { + return validate({ + key: options.key, + value: options.value, + valueSpec: functionValueSpec, + style: options.style, + styleSpec: options.styleSpec + }); + } }; diff --git a/js/style/parse_color.js b/js/style/parse_color.js deleted file mode 100644 index 38f8be5fabb..00000000000 --- a/js/style/parse_color.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; - -const parseColorString = require('csscolorparser').parseCSSColor; -const util = require('../util/util'); -const MapboxGLFunction = require('../style-spec/function'); - -const cache = {}; - -module.exports = function parseColor(input) { - - if (input && MapboxGLFunction.isFunctionDefinition(input)) { - - if (!input.stops) return input; - else return util.extend({}, input, { - stops: input.stops.map((stop) => { - return [stop[0], parseColor(stop[1])]; - }) - }); - - } else if (typeof input === 'string') { - - if (!cache[input]) { - const rgba = parseColorString(input); - if (!rgba) { throw new Error(`Invalid color ${input}`); } - - // GL expects all components to be in the range [0, 1] and to be - // multipled by the alpha value. - cache[input] = [ - rgba[0] / 255 * rgba[3], - rgba[1] / 255 * rgba[3], - rgba[2] / 255 * rgba[3], - rgba[3] - ]; - } - - return cache[input]; - - } else if (Array.isArray(input)) { - return input; - - } else { - throw new Error(`Invalid color ${input}`); - } -}; diff --git a/js/style/style_declaration.js b/js/style/style_declaration.js index 584c29d91ed..fee3c44acc3 100644 --- a/js/style/style_declaration.js +++ b/js/style/style_declaration.js @@ -1,26 +1,19 @@ 'use strict'; -const MapboxGLFunction = require('../style-spec/function'); -const parseColor = require('./parse_color'); +const createFunction = require('../style-spec/function'); const util = require('../util/util'); class StyleDeclaration { constructor(reference, value) { this.value = util.clone(value); - this.isFunction = MapboxGLFunction.isFunctionDefinition(value); + this.isFunction = createFunction.isFunctionDefinition(value); // immutable representation of value. used for comparison this.json = JSON.stringify(this.value); this.minimum = reference.minimum; - this.isColor = reference.type === 'color'; - - const parsedValue = this.isColor && this.value ? parseColor(this.value) : value; - let specDefault = reference.default; - if (specDefault && reference.type === 'color') specDefault = parseColor(specDefault); - - this.function = MapboxGLFunction[reference.function || 'piecewise-constant'](parsedValue, specDefault); + this.function = createFunction(this.value, reference); this.isFeatureConstant = this.function.isFeatureConstant; this.isZoomConstant = this.function.isZoomConstant; @@ -35,19 +28,18 @@ class StyleDeclaration { } } - this.functionInterpolationT = MapboxGLFunction.interpolated({ + this.functionInterpolationT = createFunction({ + type: 'exponential', stops: interpolationAmountStops, - base: value.base, - colorSpace: value.colorSpace + base: value.base + }, { + type: 'number' }); } } calculate(globalProperties, featureProperties) { const value = this.function(globalProperties && globalProperties.zoom, featureProperties || {}); - if (this.isColor && value) { - return parseColor(value); - } if (this.minimum !== undefined && value < this.minimum) { return this.minimum; } diff --git a/js/style/style_layer.js b/js/style/style_layer.js index 146f8f8a650..fe285ce403b 100644 --- a/js/style/style_layer.js +++ b/js/style/style_layer.js @@ -5,7 +5,7 @@ const StyleTransition = require('./style_transition'); const StyleDeclaration = require('./style_declaration'); const styleSpec = require('../style-spec/reference/latest'); const validateStyle = require('./validate_style'); -const parseColor = require('./parse_color'); +const parseColor = require('./../style-spec/util/parse_color'); const Evented = require('../util/evented'); const TRANSITION_SUFFIX = '-transition'; diff --git a/package.json b/package.json index 28b87889805..6202abe1af4 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "@mapbox/unitbezier": "^0.0.0", "brfs": "^1.4.0", "bubleify": "^0.5.1", - "csscolorparser": "^1.0.2", "earcut": "^2.0.3", "geojson-rewind": "^0.1.0", "geojson-vt": "^2.4.0", diff --git a/test/integration/render-tests/regressions/mapbox-gl-js#4124/expected.png b/test/integration/render-tests/regressions/mapbox-gl-js#4124/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..6995cc208ddf1197ede295dae87b809113f391af GIT binary patch literal 362 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Or9=|Ar*{or0Om)Ffb~EcyA9b z%!^87V7tH^F5u#O-IifnS{mmvHcL;V(_hZ43ZK-PFhl-)!1WgQ2q(4Qd<(K}Da=l& zO24hU;mz|!zve0{A%RDwD`YYbBpi7$=dd{*gI;&z6JLYeZIvtXT1GhxNlF^1r^!uZY@oWljE%SNX9u$I7q#3yy9ye9R|n z?K3m$-jzJr9?=YKgOrSZv9)rxXJ0N%-M^42cYREBBP)0Hl`Gm|vo<^B?r&wvWlcM? zE$6L4;-X67-MNOr3xXCb+xM*M$O`Z4#>LOVV>0gvd{*gI;&z6JLYeZIvtXT1GhxNlF^1r^!uZY@oWljE%SNX9u$I7q#3yy9ye9R|n z?K3m$-jzJr9?=YKgOrSZv9)rxXJ0N%-M^42cYREBBP)0Hl`Gm|vo<^B?r&wvWlcM? zE$6L4;-X67-MNOr3xXCb+xM*M$O`Z4#>LOVV>0gvd{*gI;&z6JLYeZIvtXT1GhxNlF^1r^!uZY@oWljE%SNX9u$I7q#3yy9ye9R|n z?K3m$-jzJr9?=YKgOrSZv9)rxXJ0N%-M^42cYREBBP)0Hl`Gm|vo<^B?r&wvWlcM? zE$6L4;-X67-MNOr3xXCb+xM*M$O`Z4#>LOVV>0gv(hZhT*~U+T4PGpq9xBfiZVj@3%dR?wRz5pXu(Ng)0MP=E|&lo0o4KqjG8Ihd=MJ0s6;>@wW) zfk|HFluC5;;<$3p-DltEh>M>pWtTU6x=AX|Yl63tvGd+{Uctd{-f7*}TNbmnPm;69 z)6DEk$XX+tH8zJ=yvlWMikZ~PTI=hteSXV7F_jalmX;T<-<bv(!=*gs<{L5K8uU(q-Nq1))_iZ5_&uckF|Ls11-e+yMEb8gs>ZjR{dF2wX z@cY_)e4ipYl}lMU?>vXr35XZK{`e5y#-pLUI^g69yU#b@N*t81lMF~>;FZ!|^W)i! z$IESO9~?Ma(Qtjo^LDi`RjElY8k&#We48BT79MdkeZOz?PPs?2Zhy1nx-)>@+i<4H z@u8IU8;$#YuSFxKDNJAB=F2ba6S6|XpH;YOwdVwHgOf|_j!A~kEBxyVR9|pnp0Q|KJFV9sBR1@yBzerMRrCfysfv)78&qol`;+ E0AAkvF#rGn literal 0 HcmV?d00001 diff --git a/test/integration/render-tests/regressions/mapbox-gl-js#4150/style.json b/test/integration/render-tests/regressions/mapbox-gl-js#4150/style.json new file mode 100644 index 00000000000..52b5da148d1 --- /dev/null +++ b/test/integration/render-tests/regressions/mapbox-gl-js#4150/style.json @@ -0,0 +1,126 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64, + "ignored": { + "native": "https://github.com/mapbox/mapbox-gl-native/issues/4860" + } + } + }, + "sources": { + "a": { + "type": "geojson", + "data": { + "type": "Feature", + "properties": { + "property": 0 + }, + "geometry": { + "type": "Point", + "coordinates": [ + -10, + -10 + ] + } + } + }, + "b": { + "type": "geojson", + "data": { + "type": "Feature", + "properties": { + "property": 0 + }, + "geometry": { + "type": "Point", + "coordinates": [ + 10, + -10 + ] + } + } + }, + "c": { + "type": "geojson", + "data": { + "type": "Feature", + "properties": { + "property": "invalid" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -10, + 10 + ] + } + } + }, + "d": { + "type": "geojson", + "data": { + "type": "Feature", + "properties": { + "property": "invalid" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 10, + 10 + ] + } + } + } + }, + "layers": [ + { + "id": "a", + "source": "a", + "type": "circle", + "paint": { + "circle-color": { + "type": "identity", + "property": "property", + "default": "red" + } + } + }, + { + "id": "b", + "source": "b", + "type": "circle", + "paint": { + "circle-color": { + "type": "identity", + "property": "property" + } + } + }, + { + "id": "c", + "source": "c", + "type": "circle", + "paint": { + "circle-color": { + "type": "identity", + "property": "property", + "default": "blue" + } + } + }, + { + "id": "d", + "source": "d", + "type": "circle", + "paint": { + "circle-color": { + "type": "identity", + "property": "property" + } + } + } + ] +} diff --git a/test/js/data/fill_bucket.test.js b/test/js/data/fill_bucket.test.js index 6860cc86d10..d76c0bb7215 100644 --- a/test/js/data/fill_bucket.test.js +++ b/test/js/data/fill_bucket.test.js @@ -16,6 +16,9 @@ const feature = vt.layers.water.feature(0); function createFeature(points) { return { + properties: { + 'foo': 1 + }, loadGeometry: function() { return points; } diff --git a/test/js/style-spec/fixture/functions.input.json b/test/js/style-spec/fixture/functions.input.json index 8f79cc98c8a..45f7bfb53c0 100644 --- a/test/js/style-spec/fixture/functions.input.json +++ b/test/js/style-spec/fixture/functions.input.json @@ -710,6 +710,45 @@ ] } } + }, + { + "id": "valid default", + "type": "fill", + "source": "source", + "source-layer": "layer", + "paint": { + "fill-opacity": { + "property": "mapbox", + "type": "identity", + "default": 0 + } + } + }, + { + "id": "invalid default - wrong type", + "type": "fill", + "source": "source", + "source-layer": "layer", + "paint": { + "fill-opacity": { + "property": "mapbox", + "type": "identity", + "default": "0" + } + } + }, + { + "id": "invalid default - color", + "type": "fill", + "source": "source", + "source-layer": "layer", + "paint": { + "fill-color": { + "property": "mapbox", + "type": "identity", + "default": "invalid" + } + } } ] } diff --git a/test/js/style-spec/fixture/functions.output.json b/test/js/style-spec/fixture/functions.output.json index 72633c24669..0dd064a3bc3 100644 --- a/test/js/style-spec/fixture/functions.output.json +++ b/test/js/style-spec/fixture/functions.output.json @@ -79,13 +79,13 @@ "message": "layers[18].paint.fill-color.stops[0][0]: number expected, string found\nIf you intended to use a categorical function, specify `\"type\": \"categorical\"`.", "line": 341 }, - { - "message": "layers[31].paint.fill-color.stops[0][0]: property value must be a number or string" - }, { "message": "layers[19].paint.fill-color: \"property\" property is required", "line": 354 }, + { + "message": "layers[31].paint.fill-color.stops[0][0]: property value must be a number or string" + }, { "message": "layers[22].paint.background-color.stops: identity function may not have a \"stops\" property", "line": 402 @@ -149,5 +149,13 @@ { "message": "layers[37].paint.fill-opacity.stops[1]: stop zoom values must appear in ascending order", "line": 705 + }, + { + "message": "layers[39].paint.fill-opacity.default: number expected, string found", + "line": 736 + }, + { + "message": "layers[40].paint.fill-color.default: color expected, \"invalid\" found", + "line": 749 } ] \ No newline at end of file diff --git a/test/js/style-spec/function.test.js b/test/js/style-spec/function.test.js index 4712d6ec683..e33ec84e011 100644 --- a/test/js/style-spec/function.test.js +++ b/test/js/style-spec/function.test.js @@ -1,12 +1,11 @@ 'use strict'; const test = require('mapbox-gl-js-test').test; -const createInterpolated = require('../../../js/style-spec/function').interpolated; -const createInterval = require('../../../js/style-spec/function')['piecewise-constant']; +const createFunction = require('../../../js/style-spec/function'); test('constant function', (t) => { t.test('number', (t) => { - const f = createInterpolated(1); + const f = createFunction(1, {type: 'number'}); t.equal(f(0), 1); t.equal(f(1), 1); @@ -16,7 +15,7 @@ test('constant function', (t) => { }); t.test('string', (t) => { - const f = createInterpolated('mapbox'); + const f = createFunction('mapbox', {type: 'string'}); t.equal(f(0), 'mapbox'); t.equal(f(1), 'mapbox'); @@ -25,8 +24,18 @@ test('constant function', (t) => { t.end(); }); + t.test('color', (t) => { + const f = createFunction('red', {type: 'color'}); + + t.deepEqual(f(0), [1, 0, 0, 1]); + t.deepEqual(f(1), [1, 0, 0, 1]); + t.deepEqual(f(2), [1, 0, 0, 1]); + + t.end(); + }); + t.test('array', (t) => { - const f = createInterpolated([1]); + const f = createFunction([1], {type: 'array'}); t.deepEqual(f(0), [1]); t.deepEqual(f(1), [1]); @@ -40,9 +49,12 @@ test('constant function', (t) => { test('exponential function', (t) => { t.test('is the default for interpolated properties', (t) => { - const f = createInterpolated({ + const f = createFunction({ stops: [[1, 2], [3, 6]], base: 2 + }, { + type: 'number', + function: 'interpolated' }); t.equal(f(2), 30 / 9); @@ -51,10 +63,12 @@ test('exponential function', (t) => { }); t.test('base', (t) => { - const f = createInterpolated({ + const f = createFunction({ type: 'exponential', stops: [[1, 2], [3, 6]], base: 2 + }, { + type: 'number' }); t.equal(f(0), 2); @@ -67,9 +81,11 @@ test('exponential function', (t) => { }); t.test('one stop', (t) => { - const f = createInterpolated({ + const f = createFunction({ type: 'exponential', stops: [[1, 2]] + }, { + type: 'number' }); t.equal(f(0), 2); @@ -80,9 +96,11 @@ test('exponential function', (t) => { }); t.test('two stops', (t) => { - const f = createInterpolated({ + const f = createFunction({ type: 'exponential', stops: [[1, 2], [3, 6]] + }, { + type: 'number' }); t.equal(f(0), 2); @@ -95,9 +113,11 @@ test('exponential function', (t) => { }); t.test('three stops', (t) => { - const f = createInterpolated({ + const f = createFunction({ type: 'exponential', stops: [[1, 2], [3, 6], [5, 10]] + }, { + type: 'number' }); t.equal(f(0), 2); @@ -114,9 +134,11 @@ test('exponential function', (t) => { }); t.test('four stops', (t) => { - const f = createInterpolated({ + const f = createFunction({ type: 'exponential', stops: [[1, 2], [3, 6], [5, 10], [7, 14]] + }, { + type: 'number' }); t.equal(f(0), 2); @@ -153,9 +175,11 @@ test('exponential function', (t) => { [12889, 1000000], [40000, 10000000] ]; - const f = createInterpolated({ + const f = createFunction({ type: 'exponential', stops: stops + }, { + type: 'number' }); t.equal(f(2), 100); @@ -171,11 +195,28 @@ test('exponential function', (t) => { t.end(); }); + t.test('color', (t) => { + const f = createFunction({ + type: 'exponential', + stops: [[1, 'red'], [11, 'blue']] + }, { + type: 'color' + }); + + t.deepEqual(f(0), [1, 0, 0, 1]); + t.deepEqual(f(5), [0.6, 0, 0.4, 1]); + t.deepEqual(f(11), [0, 0, 1, 1]); + + t.end(); + }); + t.test('lab colorspace', (t) => { - const f = createInterpolated({ + const f = createFunction({ type: 'exponential', colorSpace: 'lab', stops: [[1, [0, 0, 0, 1]], [10, [0, 1, 1, 1]]] + }, { + type: 'color' }); t.deepEqual(f(0), [0, 0, 0, 1]); @@ -187,10 +228,12 @@ test('exponential function', (t) => { }); t.test('rgb colorspace', (t) => { - const f = createInterpolated({ + const f = createFunction({ type: 'exponential', colorSpace: 'rgb', stops: [[0, [0, 0, 0, 1]], [10, [1, 1, 1, 1]]] + }, { + type: 'color' }); t.deepEqual(f(5).map((n) => { @@ -202,10 +245,12 @@ test('exponential function', (t) => { t.test('unknown color spaces', (t) => { t.throws(() => { - createInterpolated({ + createFunction({ type: 'exponential', colorSpace: 'unknown', stops: [[1, [0, 0, 0, 1]], [10, [0, 1, 1, 1]]] + }, { + type: 'color' }); }, 'Unknown color space: unknown'); @@ -219,16 +264,20 @@ test('exponential function', (t) => { stops: [[1, [0, 0, 0, 1]], [10, [0, 1, 1, 1]]] }; const paramsCopy = JSON.parse(JSON.stringify(params)); - createInterpolated(params); + createFunction(params, { + type: 'color' + }); t.deepEqual(params, paramsCopy); t.end(); }); - t.test('property function', (t) => { - const f = createInterpolated({ + t.test('property present', (t) => { + const f = createFunction({ property: 'foo', type: 'exponential', stops: [[0, 0], [1, 2]] + }, { + type: 'number' }); t.equal(f(0, {foo: 1}), 2); @@ -236,23 +285,73 @@ test('exponential function', (t) => { t.end(); }); - t.test('property function, missing property', (t) => { - const f = createInterpolated({ + t.test('property absent, function default', (t) => { + const f = createFunction({ + property: 'foo', + type: 'exponential', + stops: [[0, 0], [1, 2]], + default: 3 + }, { + type: 'number' + }); + + t.equal(f(0, {}), 3); + + t.end(); + }); + + t.test('property absent, spec default', (t) => { + const f = createFunction({ property: 'foo', type: 'exponential', stops: [[0, 0], [1, 2]] + }, { + type: 'number', + default: 3 }); - t.equal(f(0, {}), 2); + t.equal(f(0, {}), 3); + + t.end(); + }); + + t.test('property type mismatch, function default', (t) => { + const f = createFunction({ + property: 'foo', + type: 'exponential', + stops: [[0, 0], [1, 2]], + default: 3 + }, { + type: 'string' + }); + + t.equal(f(0, {foo: 'string'}), 3); + + t.end(); + }); + + t.test('property type mismatch, spec default', (t) => { + const f = createFunction({ + property: 'foo', + type: 'exponential', + stops: [[0, 0], [1, 2]] + }, { + type: 'string', + default: 3 + }); + + t.equal(f(0, {foo: 'string'}), 3); t.end(); }); t.test('zoom-and-property function, one stop', (t) => { - const f = createInterpolated({ + const f = createFunction({ type: 'exponential', property: 'prop', stops: [[{ zoom: 1, value: 1 }, 2]] + }, { + type: 'number' }); t.equal(f(0, { prop: 0 }), 2); @@ -269,7 +368,7 @@ test('exponential function', (t) => { }); t.test('zoom-and-property function, two stops', (t) => { - const f = createInterpolated({ + const f = createFunction({ type: 'exponential', property: 'prop', base: 1, @@ -278,6 +377,8 @@ test('exponential function', (t) => { [{ zoom: 1, value: 2 }, 4], [{ zoom: 3, value: 0 }, 0], [{ zoom: 3, value: 2 }, 12]] + }, { + type: 'number' }); t.equal(f(0, { prop: 1 }), 2); @@ -295,7 +396,7 @@ test('exponential function', (t) => { }); t.test('zoom-and-property function, three stops', (t) => { - const f = createInterpolated({ + const f = createFunction({ type: 'exponential', property: 'prop', base: 1, @@ -306,6 +407,8 @@ test('exponential function', (t) => { [{ zoom: 3, value: 2}, 12], [{ zoom: 5, value: 0}, 0], [{ zoom: 5, value: 2}, 20]] + }, { + type: 'number' }); t.equal(f(0, { prop: 1 }), 2); @@ -316,7 +419,7 @@ test('exponential function', (t) => { }); t.test('zoom-and-property function, two stops, fractional zoom', (t) => { - const f = createInterpolated({ + const f = createFunction({ type: 'exponential', property: 'prop', base: 1, @@ -324,6 +427,8 @@ test('exponential function', (t) => { [{ zoom: 1.9, value: 0 }, 4], [{ zoom: 2.1, value: 0 }, 8] ] + }, { + type: 'number' }); t.equal(f(1.9, { prop: 1 }), 4); @@ -333,13 +438,38 @@ test('exponential function', (t) => { t.end(); }); + t.test('zoom-and-property function, no default', (t) => { + // This can happen for fill-outline-color, where the spec has no default. + + const f = createFunction({ + type: 'exponential', + property: 'prop', + base: 1, + stops: [ + [{ zoom: 0, value: 1 }, 'red'], + [{ zoom: 1, value: 1 }, 'red'] + ] + }, { + type: 'color' + }); + + t.equal(f(0, {}), undefined); + t.equal(f(0.5, {}), undefined); + t.equal(f(1, {}), undefined); + + t.end(); + }); + t.end(); }); test('interval function', (t) => { t.test('is the default for piecewise-constant properties', (t) => { - const f = createInterval({ + const f = createFunction({ stops: [[-1, 11], [0, 111]] + }, { + type: 'number', + function: 'piecewise-constant' }); t.equal(f(-1.5), 11); @@ -351,9 +481,11 @@ test('interval function', (t) => { }); t.test('one stop', (t) => { - const f = createInterpolated({ + const f = createFunction({ type: 'interval', stops: [[0, 11]] + }, { + type: 'number' }); t.equal(f(-0.5), 11); @@ -364,9 +496,11 @@ test('interval function', (t) => { }); t.test('two stops', (t) => { - const f = createInterpolated({ + const f = createFunction({ type: 'interval', stops: [[-1, 11], [0, 111]] + }, { + type: 'number' }); t.equal(f(-1.5), 11); @@ -378,9 +512,11 @@ test('interval function', (t) => { }); t.test('three stops', (t) => { - const f = createInterpolated({ + const f = createFunction({ type: 'interval', stops: [[-1, 11], [0, 111], [1, 1111]] + }, { + type: 'number' }); t.equal(f(-1.5), 11); @@ -394,9 +530,11 @@ test('interval function', (t) => { }); t.test('four stops', (t) => { - const f = createInterpolated({ + const f = createFunction({ type: 'interval', stops: [[-1, 11], [0, 111], [1, 1111], [2, 11111]] + }, { + type: 'number' }); t.equal(f(-1.5), 11); @@ -411,11 +549,28 @@ test('interval function', (t) => { t.end(); }); + t.test('color', (t) => { + const f = createFunction({ + type: 'interval', + stops: [[1, 'red'], [11, 'blue']] + }, { + type: 'color' + }); + + t.deepEqual(f(0), [1, 0, 0, 1]); + t.deepEqual(f(0), [1, 0, 0, 1]); + t.deepEqual(f(11), [0, 0, 1, 1]); + + t.end(); + }); + t.test('property present', (t) => { - const f = createInterpolated({ + const f = createFunction({ property: 'foo', type: 'interval', stops: [[0, 'bad'], [1, 'good'], [2, 'bad']] + }, { + type: 'string' }); t.equal(f(0, {foo: 1.5}), 'good'); @@ -423,14 +578,62 @@ test('interval function', (t) => { t.end(); }); - t.test('property absent', (t) => { - const f = createInterpolated({ + t.test('property absent, function default', (t) => { + const f = createFunction({ + property: 'foo', + type: 'interval', + stops: [[0, 'zero'], [1, 'one'], [2, 'two']], + default: 'default' + }, { + type: 'string' + }); + + t.equal(f(0, {}), 'default'); + + t.end(); + }); + + t.test('property absent, spec default', (t) => { + const f = createFunction({ + property: 'foo', + type: 'interval', + stops: [[0, 'zero'], [1, 'one'], [2, 'two']] + }, { + type: 'string', + default: 'default' + }); + + t.equal(f(0, {}), 'default'); + + t.end(); + }); + + t.test('property type mismatch, function default', (t) => { + const f = createFunction({ + property: 'foo', + type: 'interval', + stops: [[0, 'zero'], [1, 'one'], [2, 'two']], + default: 'default' + }, { + type: 'string' + }); + + t.equal(f(0, {foo: 'string'}), 'default'); + + t.end(); + }); + + t.test('property type mismatch, spec default', (t) => { + const f = createFunction({ property: 'foo', type: 'interval', stops: [[0, 'zero'], [1, 'one'], [2, 'two']] + }, { + type: 'string', + default: 'default' }); - t.equal(f(0, {}), 'two'); + t.equal(f(0, {foo: 'string'}), 'default'); t.end(); }); @@ -439,26 +642,97 @@ test('interval function', (t) => { }); test('categorical function', (t) => { - t.test('property present', (t) => { - const f = createInterpolated({ + t.test('string', (t) => { + const f = createFunction({ property: 'foo', type: 'categorical', stops: [[0, 'bad'], [1, 'good'], [2, 'bad']] + }, { + type: 'string' }); + t.equal(f(0, {foo: 0}), 'bad'); t.equal(f(0, {foo: 1}), 'good'); + t.equal(f(0, {foo: 2}), 'bad'); t.end(); }); - t.test('property absent', (t) => { - const f = createInterpolated({ + t.test('string function default', (t) => { + const f = createFunction({ + property: 'foo', + type: 'categorical', + stops: [[0, 'zero'], [1, 'one'], [2, 'two']], + default: 'default' + }, { + type: 'string' + }); + + t.equal(f(0, {}), 'default'); + t.equal(f(0, {foo: 3}), 'default'); + + t.end(); + }); + + t.test('string spec default', (t) => { + const f = createFunction({ property: 'foo', type: 'categorical', stops: [[0, 'zero'], [1, 'one'], [2, 'two']] + }, { + type: 'string', + default: 'default' + }); + + t.equal(f(0, {}), 'default'); + t.equal(f(0, {foo: 3}), 'default'); + + t.end(); + }); + + t.test('color', (t) => { + const f = createFunction({ + property: 'foo', + type: 'categorical', + stops: [[0, 'red'], [1, 'blue']] + }, { + type: 'color' + }); + + t.deepEqual(f(0, {foo: 0}), [1, 0, 0, 1]); + t.deepEqual(f(1, {foo: 1}), [0, 0, 1, 1]); + + t.end(); + }); + + t.test('color function default', (t) => { + const f = createFunction({ + property: 'foo', + type: 'categorical', + stops: [[0, 'red'], [1, 'blue']], + default: 'lime' + }, { + type: 'color' }); - t.equal(f(0, {}), 'zero'); + t.deepEqual(f(0, {}), [0, 1, 0, 1]); + t.deepEqual(f(0, {foo: 3}), [0, 1, 0, 1]); + + t.end(); + }); + + t.test('color spec default', (t) => { + const f = createFunction({ + property: 'foo', + type: 'categorical', + stops: [[0, 'red'], [1, 'blue']] + }, { + type: 'color', + default: 'lime' + }); + + t.deepEqual(f(0, {}), [0, 1, 0, 1]); + t.deepEqual(f(0, {foo: 3}), [0, 1, 0, 1]); t.end(); }); @@ -467,10 +741,12 @@ test('categorical function', (t) => { }); test('identity function', (t) => { - t.test('property present', (t) => { - const f = createInterpolated({ + t.test('number', (t) => { + const f = createFunction({ property: 'foo', type: 'identity' + }, { + type: 'number' }); t.equal(f(0, {foo: 1}), 1); @@ -478,13 +754,114 @@ test('identity function', (t) => { t.end(); }); - t.test('property absent', (t) => { - const f = createInterpolated({ + t.test('number function default', (t) => { + const f = createFunction({ + property: 'foo', + type: 'identity', + default: 1 + }, { + type: 'string' + }); + + t.equal(f(0, {}), 1); + + t.end(); + }); + + t.test('number spec default', (t) => { + const f = createFunction({ property: 'foo', type: 'identity' + }, { + type: 'string', + default: 1 }); - t.equal(f(0, {}), undefined); + t.equal(f(0, {}), 1); + + t.end(); + }); + + t.test('color', (t) => { + const f = createFunction({ + property: 'foo', + type: 'identity' + }, { + type: 'color' + }); + + t.deepEqual(f(0, {foo: 'red'}), [1, 0, 0, 1]); + t.deepEqual(f(1, {foo: 'blue'}), [0, 0, 1, 1]); + + t.end(); + }); + + t.test('color function default', (t) => { + const f = createFunction({ + property: 'foo', + type: 'identity', + default: 'red' + }, { + type: 'color' + }); + + t.deepEqual(f(0, {}), [1, 0, 0, 1]); + + t.end(); + }); + + t.test('color spec default', (t) => { + const f = createFunction({ + property: 'foo', + type: 'identity' + }, { + type: 'color', + default: 'red' + }); + + t.deepEqual(f(0, {}), [1, 0, 0, 1]); + + t.end(); + }); + + t.test('color invalid', (t) => { + const f = createFunction({ + property: 'foo', + type: 'identity' + }, { + type: 'color', + default: 'red' + }); + + t.deepEqual(f(0, {foo: 'invalid'}), [1, 0, 0, 1]); + + t.end(); + }); + + t.test('property type mismatch, function default', (t) => { + const f = createFunction({ + property: 'foo', + type: 'identity', + default: 'default' + }, { + type: 'string' + }); + + t.equal(f(0, {foo: 0}), 'default'); + + t.end(); + }); + + t.test('property type mismatch, spec default', (t) => { + const f = createFunction({ + property: 'foo', + type: 'identity' + }, { + type: 'string', + default: 'default' + }); + + t.equal(f(0, {foo: 0}), 'default'); t.end(); }); @@ -493,13 +870,19 @@ test('identity function', (t) => { }); test('unknown function', (t) => { - t.throws(() => createInterpolated({type: 'nonesuch', stops: [[]]}), /Unknown function type "nonesuch"/); + t.throws(() => createFunction({ + type: 'nonesuch', stops: [[]] + }, { + type: 'string' + }), /Unknown function type "nonesuch"/); t.end(); }); test('isConstant', (t) => { t.test('constant', (t) => { - const f = createInterpolated(1); + const f = createFunction(1, { + type: 'string' + }); t.ok(f.isZoomConstant); t.ok(f.isFeatureConstant); @@ -508,8 +891,10 @@ test('isConstant', (t) => { }); t.test('zoom', (t) => { - const f = createInterpolated({ + const f = createFunction({ stops: [[1, 1]] + }, { + type: 'string' }); t.notOk(f.isZoomConstant); @@ -519,9 +904,11 @@ test('isConstant', (t) => { }); t.test('property', (t) => { - const f = createInterpolated({ + const f = createFunction({ stops: [[1, 1]], property: 'mapbox' + }, { + type: 'string' }); t.ok(f.isZoomConstant); @@ -531,9 +918,11 @@ test('isConstant', (t) => { }); t.test('zoom + property', (t) => { - const f = createInterpolated({ + const f = createFunction({ stops: [[{ zoom: 1, data: 1 }, 1]], property: 'mapbox' + }, { + type: 'string' }); t.notOk(f.isZoomConstant); diff --git a/test/js/style-spec/parse_color.js b/test/js/style-spec/parse_color.js new file mode 100644 index 00000000000..9052e8123b8 --- /dev/null +++ b/test/js/style-spec/parse_color.js @@ -0,0 +1,13 @@ +'use strict'; + +const test = require('mapbox-gl-js-test').test; +const parseColor = require('../../../js/style-spec/util/parse_color'); + +test('parseColor', (t) => { + t.deepEqual(parseColor('red'), [ 1, 0, 0, 1 ]); + t.deepEqual(parseColor('#ff00ff'), [ 1, 0, 1, 1 ]); + t.deepEqual(parseColor([ 1, 0, 0, 1 ]), [ 1, 0, 0, 1 ]); + t.deepEqual(parseColor(null), undefined); + t.deepEqual(parseColor('#00000'), undefined); + t.end(); +}); diff --git a/test/js/style/style_declaration.test.js b/test/js/style/style_declaration.test.js index 2734bfeb81e..45ddb86b492 100644 --- a/test/js/style/style_declaration.test.js +++ b/test/js/style/style_declaration.test.js @@ -39,24 +39,6 @@ test('StyleDeclaration', (t) => { t.end(); }); - t.test('color parsing', (t) => { - const reference = {type: "color", function: "interpolated"}; - t.deepEqual(new StyleDeclaration(reference, 'red').calculate({zoom: 0}), [ 1, 0, 0, 1 ]); - t.deepEqual(new StyleDeclaration(reference, '#ff00ff').calculate({zoom: 0}), [ 1, 0, 1, 1 ]); - t.deepEqual(new StyleDeclaration(reference, { stops: [[0, '#f00'], [1, '#0f0']] }).calculate({zoom: 0}), [1, 0, 0, 1]); - t.throws(() => { - t.ok(new StyleDeclaration(reference, { stops: [[0, '#f00'], [1, null]] })); - }, /Invalid color/); - t.throws(() => { - // hex value with only 5 digits should throw an Invalid color error - t.ok(new StyleDeclaration(reference, '#00000')); - }, Error, /Invalid color/i); - // cached - t.deepEqual(new StyleDeclaration(reference, '#ff00ff').calculate({zoom: 0}), [ 1, 0, 1, 1 ]); - t.deepEqual(new StyleDeclaration(reference, 'rgba(255, 51, 0, 1)').calculate({zoom: 0}), [ 1, 0.2, 0, 1 ]); - t.end(); - }); - t.test('property functions', (t) => { const declaration = new StyleDeclaration( {type: "number", function: "interpolated"},