UNPKG

@mapwhit/style-expressions

Version:
299 lines (267 loc) 10.2 kB
import Interpolate from '../expression/definitions/interpolate.js'; import { Formatted } from '../expression/types/formatted.js'; import Color from '../util/color.js'; import * as colorSpaces from '../util/color_spaces.js'; import getType from '../util/get_type.js'; import * as interpolate from '../util/interpolate.js'; import { supportsInterpolation } from '../util/properties.js'; export function isFunction(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); } function identityFunction(x) { return x; } export function createFunction(parameters, propertySpec) { const isColor = propertySpec.type === 'color'; const zoomAndFeatureDependent = parameters.stops && typeof parameters.stops[0][0] === 'object'; const featureDependent = zoomAndFeatureDependent || parameters.property !== undefined; const zoomDependent = zoomAndFeatureDependent || !featureDependent; const type = parameters.type || (supportsInterpolation(propertySpec) ? 'exponential' : 'interval'); if (isColor) { parameters = { ...parameters }; if (parameters.stops) { parameters.stops = parameters.stops.map(stop => { return [stop[0], Color.parse(stop[1])]; }); } if (parameters.default) { parameters.default = Color.parse(parameters.default); } else { parameters.default = Color.parse(propertySpec.default); } } if (parameters.colorSpace && parameters.colorSpace !== 'rgb' && !colorSpaces[parameters.colorSpace]) { throw new Error(`Unknown color space: ${parameters.colorSpace}`); } let innerFun; let hashedStops; let categoricalKeyType; 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]; } // Infer key type based on first stop key-- used to encforce strict type checking later categoricalKeyType = typeof parameters.stops[0][0]; } else if (type === 'identity') { innerFun = evaluateIdentityFunction; } else { throw new Error(`Unknown function type "${type}"`); } if (zoomAndFeatureDependent) { const featureFunctions = {}; const zoomStops = []; for (let s = 0; s < parameters.stops.length; s++) { const stop = parameters.stops[s]; const zoom = stop[0].zoom; if (featureFunctions[zoom] === undefined) { featureFunctions[zoom] = { zoom, type: parameters.type, property: parameters.property, default: parameters.default, stops: [] }; zoomStops.push(zoom); } featureFunctions[zoom].stops.push([stop[0].value, stop[1]]); } const featureFunctionStops = []; for (const z of zoomStops) { featureFunctionStops.push([featureFunctions[z].zoom, createFunction(featureFunctions[z], propertySpec)]); } return { kind: 'composite', interpolationFactor: Interpolate.interpolationFactor.bind(undefined, { name: 'linear' }), zoomStops: featureFunctionStops.map(s => s[0]), evaluate({ zoom }, properties) { return evaluateExponentialFunction( { stops: featureFunctionStops, base: parameters.base }, propertySpec, zoom ).evaluate(zoom, properties); } }; } if (zoomDependent) { return { kind: 'camera', interpolationFactor: type === 'exponential' ? Interpolate.interpolationFactor.bind(undefined, { name: 'exponential', base: parameters.base !== undefined ? parameters.base : 1 }) : () => 0, zoomStops: parameters.stops.map(s => s[0]), evaluate: ({ zoom }) => innerFun(parameters, propertySpec, zoom, hashedStops, categoricalKeyType) }; } return { kind: 'source', evaluate(_, feature) { const value = feature?.properties ? feature.properties[parameters.property] : undefined; if (value === undefined) { return coalesce(parameters.default, propertySpec.default); } return innerFun(parameters, propertySpec, value, hashedStops, categoricalKeyType); } }; } function coalesce(a, b, c) { if (a !== undefined) return a; if (b !== undefined) return b; if (c !== undefined) return c; } function evaluateCategoricalFunction(parameters, propertySpec, input, hashedStops, keyType) { const evaluated = typeof input === keyType ? hashedStops[input] : undefined; // Enforce strict typing on input return coalesce(evaluated, parameters.default, propertySpec.default); } 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 <= parameters.stops[0][0]) return parameters.stops[0][1]; if (input >= parameters.stops[n - 1][0]) return parameters.stops[n - 1][1]; const index = findStopLessThanOrEqualTo(parameters.stops, input); return parameters.stops[index][1]; } 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 <= parameters.stops[0][0]) return parameters.stops[0][1]; if (input >= parameters.stops[n - 1][0]) return parameters.stops[n - 1][1]; const index = findStopLessThanOrEqualTo(parameters.stops, input); const t = interpolationFactor(input, base, parameters.stops[index][0], parameters.stops[index + 1][0]); const outputLower = parameters.stops[index][1]; const outputUpper = parameters.stops[index + 1][1]; let interp = interpolate[propertySpec.type] || identityFunction; if (parameters.colorSpace && parameters.colorSpace !== 'rgb') { const colorspace = colorSpaces[parameters.colorSpace]; interp = (a, b) => colorspace.reverse(colorspace.interpolate(colorspace.forward(a), colorspace.forward(b), t)); } if (typeof outputLower.evaluate === 'function') { return { evaluate(...args) { const evaluatedLower = outputLower.evaluate.apply(undefined, args); const evaluatedUpper = outputUpper.evaluate.apply(undefined, args); // Special case for fill-outline-color, which has no spec default. if (evaluatedLower === undefined || evaluatedUpper === undefined) { return undefined; } return interp(evaluatedLower, evaluatedUpper, t); } }; } return interp(outputLower, outputUpper, t); } function evaluateIdentityFunction(parameters, propertySpec, input) { switch (propertySpec.type) { case 'color': input = Color.parse(input); break; case 'formatted': input = Formatted.fromString(input.toString()); break; case 'enum': if (!propertySpec.values.includes(input)) { input = undefined; } break; default: if (getType(input) !== propertySpec.type) { input = undefined; } } return coalesce(input, parameters.default, propertySpec.default); } /** * Returns the index of the last stop <= input, or 0 if it doesn't exist. * * @private */ function findStopLessThanOrEqualTo(stops, input) { const n = stops.length; let lowerIndex = 0; let upperIndex = n - 1; let currentIndex = 0; let currentValue; let upperValue; while (lowerIndex <= upperIndex) { currentIndex = Math.floor((lowerIndex + upperIndex) / 2); currentValue = stops[currentIndex][0]; upperValue = stops[currentIndex + 1][0]; if (input === currentValue || (input > currentValue && input < upperValue)) { // Search complete return currentIndex; } if (currentValue < input) { lowerIndex = currentIndex + 1; } else if (currentValue > input) { upperIndex = currentIndex - 1; } } return Math.max(currentIndex - 1, 0); } /** * Returns a ratio that can be used to interpolate between exponential function * stops. * * How it works: * Two consecutive stop values define a (scaled and shifted) exponential * function `f(x) = a * base^x + b`, where `base` is the user-specified base, * and `a` and `b` are constants affording sufficient degrees of freedom to fit * the function to the given stops. * * Here's a bit of algebra that lets us compute `f(x)` directly from the stop * values without explicitly solving for `a` and `b`: * * First stop value: `f(x0) = y0 = a * base^x0 + b` * Second stop value: `f(x1) = y1 = a * base^x1 + b` * => `y1 - y0 = a(base^x1 - base^x0)` * => `a = (y1 - y0)/(base^x1 - base^x0)` * * Desired value: `f(x) = y = a * base^x + b` * => `f(x) = y0 + a * (base^x - base^x0)` * * From the above, we can replace the `a` in `a * (base^x - base^x0)` and do a * little algebra: * ``` * a * (base^x - base^x0) = (y1 - y0)/(base^x1 - base^x0) * (base^x - base^x0) * = (y1 - y0) * (base^x - base^x0) / (base^x1 - base^x0) * ``` * * If we let `(base^x - base^x0) / (base^x1 base^x0)`, then we have * `f(x) = y0 + (y1 - y0) * ratio`. In other words, `ratio` may be treated as * an interpolation factor between the two stops' output values. * * (Note: a slightly different form for `ratio`, * `(base^(x-x0) - 1) / (base^(x1-x0) - 1) `, is equivalent, but requires fewer * expensive `Math.pow()` operations.) * * @private */ function interpolationFactor(input, base, lowerValue, upperValue) { const difference = upperValue - lowerValue; const progress = input - lowerValue; if (difference === 0) { return 0; } if (base === 1) { return progress / difference; } return (base ** progress - 1) / (base ** difference - 1); }