Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "default" property for functions #4175

Merged
merged 4 commits into from
Feb 3, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/style-spec/_generate/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,17 @@ <h3 class='space-bottom1'><a href='#types-function' title='link to function'>Fun
<dd>functions return the output value of the stop equal to the function input.</dd>
</dl>
</div>
<div class="col12 clearfix pad0y pad2x space-bottom2">
<div><span class='code'><a id="function-default" href="#function-default">default</a></span></div>
<div>A value to serve as a fallback function result when a value isn't otherwise available. It is used in the following circumstances:</div>
<ul>
<li>In categorical functions, when the feature value does not match any of the stop domain values.</li>
<li>In property and zoom-and-property functions, when a feature does not contain a value for the specified property.</li>
<li>In identity functions, when the feature value is not valid for the style property (for example, if the function is being used for a <var>circle-color</var> property but the feature property value is not a string or not a valid color).</li>
<li>In interval or exponential property and zoom-and-property functions, when the feature value is not numeric.</li>
</ul>
<div>If no default is provided, the style property's default is used in these circumstances.</div>
</div>
<div class="col12 clearfix pad0y pad2x space-bottom2">
<div><span class='code'><a id="function-colorSpace" href="#function-colorSpace">colorSpace</a></span></div>
<div><em class='quiet'>Optional <a href='#types-string'>enum</a>. One of <var>rgb</var>, <var>lab</var>, <var>hcl</var>.</em></div>
Expand Down
9 changes: 5 additions & 4 deletions js/data/bucket/symbol_bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,12 @@ class SymbolBucket {
}

populate(features, options) {
const layout = this.layers[0].layout;
const layer = this.layers[0];
const layout = layer.layout;
const textFont = layout['text-font'];
const iconImage = layout['icon-image'];

const hasText = layout['text-field'] && textFont;
const hasText = textFont && (!layer.isLayoutValueFeatureConstant('text-field') || layout['text-field']);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the text-field property is data-driven, the value of layout['text-field'] is not defined.

@jfirebaugh ah, whoops - I made the bad assumption that layout contained the style JSON.

const hasIcon = iconImage;

this.features = [];
Expand All @@ -137,13 +138,13 @@ class SymbolBucket {

for (let i = 0; i < features.length; i++) {
const feature = features[i];
if (!this.layers[0].filter(feature)) {
if (!layer.filter(feature)) {
continue;
}

let text;
if (hasText) {
text = resolveText(this.layers[0], {zoom: this.zoom}, feature.properties);
text = resolveText(layer, {zoom: this.zoom}, feature.properties);
if (rtlTextPlugin.applyArabicShaping) {
text = rtlTextPlugin.applyArabicShaping(text);
}
Expand Down
9 changes: 9 additions & 0 deletions js/style-spec/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


## 8.11.0

* Merge `feature-filter` repository into this repository #639
Expand Down
107 changes: 63 additions & 44 deletions js/style-spec/function/index.js
Original file line number Diff line number Diff line change
@@ -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;
};
Expand All @@ -20,15 +28,38 @@ 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');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you thought about renaming the propertySpec.function "enum" values now that this is part of our public API?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed #4194 for what I think we should do there.


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;
if (type === 'exponential') {
innerFun = evaluateExponentialFunction;
} else if (type === 'interval') {
innerFun = evaluateIntervalFunction;
} else if (type === 'categorical') {
innerFun = evaluateCategoricalFunction;

// For categorical functions, generate an Object as a hashmap of the stops for fast searching
hashedStops = Object.create(null);
for (const stop of parameters.stops) {
hashedStops[stop[0]] = stop[1];
}
} else if (type === 'identity') {
innerFun = evaluateIdentityFunction;
} else {
Expand Down Expand Up @@ -60,15 +91,6 @@ function createFunction(parameters, defaultType) {
outputFunction = identityFunction;
}


// For categorical functions, generate an Object as a hashmap of the stops for fast searching
const hashedStops = Object.create(null);
if (innerFun === evaluateCategoricalFunction) {
for (let i = 0; i < parameters.stops.length; i++) {
hashedStops[parameters.stops[i][0]] = parameters.stops[i][1];
}
}

if (zoomAndFeatureDependent) {
const featureFunctions = {};
const featureFunctionStops = [];
Expand All @@ -86,34 +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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to pass propertySpec to evaluateExponentialFunction both as an argument and embedded within featureFunctionStops?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. I misread the code. This is 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is it embedded within featureFunctionStops?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oop, didn't see your followup comment. 👍

};
fun.isFeatureConstant = false;
fun.isZoomConstant = false;

} else if (zoomDependent) {
fun = function(zoom) {
if (innerFun === evaluateCategoricalFunction) {
return outputFunction(innerFun(parameters, zoom, hashedStops));
} else {
return outputFunction(innerFun(parameters, zoom));
}
return outputFunction(innerFun(parameters, propertySpec, zoom, hashedStops));
};
fun.isFeatureConstant = true;
fun.isZoomConstant = false;
} else {
fun = function(zoom, feature) {
if (innerFun === evaluateCategoricalFunction) {
return outputFunction(innerFun(parameters, feature[parameters.property], hashedStops));
} else {
return outputFunction(innerFun(parameters, feature[parameters.property]));
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;
Expand All @@ -123,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];

Expand All @@ -146,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];

Expand All @@ -168,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) {
Expand Down Expand Up @@ -200,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;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ref #4088

return interpolate(input, base, inputLower, inputUpper, evaluatedLower, evaluatedUpper);
};
} else if (outputLower.length) {
Expand Down Expand Up @@ -235,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');
};
5 changes: 5 additions & 0 deletions js/style-spec/reference/v8.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
25 changes: 25 additions & 0 deletions js/style-spec/util/parse_color.js
Original file line number Diff line number Diff line change
@@ -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;
}
};
14 changes: 13 additions & 1 deletion js/style-spec/validate/validate_function.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
});
}
};
44 changes: 0 additions & 44 deletions js/style/parse_color.js

This file was deleted.

Loading