// @flow import assert from 'assert'; import type {StylePropertySpecification} from '../style-spec'; export default convertFunction; function convertLiteral(value) { return typeof value === 'object' ? ['literal', value] : value; } function convertFunction(parameters: any, propertySpec: StylePropertySpecification) { let stops = parameters.stops; if (!stops) { // identity function return convertIdentityFunction(parameters, propertySpec); } const zoomAndFeatureDependent = stops && typeof stops[0][0] === 'object'; const featureDependent = zoomAndFeatureDependent || parameters.property !== undefined; const zoomDependent = zoomAndFeatureDependent || !featureDependent; stops = stops.map((stop) => { if (!featureDependent && propertySpec.tokens && typeof stop[1] === 'string') { return [stop[0], convertTokenString(stop[1])]; } return [stop[0], convertLiteral(stop[1])]; }); if (zoomAndFeatureDependent) { return convertZoomAndPropertyFunction(parameters, propertySpec, stops); } else if (zoomDependent) { return convertZoomFunction(parameters, propertySpec, stops); } else { return convertPropertyFunction(parameters, propertySpec, stops); } } function convertIdentityFunction(parameters, propertySpec): Array { const get = ['get', parameters.property]; if (parameters.default === undefined) { // By default, expressions for string-valued properties get coerced. To preserve // legacy function semantics, insert an explicit assertion instead. return propertySpec.type === 'string' ? ['string', get] : get; } else if (propertySpec.type === 'enum') { return [ 'match', get, Object.keys(propertySpec.values), get, parameters.default ]; } else { const expression = [propertySpec.type === 'color' ? 'to-color' : propertySpec.type, get, convertLiteral(parameters.default)]; if (propertySpec.type === 'array') { expression.splice(1, 0, propertySpec.value, propertySpec.length || null); } return expression; } } function getInterpolateOperator(parameters) { switch (parameters.colorSpace) { case 'hcl': return 'interpolate-hcl'; case 'lab': return 'interpolate-lab'; default: return 'interpolate'; } } function convertZoomAndPropertyFunction(parameters, propertySpec, stops) { const featureFunctionParameters = {}; const featureFunctionStops = {}; const zoomStops = []; for (let s = 0; s < stops.length; s++) { const stop = stops[s]; const zoom = stop[0].zoom; if (featureFunctionParameters[zoom] === undefined) { featureFunctionParameters[zoom] = { zoom, type: parameters.type, property: parameters.property, default: parameters.default, }; featureFunctionStops[zoom] = []; zoomStops.push(zoom); } featureFunctionStops[zoom].push([stop[0].value, stop[1]]); } // the interpolation type for the zoom dimension of a zoom-and-property // function is determined directly from the style property specification // for which it's being used: linear for interpolatable properties, step // otherwise. const functionType = getFunctionType({}, propertySpec); if (functionType === 'exponential') { const expression = [getInterpolateOperator(parameters), ['linear'], ['zoom']]; for (const z of zoomStops) { const output = convertPropertyFunction(featureFunctionParameters[z], propertySpec, featureFunctionStops[z]); appendStopPair(expression, z, output, false); } return expression; } else { const expression = ['step', ['zoom']]; for (const z of zoomStops) { const output = convertPropertyFunction(featureFunctionParameters[z], propertySpec, featureFunctionStops[z]); appendStopPair(expression, z, output, true); } fixupDegenerateStepCurve(expression); return expression; } } function coalesce(a, b) { if (a !== undefined) return a; if (b !== undefined) return b; } function getFallback(parameters, propertySpec) { const defaultValue = convertLiteral(coalesce(parameters.default, propertySpec.default)); /* * Some fields with type: resolvedImage have an undefined default. * Because undefined is an invalid value for resolvedImage, set fallback to * an empty string instead of undefined to ensure output * passes validation. */ if (defaultValue === undefined && propertySpec.type === 'resolvedImage') { return ''; } return defaultValue; } function convertPropertyFunction(parameters, propertySpec, stops) { const type = getFunctionType(parameters, propertySpec); const get = ['get', parameters.property]; if (type === 'categorical' && typeof stops[0][0] === 'boolean') { assert(parameters.stops.length > 0 && parameters.stops.length <= 2); const expression = ['case']; for (const stop of stops) { expression.push(['==', get, stop[0]], stop[1]); } expression.push(getFallback(parameters, propertySpec)); return expression; } else if (type === 'categorical') { const expression = ['match', get]; for (const stop of stops) { appendStopPair(expression, stop[0], stop[1], false); } expression.push(getFallback(parameters, propertySpec)); return expression; } else if (type === 'interval') { const expression = ['step', ['number', get]]; for (const stop of stops) { appendStopPair(expression, stop[0], stop[1], true); } fixupDegenerateStepCurve(expression); return parameters.default === undefined ? expression : [ 'case', ['==', ['typeof', get], 'number'], expression, convertLiteral(parameters.default) ]; } else if (type === 'exponential') { const base = parameters.base !== undefined ? parameters.base : 1; const expression = [ getInterpolateOperator(parameters), base === 1 ? ["linear"] : ["exponential", base], ["number", get] ]; for (const stop of stops) { appendStopPair(expression, stop[0], stop[1], false); } return parameters.default === undefined ? expression : [ 'case', ['==', ['typeof', get], 'number'], expression, convertLiteral(parameters.default) ]; } else { throw new Error(`Unknown property function type ${type}`); } } function convertZoomFunction(parameters, propertySpec, stops, input = ['zoom']) { const type = getFunctionType(parameters, propertySpec); let expression; let isStep = false; if (type === 'interval') { expression = ['step', input]; isStep = true; } else if (type === 'exponential') { const base = parameters.base !== undefined ? parameters.base : 1; expression = [getInterpolateOperator(parameters), base === 1 ? ["linear"] : ["exponential", base], input]; } else { throw new Error(`Unknown zoom function type "${type}"`); } for (const stop of stops) { appendStopPair(expression, stop[0], stop[1], isStep); } fixupDegenerateStepCurve(expression); return expression; } function fixupDegenerateStepCurve(expression) { // degenerate step curve (i.e. a constant function): add a noop stop if (expression[0] === 'step' && expression.length === 3) { expression.push(0); expression.push(expression[3]); } } 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); } curve.push(output); } function getFunctionType(parameters, propertySpec) { if (parameters.type) { return parameters.type; } else { assert(propertySpec.expression); return (propertySpec.expression: any).interpolated ? 'exponential' : 'interval'; } } // "String with {name} token" => ["concat", "String with ", ["get", "name"], " token"] export function convertTokenString(s: string) { const result = ['concat']; const re = /{([^{}]+)}/g; let pos = 0; for (let match = re.exec(s); match !== null; match = re.exec(s)) { const literal = s.slice(pos, re.lastIndex - match[0].length); pos = re.lastIndex; if (literal.length > 0) result.push(literal); result.push(['get', match[1]]); } if (result.length === 1) { return s; } if (pos < s.length) { result.push(s.slice(pos)); } else if (result.length === 2) { return ['to-string', result[1]]; } return result; }