ol
Version:
OpenLayers mapping library
1,351 lines (1,199 loc) • 35.9 kB
JavaScript
/**
* @module ol/render/canvas/style
*/
import {NO_COLOR} from '../../color.js';
import {buildExpression, newEvaluationContext} from '../../expr/cpu.js';
import {
BooleanType,
ColorType,
NumberArrayType,
NumberType,
StringType,
computeGeometryType,
newParsingContext,
} from '../../expr/expression.js';
import {isEmpty} from '../../obj.js';
import {toSize} from '../../size.js';
import Circle from '../../style/Circle.js';
import Fill from '../../style/Fill.js';
import Icon from '../../style/Icon.js';
import RegularShape from '../../style/RegularShape.js';
import Stroke from '../../style/Stroke.js';
import Style from '../../style/Style.js';
import Text from '../../style/Text.js';
/**
* @fileoverview This module includes functions to build styles for the canvas renderer. Building
* is composed of two steps: parsing and compiling. The parsing step takes an encoded expression
* and returns an instance of one of the expression classes. The compiling step takes the
* expression instance and returns a function that can be evaluated to return a literal value. The
* evaluator function should do as little allocation and work as possible.
*/
/**
* @typedef {import("../../style/flat.js").FlatStyle} FlatStyle
*/
/**
* @typedef {import("../../expr/expression.js").EncodedExpression} EncodedExpression
*/
/**
* @typedef {import("../../expr/expression.js").ParsingContext} ParsingContext
*/
/**
* @typedef {import("../../expr/expression.js").CallExpression} CallExpression
*/
/**
* @typedef {import("../../expr/cpu.js").EvaluationContext} EvaluationContext
*/
/**
* @typedef {import("../../expr/cpu.js").ExpressionEvaluator} ExpressionEvaluator
*/
/**
* @param {EvaluationContext} context The evaluation context.
* @return {boolean} Always true.
*/
function always(context) {
return true;
}
/**
* This function adapts a rule evaluator to the existing style function interface.
* After we have deprecated the style function, we can use the compiled rules directly
* and pass a more complete evaluation context (variables, zoom, time, etc.).
*
* @param {Array<import('../../style/flat.js').Rule>} rules The rules.
* @return {import('../../style/Style.js').StyleFunction} A style function.
*/
export function rulesToStyleFunction(rules) {
const parsingContext = newParsingContext();
const evaluator = buildRuleSet(rules, parsingContext);
const evaluationContext = newEvaluationContext();
return function (feature, resolution) {
evaluationContext.properties = feature.getPropertiesInternal();
evaluationContext.resolution = resolution;
if (parsingContext.featureId) {
const id = feature.getId();
if (id !== undefined) {
evaluationContext.featureId = id;
} else {
evaluationContext.featureId = null;
}
}
if (parsingContext.geometryType) {
evaluationContext.geometryType = computeGeometryType(
feature.getGeometry(),
);
}
return evaluator(evaluationContext);
};
}
/**
* This function adapts a style evaluator to the existing style function interface.
* After we have deprecated the style function, we can use the compiled rules directly
* and pass a more complete evaluation context (variables, zoom, time, etc.).
*
* @param {Array<import('../../style/flat.js').FlatStyle>} flatStyles The flat styles.
* @return {import('../../style/Style.js').StyleFunction} A style function.
*/
export function flatStylesToStyleFunction(flatStyles) {
const parsingContext = newParsingContext();
const length = flatStyles.length;
/**
* @type {Array<StyleEvaluator>}
*/
const evaluators = new Array(length);
for (let i = 0; i < length; ++i) {
evaluators[i] = buildStyle(flatStyles[i], parsingContext);
}
const evaluationContext = newEvaluationContext();
/**
* @type {Array<Style>}
*/
const styles = new Array(length);
return function (feature, resolution) {
evaluationContext.properties = feature.getPropertiesInternal();
evaluationContext.resolution = resolution;
if (parsingContext.featureId) {
const id = feature.getId();
if (id !== undefined) {
evaluationContext.featureId = id;
} else {
evaluationContext.featureId = null;
}
}
let nonNullCount = 0;
for (let i = 0; i < length; ++i) {
const style = evaluators[i](evaluationContext);
if (style) {
styles[nonNullCount] = style;
nonNullCount += 1;
}
}
styles.length = nonNullCount;
return styles;
};
}
/**
* @typedef {function(EvaluationContext):Array<Style>} RuleSetEvaluator
*/
/**
* @typedef {Object} CompiledRule
* @property {ExpressionEvaluator} filter The compiled filter evaluator.
* @property {Array<StyleEvaluator>} styles The list of compiled style evaluators.
*/
/**
* @param {Array<import('../../style/flat.js').Rule>} rules The rules.
* @param {ParsingContext} context The parsing context.
* @return {RuleSetEvaluator} The evaluator function.
*/
export function buildRuleSet(rules, context) {
const length = rules.length;
/**
* @type {Array<CompiledRule>}
*/
const compiledRules = new Array(length);
for (let i = 0; i < length; ++i) {
const rule = rules[i];
const filter =
'filter' in rule
? buildExpression(rule.filter, BooleanType, context)
: always;
/**
* @type {Array<StyleEvaluator>}
*/
let styles;
if (Array.isArray(rule.style)) {
const styleLength = rule.style.length;
styles = new Array(styleLength);
for (let j = 0; j < styleLength; ++j) {
styles[j] = buildStyle(rule.style[j], context);
}
} else {
styles = [buildStyle(rule.style, context)];
}
compiledRules[i] = {filter, styles};
}
return function (context) {
/**
* @type {Array<Style>}
*/
const styles = [];
let someMatched = false;
for (let i = 0; i < length; ++i) {
const filterEvaluator = compiledRules[i].filter;
if (!filterEvaluator(context)) {
continue;
}
if (rules[i].else && someMatched) {
continue;
}
someMatched = true;
for (const styleEvaluator of compiledRules[i].styles) {
const style = styleEvaluator(context);
if (!style) {
continue;
}
styles.push(style);
}
}
return styles;
};
}
/**
* @typedef {function(EvaluationContext):Style|null} StyleEvaluator
*/
/**
* @param {FlatStyle} flatStyle A flat style literal.
* @param {ParsingContext} context The parsing context.
* @return {StyleEvaluator} A function that evaluates to a style. The style returned by
* this function will be reused between invocations.
*/
export function buildStyle(flatStyle, context) {
const evaluateFill = buildFill(flatStyle, '', context);
const evaluateStroke = buildStroke(flatStyle, '', context);
const evaluateText = buildText(flatStyle, context);
const evaluateImage = buildImage(flatStyle, context);
const evaluateZIndex = numberEvaluator(flatStyle, 'z-index', context);
if (
!evaluateFill &&
!evaluateStroke &&
!evaluateText &&
!evaluateImage &&
!isEmpty(flatStyle)
) {
// assume this is a user error
// would be nice to check the properties and suggest "did you mean..."
throw new Error(
'No fill, stroke, point, or text symbolizer properties in style: ' +
JSON.stringify(flatStyle),
);
}
const style = new Style();
return function (context) {
let empty = true;
if (evaluateFill) {
const fill = evaluateFill(context);
if (fill) {
empty = false;
}
style.setFill(fill);
}
if (evaluateStroke) {
const stroke = evaluateStroke(context);
if (stroke) {
empty = false;
}
style.setStroke(stroke);
}
if (evaluateText) {
const text = evaluateText(context);
if (text) {
empty = false;
}
style.setText(text);
}
if (evaluateImage) {
const image = evaluateImage(context);
if (image) {
empty = false;
}
style.setImage(image);
}
if (evaluateZIndex) {
style.setZIndex(evaluateZIndex(context));
}
if (empty) {
return null;
}
return style;
};
}
/**
* @typedef {function(EvaluationContext):Fill|null} FillEvaluator
*/
/**
* @param {FlatStyle} flatStyle The flat style.
* @param {string} prefix The property prefix.
* @param {ParsingContext} context The parsing context.
* @return {FillEvaluator?} A function that evaluates to a fill.
*/
function buildFill(flatStyle, prefix, context) {
let evaluateColor;
if (prefix + 'fill-pattern-src' in flatStyle) {
evaluateColor = patternEvaluator(flatStyle, prefix + 'fill-', context);
} else {
if (flatStyle[prefix + 'fill-color'] === 'none') {
// avoids hit detection
return (context) => null;
}
evaluateColor = colorLikeEvaluator(
flatStyle,
prefix + 'fill-color',
context,
);
}
if (!evaluateColor) {
return null;
}
const fill = new Fill();
return function (context) {
const color = evaluateColor(context);
if (color === NO_COLOR) {
return null;
}
fill.setColor(color);
return fill;
};
}
/**
* @typedef {function(EvaluationContext):Stroke|null} StrokeEvaluator
*/
/**
* @param {FlatStyle} flatStyle The flat style.
* @param {string} prefix The property prefix.
* @param {ParsingContext} context The parsing context.
* @return {StrokeEvaluator?} A function the evaluates to a stroke.
*/
function buildStroke(flatStyle, prefix, context) {
const evaluateWidth = numberEvaluator(
flatStyle,
prefix + 'stroke-width',
context,
);
const evaluateColor = colorLikeEvaluator(
flatStyle,
prefix + 'stroke-color',
context,
);
if (!evaluateWidth && !evaluateColor) {
return null;
}
const evaluateLineCap = stringEvaluator(
flatStyle,
prefix + 'stroke-line-cap',
context,
);
const evaluateLineJoin = stringEvaluator(
flatStyle,
prefix + 'stroke-line-join',
context,
);
const evaluateLineDash = numberArrayEvaluator(
flatStyle,
prefix + 'stroke-line-dash',
context,
);
const evaluateLineDashOffset = numberEvaluator(
flatStyle,
prefix + 'stroke-line-dash-offset',
context,
);
const evaluateMiterLimit = numberEvaluator(
flatStyle,
prefix + 'stroke-miter-limit',
context,
);
const stroke = new Stroke();
return function (context) {
if (evaluateColor) {
const color = evaluateColor(context);
if (color === NO_COLOR) {
return null;
}
stroke.setColor(color);
}
if (evaluateWidth) {
stroke.setWidth(evaluateWidth(context));
}
if (evaluateLineCap) {
const lineCap = evaluateLineCap(context);
if (lineCap !== 'butt' && lineCap !== 'round' && lineCap !== 'square') {
throw new Error('Expected butt, round, or square line cap');
}
stroke.setLineCap(lineCap);
}
if (evaluateLineJoin) {
const lineJoin = evaluateLineJoin(context);
if (
lineJoin !== 'bevel' &&
lineJoin !== 'round' &&
lineJoin !== 'miter'
) {
throw new Error('Expected bevel, round, or miter line join');
}
stroke.setLineJoin(lineJoin);
}
if (evaluateLineDash) {
stroke.setLineDash(evaluateLineDash(context));
}
if (evaluateLineDashOffset) {
stroke.setLineDashOffset(evaluateLineDashOffset(context));
}
if (evaluateMiterLimit) {
stroke.setMiterLimit(evaluateMiterLimit(context));
}
return stroke;
};
}
/**
* @typedef {function(EvaluationContext):Text} TextEvaluator
*/
/**
* @param {FlatStyle} flatStyle The flat style.
* @param {ParsingContext} context The parsing context.
* @return {TextEvaluator?} A function that evaluates to a text symbolizer.
*/
function buildText(flatStyle, context) {
const prefix = 'text-';
// Currently, an Array<string> may be used for rich text support. This doesn't
// work with our expression syntax where arrays of strings are interpreted as
// call expressions. To support rich text, we could add a 'strings' operator
// where all the following arguments would be string values.
const evaluateValue = stringEvaluator(flatStyle, prefix + 'value', context);
if (!evaluateValue) {
return null;
}
const evaluateFill = buildFill(flatStyle, prefix, context);
const evaluateBackgroundFill = buildFill(
flatStyle,
prefix + 'background-',
context,
);
const evaluateStroke = buildStroke(flatStyle, prefix, context);
const evaluateBackgroundStroke = buildStroke(
flatStyle,
prefix + 'background-',
context,
);
const evaluateFont = stringEvaluator(flatStyle, prefix + 'font', context);
const evaluateMaxAngle = numberEvaluator(
flatStyle,
prefix + 'max-angle',
context,
);
const evaluateOffsetX = numberEvaluator(
flatStyle,
prefix + 'offset-x',
context,
);
const evaluateOffsetY = numberEvaluator(
flatStyle,
prefix + 'offset-y',
context,
);
const evaluateOverflow = booleanEvaluator(
flatStyle,
prefix + 'overflow',
context,
);
const evaluatePlacement = stringEvaluator(
flatStyle,
prefix + 'placement',
context,
);
const evaluateRepeat = numberEvaluator(flatStyle, prefix + 'repeat', context);
const evaluateScale = sizeLikeEvaluator(flatStyle, prefix + 'scale', context);
const evaluateRotateWithView = booleanEvaluator(
flatStyle,
prefix + 'rotate-with-view',
context,
);
const evaluateRotation = numberEvaluator(
flatStyle,
prefix + 'rotation',
context,
);
const evaluateAlign = stringEvaluator(flatStyle, prefix + 'align', context);
const evaluateJustify = stringEvaluator(
flatStyle,
prefix + 'justify',
context,
);
const evaluateBaseline = stringEvaluator(
flatStyle,
prefix + 'baseline',
context,
);
const evaluateKeepUpright = booleanEvaluator(
flatStyle,
prefix + 'keep-upright',
context,
);
const evaluatePadding = numberArrayEvaluator(
flatStyle,
prefix + 'padding',
context,
);
// The following properties are not currently settable
const declutterMode = optionalDeclutterMode(
flatStyle,
prefix + 'declutter-mode',
);
const text = new Text({declutterMode});
return function (context) {
text.setText(evaluateValue(context));
if (evaluateFill) {
text.setFill(evaluateFill(context));
}
if (evaluateBackgroundFill) {
text.setBackgroundFill(evaluateBackgroundFill(context));
}
if (evaluateStroke) {
text.setStroke(evaluateStroke(context));
}
if (evaluateBackgroundStroke) {
text.setBackgroundStroke(evaluateBackgroundStroke(context));
}
if (evaluateFont) {
text.setFont(evaluateFont(context));
}
if (evaluateMaxAngle) {
text.setMaxAngle(evaluateMaxAngle(context));
}
if (evaluateOffsetX) {
text.setOffsetX(evaluateOffsetX(context));
}
if (evaluateOffsetY) {
text.setOffsetY(evaluateOffsetY(context));
}
if (evaluateOverflow) {
text.setOverflow(evaluateOverflow(context));
}
if (evaluatePlacement) {
const placement = evaluatePlacement(context);
if (placement !== 'point' && placement !== 'line') {
throw new Error('Expected point or line for text-placement');
}
text.setPlacement(placement);
}
if (evaluateRepeat) {
text.setRepeat(evaluateRepeat(context));
}
if (evaluateScale) {
text.setScale(evaluateScale(context));
}
if (evaluateRotateWithView) {
text.setRotateWithView(evaluateRotateWithView(context));
}
if (evaluateRotation) {
text.setRotation(evaluateRotation(context));
}
if (evaluateAlign) {
const textAlign = evaluateAlign(context);
if (
textAlign !== 'left' &&
textAlign !== 'center' &&
textAlign !== 'right' &&
textAlign !== 'end' &&
textAlign !== 'start'
) {
throw new Error(
'Expected left, right, center, start, or end for text-align',
);
}
text.setTextAlign(textAlign);
}
if (evaluateJustify) {
const justify = evaluateJustify(context);
if (justify !== 'left' && justify !== 'right' && justify !== 'center') {
throw new Error('Expected left, right, or center for text-justify');
}
text.setJustify(justify);
}
if (evaluateBaseline) {
const textBaseline = evaluateBaseline(context);
if (
textBaseline !== 'bottom' &&
textBaseline !== 'top' &&
textBaseline !== 'middle' &&
textBaseline !== 'alphabetic' &&
textBaseline !== 'hanging'
) {
throw new Error(
'Expected bottom, top, middle, alphabetic, or hanging for text-baseline',
);
}
text.setTextBaseline(textBaseline);
}
if (evaluatePadding) {
text.setPadding(evaluatePadding(context));
}
if (evaluateKeepUpright) {
text.setKeepUpright(evaluateKeepUpright(context));
}
return text;
};
}
/**
* @typedef {function(EvaluationContext):import("../../style/Image.js").default} ImageEvaluator
*/
/**
* @param {FlatStyle} flatStyle The flat style.
* @param {ParsingContext} context The parsing context.
* @return {ImageEvaluator?} A function that evaluates to an image symbolizer.
*/
function buildImage(flatStyle, context) {
if ('icon-src' in flatStyle) {
return buildIcon(flatStyle, context);
}
if ('shape-points' in flatStyle) {
return buildShape(flatStyle, context);
}
if ('circle-radius' in flatStyle) {
return buildCircle(flatStyle, context);
}
return null;
}
/**
* @param {FlatStyle} flatStyle The flat style.
* @param {ParsingContext} context The parsing context.
* @return {ImageEvaluator} A function that evaluates to an image symbolizer.
*/
function buildIcon(flatStyle, context) {
const prefix = 'icon-';
// required property
const srcName = prefix + 'src';
const src = requireString(flatStyle[srcName], srcName);
// settable properties
const evaluateAnchor = coordinateEvaluator(
flatStyle,
prefix + 'anchor',
context,
);
const evaluateScale = sizeLikeEvaluator(flatStyle, prefix + 'scale', context);
const evaluateOpacity = numberEvaluator(
flatStyle,
prefix + 'opacity',
context,
);
const evaluateDisplacement = coordinateEvaluator(
flatStyle,
prefix + 'displacement',
context,
);
const evaluateRotation = numberEvaluator(
flatStyle,
prefix + 'rotation',
context,
);
const evaluateRotateWithView = booleanEvaluator(
flatStyle,
prefix + 'rotate-with-view',
context,
);
// the remaining symbolizer properties are not currently settable
const anchorOrigin = optionalIconOrigin(flatStyle, prefix + 'anchor-origin');
const anchorXUnits = optionalIconAnchorUnits(
flatStyle,
prefix + 'anchor-x-units',
);
const anchorYUnits = optionalIconAnchorUnits(
flatStyle,
prefix + 'anchor-y-units',
);
const color = optionalColorLike(flatStyle, prefix + 'color');
const crossOrigin = optionalString(flatStyle, prefix + 'cross-origin');
const offset = optionalNumberArray(flatStyle, prefix + 'offset');
const offsetOrigin = optionalIconOrigin(flatStyle, prefix + 'offset-origin');
const width = optionalNumber(flatStyle, prefix + 'width');
const height = optionalNumber(flatStyle, prefix + 'height');
const size = optionalSize(flatStyle, prefix + 'size');
const declutterMode = optionalDeclutterMode(
flatStyle,
prefix + 'declutter-mode',
);
const icon = new Icon({
src,
anchorOrigin,
anchorXUnits,
anchorYUnits,
color,
crossOrigin,
offset,
offsetOrigin,
height,
width,
size,
declutterMode,
});
return function (context) {
if (evaluateOpacity) {
icon.setOpacity(evaluateOpacity(context));
}
if (evaluateDisplacement) {
icon.setDisplacement(evaluateDisplacement(context));
}
if (evaluateRotation) {
icon.setRotation(evaluateRotation(context));
}
if (evaluateRotateWithView) {
icon.setRotateWithView(evaluateRotateWithView(context));
}
if (evaluateScale) {
icon.setScale(evaluateScale(context));
}
if (evaluateAnchor) {
icon.setAnchor(evaluateAnchor(context));
}
return icon;
};
}
/**
* @param {FlatStyle} flatStyle The flat style.
* @param {ParsingContext} context The parsing context.
* @return {ImageEvaluator} A function that evaluates to an icon symbolizer.
*/
function buildShape(flatStyle, context) {
const prefix = 'shape-';
// required property
const pointsName = prefix + 'points';
const radiusName = prefix + 'radius';
const points = requireNumber(flatStyle[pointsName], pointsName);
const radius = requireNumber(flatStyle[radiusName], radiusName);
// settable properties
const evaluateFill = buildFill(flatStyle, prefix, context);
const evaluateStroke = buildStroke(flatStyle, prefix, context);
const evaluateScale = sizeLikeEvaluator(flatStyle, prefix + 'scale', context);
const evaluateDisplacement = coordinateEvaluator(
flatStyle,
prefix + 'displacement',
context,
);
const evaluateRotation = numberEvaluator(
flatStyle,
prefix + 'rotation',
context,
);
const evaluateRotateWithView = booleanEvaluator(
flatStyle,
prefix + 'rotate-with-view',
context,
);
// the remaining properties are not currently settable
const radius2 = optionalNumber(flatStyle, prefix + 'radius2');
const angle = optionalNumber(flatStyle, prefix + 'angle');
const declutterMode = optionalDeclutterMode(
flatStyle,
prefix + 'declutter-mode',
);
const shape = new RegularShape({
points,
radius,
radius2,
angle,
declutterMode,
});
return function (context) {
if (evaluateFill) {
shape.setFill(evaluateFill(context));
}
if (evaluateStroke) {
shape.setStroke(evaluateStroke(context));
}
if (evaluateDisplacement) {
shape.setDisplacement(evaluateDisplacement(context));
}
if (evaluateRotation) {
shape.setRotation(evaluateRotation(context));
}
if (evaluateRotateWithView) {
shape.setRotateWithView(evaluateRotateWithView(context));
}
if (evaluateScale) {
shape.setScale(evaluateScale(context));
}
return shape;
};
}
/**
* @param {FlatStyle} flatStyle The flat style.
* @param {ParsingContext} context The parsing context.
* @return {ImageEvaluator} A function that evaluates to a circle symbolizer.
*/
function buildCircle(flatStyle, context) {
const prefix = 'circle-';
// settable properties
const evaluateFill = buildFill(flatStyle, prefix, context);
const evaluateStroke = buildStroke(flatStyle, prefix, context);
const evaluateRadius = numberEvaluator(flatStyle, prefix + 'radius', context);
const evaluateScale = sizeLikeEvaluator(flatStyle, prefix + 'scale', context);
const evaluateDisplacement = coordinateEvaluator(
flatStyle,
prefix + 'displacement',
context,
);
const evaluateRotation = numberEvaluator(
flatStyle,
prefix + 'rotation',
context,
);
const evaluateRotateWithView = booleanEvaluator(
flatStyle,
prefix + 'rotate-with-view',
context,
);
// the remaining properties are not currently settable
const declutterMode = optionalDeclutterMode(
flatStyle,
prefix + 'declutter-mode',
);
const circle = new Circle({
radius: 5, // this is arbitrary, but required - the evaluated radius is used below
declutterMode,
});
return function (context) {
if (evaluateRadius) {
circle.setRadius(evaluateRadius(context));
}
if (evaluateFill) {
circle.setFill(evaluateFill(context));
}
if (evaluateStroke) {
circle.setStroke(evaluateStroke(context));
}
if (evaluateDisplacement) {
circle.setDisplacement(evaluateDisplacement(context));
}
if (evaluateRotation) {
circle.setRotation(evaluateRotation(context));
}
if (evaluateRotateWithView) {
circle.setRotateWithView(evaluateRotateWithView(context));
}
if (evaluateScale) {
circle.setScale(evaluateScale(context));
}
return circle;
};
}
/**
* @param {FlatStyle} flatStyle The flat style.
* @param {string} name The property name.
* @param {ParsingContext} context The parsing context.
* @return {import('../../expr/cpu.js').NumberEvaluator|undefined} The expression evaluator or undefined.
*/
function numberEvaluator(flatStyle, name, context) {
if (!(name in flatStyle)) {
return undefined;
}
const evaluator = buildExpression(flatStyle[name], NumberType, context);
return function (context) {
return requireNumber(evaluator(context), name);
};
}
/**
* @param {FlatStyle} flatStyle The flat style.
* @param {string} name The property name.
* @param {ParsingContext} context The parsing context.
* @return {import('../../expr/cpu.js').StringEvaluator?} The expression evaluator.
*/
function stringEvaluator(flatStyle, name, context) {
if (!(name in flatStyle)) {
return null;
}
const evaluator = buildExpression(flatStyle[name], StringType, context);
return function (context) {
return requireString(evaluator(context), name);
};
}
function patternEvaluator(flatStyle, prefix, context) {
const srcEvaluator = stringEvaluator(
flatStyle,
prefix + 'pattern-src',
context,
);
const offsetEvaluator = sizeEvaluator(
flatStyle,
prefix + 'pattern-offset',
context,
);
const patternSizeEvaluator = sizeEvaluator(
flatStyle,
prefix + 'pattern-size',
context,
);
const colorEvaluator = colorLikeEvaluator(
flatStyle,
prefix + 'color',
context,
);
return function (context) {
return {
src: srcEvaluator(context),
offset: offsetEvaluator && offsetEvaluator(context),
size: patternSizeEvaluator && patternSizeEvaluator(context),
color: colorEvaluator && colorEvaluator(context),
};
};
}
/**
* @param {FlatStyle} flatStyle The flat style.
* @param {string} name The property name.
* @param {ParsingContext} context The parsing context.
* @return {import('../../expr/cpu.js').BooleanEvaluator?} The expression evaluator.
*/
function booleanEvaluator(flatStyle, name, context) {
if (!(name in flatStyle)) {
return null;
}
const evaluator = buildExpression(flatStyle[name], BooleanType, context);
return function (context) {
const value = evaluator(context);
if (typeof value !== 'boolean') {
throw new Error(`Expected a boolean for ${name}`);
}
return value;
};
}
/**
* @param {FlatStyle} flatStyle The flat style.
* @param {string} name The property name.
* @param {ParsingContext} context The parsing context.
* @return {import('../../expr/cpu.js').ColorLikeEvaluator?} The expression evaluator.
*/
function colorLikeEvaluator(flatStyle, name, context) {
if (!(name in flatStyle)) {
return null;
}
const evaluator = buildExpression(flatStyle[name], ColorType, context);
return function (context) {
return requireColorLike(evaluator(context), name);
};
}
/**
* @param {FlatStyle} flatStyle The flat style.
* @param {string} name The property name.
* @param {ParsingContext} context The parsing context.
* @return {import('../../expr/cpu.js').NumberArrayEvaluator?} The expression evaluator.
*/
function numberArrayEvaluator(flatStyle, name, context) {
if (!(name in flatStyle)) {
return null;
}
const evaluator = buildExpression(flatStyle[name], NumberArrayType, context);
return function (context) {
return requireNumberArray(evaluator(context), name);
};
}
/**
* @param {FlatStyle} flatStyle The flat style.
* @param {string} name The property name.
* @param {ParsingContext} context The parsing context.
* @return {import('../../expr/cpu.js').CoordinateEvaluator?} The expression evaluator.
*/
function coordinateEvaluator(flatStyle, name, context) {
if (!(name in flatStyle)) {
return null;
}
const evaluator = buildExpression(flatStyle[name], NumberArrayType, context);
return function (context) {
const array = requireNumberArray(evaluator(context), name);
if (array.length !== 2) {
throw new Error(`Expected two numbers for ${name}`);
}
return array;
};
}
/**
* @param {FlatStyle} flatStyle The flat style.
* @param {string} name The property name.
* @param {ParsingContext} context The parsing context.
* @return {import('../../expr/cpu.js').SizeEvaluator?} The expression evaluator.
*/
function sizeEvaluator(flatStyle, name, context) {
if (!(name in flatStyle)) {
return null;
}
const evaluator = buildExpression(flatStyle[name], NumberArrayType, context);
return function (context) {
return requireSize(evaluator(context), name);
};
}
/**
* @param {FlatStyle} flatStyle The flat style.
* @param {string} name The property name.
* @param {ParsingContext} context The parsing context.
* @return {import('../../expr/cpu.js').SizeLikeEvaluator?} The expression evaluator.
*/
function sizeLikeEvaluator(flatStyle, name, context) {
if (!(name in flatStyle)) {
return null;
}
const evaluator = buildExpression(
flatStyle[name],
NumberArrayType | NumberType,
context,
);
return function (context) {
return requireSizeLike(evaluator(context), name);
};
}
/**
* @param {FlatStyle} flatStyle The flat style.
* @param {string} property The symbolizer property.
* @return {number|undefined} A number or undefined.
*/
function optionalNumber(flatStyle, property) {
const value = flatStyle[property];
if (value === undefined) {
return undefined;
}
if (typeof value !== 'number') {
throw new Error(`Expected a number for ${property}`);
}
return value;
}
/**
* @param {FlatStyle} flatStyle The flat style.
* @param {string} property The symbolizer property.
* @return {import("../../size.js").Size|undefined} A size or undefined.
*/
function optionalSize(flatStyle, property) {
const encoded = flatStyle[property];
if (encoded === undefined) {
return undefined;
}
if (typeof encoded === 'number') {
return toSize(encoded);
}
if (!Array.isArray(encoded)) {
throw new Error(`Expected a number or size array for ${property}`);
}
if (
encoded.length !== 2 ||
typeof encoded[0] !== 'number' ||
typeof encoded[1] !== 'number'
) {
throw new Error(`Expected a number or size array for ${property}`);
}
return encoded;
}
/**
* @param {FlatStyle} flatStyle The flat style.
* @param {string} property The symbolizer property.
* @return {string|undefined} A string or undefined.
*/
function optionalString(flatStyle, property) {
const encoded = flatStyle[property];
if (encoded === undefined) {
return undefined;
}
if (typeof encoded !== 'string') {
throw new Error(`Expected a string for ${property}`);
}
return encoded;
}
/**
* @param {FlatStyle} flatStyle The flat style.
* @param {string} property The symbolizer property.
* @return {import("../../style/Icon.js").IconOrigin|undefined} An icon origin or undefined.
*/
function optionalIconOrigin(flatStyle, property) {
const encoded = flatStyle[property];
if (encoded === undefined) {
return undefined;
}
if (
encoded !== 'bottom-left' &&
encoded !== 'bottom-right' &&
encoded !== 'top-left' &&
encoded !== 'top-right'
) {
throw new Error(
`Expected bottom-left, bottom-right, top-left, or top-right for ${property}`,
);
}
return encoded;
}
/**
* @param {FlatStyle} flatStyle The flat style.
* @param {string} property The symbolizer property.
* @return {import("../../style/Icon.js").IconAnchorUnits|undefined} Icon anchor units or undefined.
*/
function optionalIconAnchorUnits(flatStyle, property) {
const encoded = flatStyle[property];
if (encoded === undefined) {
return undefined;
}
if (encoded !== 'pixels' && encoded !== 'fraction') {
throw new Error(`Expected pixels or fraction for ${property}`);
}
return encoded;
}
/**
* @param {FlatStyle} flatStyle The flat style.
* @param {string} property The symbolizer property.
* @return {Array<number>|undefined} An array of numbers or undefined.
*/
function optionalNumberArray(flatStyle, property) {
const encoded = flatStyle[property];
if (encoded === undefined) {
return undefined;
}
return requireNumberArray(encoded, property);
}
/**
* @param {FlatStyle} flatStyle The flat style.
* @param {string} property The symbolizer property.
* @return {import('../../style/Style.js').DeclutterMode} Icon declutter mode.
*/
function optionalDeclutterMode(flatStyle, property) {
const encoded = flatStyle[property];
if (encoded === undefined) {
return undefined;
}
if (typeof encoded !== 'string') {
throw new Error(`Expected a string for ${property}`);
}
if (encoded !== 'declutter' && encoded !== 'obstacle' && encoded !== 'none') {
throw new Error(`Expected declutter, obstacle, or none for ${property}`);
}
return encoded;
}
/**
* @param {FlatStyle} flatStyle The flat style.
* @param {string} property The symbolizer property.
* @return {string|Array<number>|undefined} A string or an array of color values or undefined.
*/
function optionalColorLike(flatStyle, property) {
const encoded = flatStyle[property];
if (encoded === undefined) {
return undefined;
}
return requireColorLike(encoded, property);
}
/**
* @param {any} value The value.
* @param {string} property The property.
* @return {Array<number>} An array of numbers.
*/
function requireNumberArray(value, property) {
if (!Array.isArray(value)) {
throw new Error(`Expected an array for ${property}`);
}
const length = value.length;
for (let i = 0; i < length; ++i) {
if (typeof value[i] !== 'number') {
throw new Error(`Expected an array of numbers for ${property}`);
}
}
return value;
}
/**
* @param {any} value The value.
* @param {string} property The property.
* @return {string} A string.
*/
function requireString(value, property) {
if (typeof value !== 'string') {
throw new Error(`Expected a string for ${property}`);
}
return value;
}
/**
* @param {any} value The value.
* @param {string} property The property.
* @return {number} A number.
*/
function requireNumber(value, property) {
if (typeof value !== 'number') {
throw new Error(`Expected a number for ${property}`);
}
return value;
}
/**
* @param {any} value The value.
* @param {string} property The property.
* @return {Array<number>|string} A color.
*/
function requireColorLike(value, property) {
if (typeof value === 'string') {
return value;
}
const array = requireNumberArray(value, property);
const length = array.length;
if (length < 3 || length > 4) {
throw new Error(`Expected a color with 3 or 4 values for ${property}`);
}
return array;
}
/**
* @param {any} value The value.
* @param {string} property The property.
* @return {Array<number>} A number or an array of two numbers.
*/
function requireSize(value, property) {
const size = requireNumberArray(value, property);
if (size.length !== 2) {
throw new Error(`Expected an array of two numbers for ${property}`);
}
return size;
}
/**
* @param {any} value The value.
* @param {string} property The property.
* @return {number|Array<number>} A number or an array of two numbers.
*/
function requireSizeLike(value, property) {
if (typeof value === 'number') {
return value;
}
return requireSize(value, property);
}