Files
coopgo/node_modules/ol-mapbox-style/src/stylefunction.js
sgauthier 6e64e138e2
All checks were successful
Publish To Prod / deploy_and_publish (push) Successful in 35s
planning
2024-10-14 09:15:30 +02:00

1407 lines
43 KiB
JavaScript

/*
ol-mapbox-style - Use Mapbox Style objects with OpenLayers
Copyright 2016-present ol-mapbox-style contributors
License: https://raw.githubusercontent.com/openlayers/ol-mapbox-style/master/LICENSE
*/
import Circle from 'ol/style/Circle.js';
import Fill from 'ol/style/Fill.js';
import Icon from 'ol/style/Icon.js';
import RenderFeature from 'ol/render/Feature.js';
import Stroke from 'ol/style/Stroke.js';
import Style from 'ol/style/Style.js';
import Text from 'ol/style/Text.js';
import Color from '@mapbox/mapbox-gl-style-spec/util/color.js';
import convertFunction from '@mapbox/mapbox-gl-style-spec/function/convert.js';
import createFilter from '@mapbox/mapbox-gl-style-spec/feature_filter/index.js';
import derefLayers from '@mapbox/mapbox-gl-style-spec/deref.js';
import mb2css from 'mapbox-to-css-font';
import spec from '@mapbox/mapbox-gl-style-spec/reference/v8.json';
import {applyLetterSpacing, wrapText} from './text.js';
import {
createCanvas,
defaultResolutions,
deg2rad,
getZoomForResolution,
} from './util.js';
import {
createPropertyExpression,
isExpression,
} from '@mapbox/mapbox-gl-style-spec/expression/index.js';
import {isFunction} from '@mapbox/mapbox-gl-style-spec/function/index.js';
/**
* @typedef {import("ol/layer/Vector").default} VectorLayer
* @typedef {import("ol/layer/VectorTile").default} VectorTileLayer
* @typedef {import("ol/style/Style").StyleFunction} StyleFunction
*/
const types = {
'Point': 1,
'MultiPoint': 1,
'LineString': 2,
'MultiLineString': 2,
'Polygon': 3,
'MultiPolygon': 3,
};
const anchor = {
'center': [0.5, 0.5],
'left': [0, 0.5],
'right': [1, 0.5],
'top': [0.5, 0],
'bottom': [0.5, 1],
'top-left': [0, 0],
'top-right': [1, 0],
'bottom-left': [0, 1],
'bottom-right': [1, 1],
};
const expressionData = function (rawExpression, propertySpec) {
const compiledExpression = createPropertyExpression(
rawExpression,
propertySpec
);
if (compiledExpression.result === 'error') {
throw new Error(
compiledExpression.value
.map((err) => `${err.key}: ${err.message}`)
.join(', ')
);
}
return compiledExpression.value;
};
const emptyObj = {};
const zoomObj = {zoom: 0};
let renderFeatureCoordinates, renderFeature;
/**
* @private
* @param {Object} layer Gl object layer.
* @param {string} layoutOrPaint 'layout' or 'paint'.
* @param {string} property Feature property.
* @param {number} zoom Zoom.
* @param {Object} feature Gl feature.
* @param {Object} [functionCache] Function cache.
* @param {Object} [featureState] Feature state.
* @return {?} Value.
*/
export function getValue(
layer,
layoutOrPaint,
property,
zoom,
feature,
functionCache,
featureState
) {
const layerId = layer.id;
if (!functionCache) {
functionCache = {};
console.warn('No functionCache provided to getValue()'); //eslint-disable-line no-console
}
if (!functionCache[layerId]) {
functionCache[layerId] = {};
}
const functions = functionCache[layerId];
if (!functions[property]) {
let value = (layer[layoutOrPaint] || emptyObj)[property];
const propertySpec = spec[`${layoutOrPaint}_${layer.type}`][property];
if (value === undefined) {
value = propertySpec.default;
}
let isExpr = isExpression(value);
if (!isExpr && isFunction(value)) {
value = convertFunction(value, propertySpec);
isExpr = true;
}
if (isExpr) {
const compiledExpression = expressionData(value, propertySpec);
functions[property] =
compiledExpression.evaluate.bind(compiledExpression);
} else {
if (propertySpec.type == 'color') {
value = Color.parse(value);
}
functions[property] = function () {
return value;
};
}
}
zoomObj.zoom = zoom;
return functions[property](zoomObj, feature, featureState);
}
/**
* @private
* @param {Object} layer Gl object layer.
* @param {number} zoom Zoom.
* @param {Object} feature Gl feature.
* @param {Object} [functionCache] Function cache.
* @return {"declutter"|"obstacle"|"none"} Value.
*/
function getIconDeclutterMode(layer, zoom, feature, functionCache) {
const allowOverlap = getValue(
layer,
'layout',
'icon-allow-overlap',
zoom,
feature,
functionCache
);
if (!allowOverlap) {
return 'declutter';
}
const ignorePlacement = getValue(
layer,
'layout',
'icon-ignore-placement',
zoom,
feature,
functionCache
);
if (!ignorePlacement) {
return 'obstacle';
}
return 'none';
}
/**
* @private
* @param {string} layerId Layer id.
* @param {?} filter Filter.
* @param {Object} feature Feature.
* @param {number} zoom Zoom.
* @param {Object} [filterCache] Filter cache.
* @return {boolean} Filter result.
*/
function evaluateFilter(layerId, filter, feature, zoom, filterCache) {
if (!filterCache) {
console.warn('No filterCache provided to evaluateFilter()'); //eslint-disable-line no-console
}
if (!(layerId in filterCache)) {
filterCache[layerId] = createFilter(filter).filter;
}
zoomObj.zoom = zoom;
return filterCache[layerId](zoomObj, feature);
}
let renderTransparentEnabled = false;
/**
* Configure whether features with a transparent style should be rendered. When
* set to `true`, it will be possible to hit detect content that is not visible,
* like transparent fills of polygons, using `ol/layer/Layer#getFeatures()` or
* `ol/Map#getFeaturesAtPixel()`
* @param {boolean} enabled Rendering of transparent elements is enabled.
* Default is `false`.
*/
export function renderTransparent(enabled) {
renderTransparentEnabled = enabled;
}
/**
* @private
* @param {?} color Color.
* @param {number} [opacity] Opacity.
* @return {string} Color.
*/
function colorWithOpacity(color, opacity) {
if (color) {
if (!renderTransparentEnabled && (color.a === 0 || opacity === 0)) {
return undefined;
}
const a = color.a;
opacity = opacity === undefined ? 1 : opacity;
return a === 0
? 'transparent'
: 'rgba(' +
Math.round((color.r * 255) / a) +
',' +
Math.round((color.g * 255) / a) +
',' +
Math.round((color.b * 255) / a) +
',' +
a * opacity +
')';
}
return color;
}
const templateRegEx = /\{[^{}}]*\}/g;
/**
* @private
* @param {string} text Text.
* @param {Object} properties Properties.
* @return {string} Text.
*/
function fromTemplate(text, properties) {
return text.replace(templateRegEx, function (match) {
return properties[match.slice(1, -1)] || '';
});
}
let recordLayer = false;
/**
* Turns recording of the Mapbox Style's `layer` on and off. When turned on,
* the layer that a rendered feature belongs to will be set as the feature's
* `mapbox-layer` property.
* @param {boolean} record Recording of the style layer is on.
*/
export function recordStyleLayer(record = false) {
recordLayer = record;
}
/**
* Creates a style function from the `glStyle` object for all layers that use
* the specified `source`, which needs to be a `"type": "vector"` or
* `"type": "geojson"` source and applies it to the specified OpenLayers layer.
*
* Two additional properties will be set on the provided layer:
*
* * `mapbox-source`: The `id` of the Mapbox Style document's source that the
* OpenLayers layer was created from. Usually `apply()` creates one
* OpenLayers layer per Mapbox Style source, unless the layer stack has
* layers from different sources in between.
* * `mapbox-layers`: The `id`s of the Mapbox Style document's layers that are
* included in the OpenLayers layer.
*
* This function also works in a web worker. In worker mode, the main thread needs
* to listen to messages from the worker and respond with another message to make
* sure that sprite image loading works:
*
* ```js
* worker.addEventListener('message', event => {
* if (event.data.action === 'loadImage') {
* const image = new Image();
* image.crossOrigin = 'anonymous';
* image.addEventListener('load', function() {
* createImageBitmap(image, 0, 0, image.width, image.height).then(imageBitmap => {
* worker.postMessage({
* action: 'imageLoaded',
* image: imageBitmap,
* src: event.data.src
* }, [imageBitmap]);
* });
* });
* image.src = event.data.src;
* }
* });
* ```
*
* @param {VectorLayer|VectorTileLayer} olLayer OpenLayers layer to
* apply the style to. In addition to the style, the layer will get two
* properties: `mapbox-source` will be the `id` of the `glStyle`'s source used
* for the layer, and `mapbox-layers` will be an array of the `id`s of the
* `glStyle`'s layers.
* @param {string|Object} glStyle Mapbox Style object.
* @param {string|Array<string>} sourceOrLayers `source` key or an array of layer `id`s
* from the Mapbox Style object. When a `source` key is provided, all layers for
* the specified source will be included in the style function. When layer `id`s
* are provided, they must be from layers that use the same source.
* @param {Array<number>} resolutions
* Resolutions for mapping resolution to zoom level.
* @param {Object} spriteData Sprite data from the url specified in
* the Mapbox Style object's `sprite` property. Only required if a `sprite`
* property is specified in the Mapbox Style object.
* @param {string} spriteImageUrl Sprite image url for the sprite
* specified in the Mapbox Style object's `sprite` property. Only required if a
* `sprite` property is specified in the Mapbox Style object.
* @param {function(Array<string>):Array<string>} getFonts Function that
* receives a font stack as arguments, and returns a (modified) font stack that
* is available. Font names are the names used in the Mapbox Style object. If
* not provided, the font stack will be used as-is. This function can also be
* used for loading web fonts.
* @return {StyleFunction} Style function for use in
* `ol.layer.Vector` or `ol.layer.VectorTile`.
*/
export function stylefunction(
olLayer,
glStyle,
sourceOrLayers,
resolutions = defaultResolutions,
spriteData = undefined,
spriteImageUrl = undefined,
getFonts = undefined
) {
if (typeof glStyle == 'string') {
glStyle = JSON.parse(glStyle);
}
if (glStyle.version != 8) {
throw new Error('glStyle version 8 required.');
}
let spriteImage, spriteImgSize;
if (spriteImageUrl) {
if (typeof Image !== 'undefined') {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function () {
spriteImage = img;
spriteImgSize = [img.width, img.height];
olLayer.changed();
img.onload = null;
};
img.src = spriteImageUrl;
} else if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) { //eslint-disable-line
const worker = /** @type {*} */ (self);
// Main thread needs to handle 'loadImage' and dispatch 'imageLoaded'
worker.postMessage({
action: 'loadImage',
src: spriteImageUrl,
});
worker.addEventListener('message', function handler(event) {
if (
event.data.action === 'imageLoaded' &&
event.data.src === spriteImageUrl
) {
spriteImage = event.data.image;
spriteImgSize = [spriteImage.width, spriteImage.height];
}
});
}
}
const allLayers = derefLayers(glStyle.layers);
const layersBySourceLayer = {};
const mapboxLayers = [];
const iconImageCache = {};
const patternCache = {};
const functionCache = {};
const filterCache = {};
let mapboxSource;
for (let i = 0, ii = allLayers.length; i < ii; ++i) {
const layer = allLayers[i];
const layerId = layer.id;
if (
(typeof sourceOrLayers == 'string' && layer.source == sourceOrLayers) ||
sourceOrLayers.indexOf(layerId) !== -1
) {
const sourceLayer = layer['source-layer'];
if (!mapboxSource) {
mapboxSource = layer.source;
const source = glStyle.sources[mapboxSource];
if (!source) {
throw new Error(`Source "${mapboxSource}" is not defined`);
}
const type = source.type;
if (type !== 'vector' && type !== 'geojson') {
throw new Error(
`Source "${mapboxSource}" is not of type "vector" or "geojson", but "${type}"`
);
}
} else if (layer.source !== mapboxSource) {
throw new Error(
`Layer "${layerId}" does not use source "${mapboxSource}`
);
}
let layers = layersBySourceLayer[sourceLayer];
if (!layers) {
layers = [];
layersBySourceLayer[sourceLayer] = layers;
}
layers.push({
layer: layer,
index: i,
});
mapboxLayers.push(layerId);
}
}
const textHalo = new Stroke();
const textColor = new Fill();
const styles = [];
const styleFunction = function (feature, resolution) {
const properties = feature.getProperties();
const layers = layersBySourceLayer[properties.layer];
if (!layers) {
return;
}
let zoom = resolutions.indexOf(resolution);
if (zoom == -1) {
zoom = getZoomForResolution(resolution, resolutions);
}
const type = types[feature.getGeometry().getType()];
const f = {
properties: properties,
type: type,
};
const featureState = olLayer.get('mapbox-featurestate')[feature.getId()];
let stylesLength = -1;
let featureBelongsToLayer;
for (let i = 0, ii = layers.length; i < ii; ++i) {
const layerData = layers[i];
const layer = layerData.layer;
const layerId = layer.id;
const layout = layer.layout || emptyObj;
const paint = layer.paint || emptyObj;
if (
layout.visibility === 'none' ||
('minzoom' in layer && zoom < layer.minzoom) ||
('maxzoom' in layer && zoom >= layer.maxzoom)
) {
continue;
}
const filter = layer.filter;
if (!filter || evaluateFilter(layerId, filter, f, zoom, filterCache)) {
featureBelongsToLayer = layer;
let color, opacity, fill, stroke, strokeColor, style;
const index = layerData.index;
if (
type == 3 &&
(layer.type == 'fill' || layer.type == 'fill-extrusion')
) {
opacity = getValue(
layer,
'paint',
layer.type + '-opacity',
zoom,
f,
functionCache,
featureState
);
if (layer.type + '-pattern' in paint) {
const fillIcon = getValue(
layer,
'paint',
layer.type + '-pattern',
zoom,
f,
functionCache,
featureState
);
if (fillIcon) {
const icon =
typeof fillIcon === 'string'
? fromTemplate(fillIcon, properties)
: fillIcon.toString();
if (spriteImage && spriteData && spriteData[icon]) {
++stylesLength;
style = styles[stylesLength];
if (
!style ||
!style.getFill() ||
style.getStroke() ||
style.getText()
) {
style = new Style({
fill: new Fill(),
});
styles[stylesLength] = style;
}
fill = style.getFill();
style.setZIndex(index);
const icon_cache_key = icon + '.' + opacity;
let pattern = patternCache[icon_cache_key];
if (!pattern) {
const spriteImageData = spriteData[icon];
const canvas = createCanvas(
spriteImageData.width,
spriteImageData.height
);
const ctx = /** @type {CanvasRenderingContext2D} */ (
canvas.getContext('2d')
);
ctx.globalAlpha = opacity;
ctx.drawImage(
spriteImage,
spriteImageData.x,
spriteImageData.y,
spriteImageData.width,
spriteImageData.height,
0,
0,
spriteImageData.width,
spriteImageData.height
);
pattern = ctx.createPattern(canvas, 'repeat');
patternCache[icon_cache_key] = pattern;
}
fill.setColor(pattern);
}
}
} else {
color = colorWithOpacity(
getValue(
layer,
'paint',
layer.type + '-color',
zoom,
f,
functionCache,
featureState
),
opacity
);
if (layer.type + '-outline-color' in paint) {
strokeColor = colorWithOpacity(
getValue(
layer,
'paint',
layer.type + '-outline-color',
zoom,
f,
functionCache,
featureState
),
opacity
);
}
if (!strokeColor) {
strokeColor = color;
}
if (color || strokeColor) {
++stylesLength;
style = styles[stylesLength];
if (
!style ||
(color && !style.getFill()) ||
(!color && style.getFill()) ||
(strokeColor && !style.getStroke()) ||
(!strokeColor && style.getStroke()) ||
style.getText()
) {
style = new Style({
fill: color ? new Fill() : undefined,
stroke: strokeColor ? new Stroke() : undefined,
});
styles[stylesLength] = style;
}
if (color) {
fill = style.getFill();
fill.setColor(color);
}
if (strokeColor) {
stroke = style.getStroke();
stroke.setColor(strokeColor);
stroke.setWidth(0.5);
}
style.setZIndex(index);
}
}
}
if (type != 1 && layer.type == 'line') {
color =
!('line-pattern' in paint) && 'line-color' in paint
? colorWithOpacity(
getValue(
layer,
'paint',
'line-color',
zoom,
f,
functionCache,
featureState
),
getValue(
layer,
'paint',
'line-opacity',
zoom,
f,
functionCache,
featureState
)
)
: undefined;
const width = getValue(
layer,
'paint',
'line-width',
zoom,
f,
functionCache,
featureState
);
if (color && width > 0) {
++stylesLength;
style = styles[stylesLength];
if (
!style ||
!style.getStroke() ||
style.getFill() ||
style.getText()
) {
style = new Style({
stroke: new Stroke(),
});
styles[stylesLength] = style;
}
stroke = style.getStroke();
stroke.setLineCap(
getValue(
layer,
'layout',
'line-cap',
zoom,
f,
functionCache,
featureState
)
);
stroke.setLineJoin(
getValue(
layer,
'layout',
'line-join',
zoom,
f,
functionCache,
featureState
)
);
stroke.setMiterLimit(
getValue(
layer,
'layout',
'line-miter-limit',
zoom,
f,
functionCache,
featureState
)
);
stroke.setColor(color);
stroke.setWidth(width);
stroke.setLineDash(
paint['line-dasharray']
? getValue(
layer,
'paint',
'line-dasharray',
zoom,
f,
functionCache,
featureState
).map(function (x) {
return x * width;
})
: null
);
style.setZIndex(index);
}
}
let hasImage = false;
let text = null;
let placementAngle = 0;
let icon, iconImg, skipLabel;
if ((type == 1 || type == 2) && 'icon-image' in layout) {
const iconImage = getValue(
layer,
'layout',
'icon-image',
zoom,
f,
functionCache,
featureState
);
if (iconImage) {
icon =
typeof iconImage === 'string'
? fromTemplate(iconImage, properties)
: iconImage.toString();
let styleGeom = undefined;
if (spriteImage && spriteData && spriteData[icon]) {
const iconRotationAlignment = getValue(
layer,
'layout',
'icon-rotation-alignment',
zoom,
f,
functionCache,
featureState
);
if (type == 2) {
const geom = feature.getGeometry();
// ol package and ol-debug.js only
if (geom.getFlatMidpoint || geom.getFlatMidpoints) {
const extent = geom.getExtent();
const size = Math.sqrt(
Math.max(
Math.pow((extent[2] - extent[0]) / resolution, 2),
Math.pow((extent[3] - extent[1]) / resolution, 2)
)
);
if (size > 150) {
//FIXME Do not hard-code a size of 150
const midpoint =
geom.getType() === 'MultiLineString'
? geom.getFlatMidpoints()
: geom.getFlatMidpoint();
if (!renderFeature) {
renderFeatureCoordinates = [NaN, NaN];
renderFeature = new RenderFeature(
'Point',
renderFeatureCoordinates,
[],
{},
null
);
}
styleGeom = renderFeature;
renderFeatureCoordinates[0] = midpoint[0];
renderFeatureCoordinates[1] = midpoint[1];
const placement = getValue(
layer,
'layout',
'symbol-placement',
zoom,
f,
functionCache,
featureState
);
if (
placement === 'line' &&
iconRotationAlignment === 'map'
) {
const stride = geom.getStride();
const coordinates = geom.getFlatCoordinates();
for (
let i = 0, ii = coordinates.length - stride;
i < ii;
i += stride
) {
const x1 = coordinates[i];
const y1 = coordinates[i + 1];
const x2 = coordinates[i + stride];
const y2 = coordinates[i + stride + 1];
const minX = Math.min(x1, x2);
const minY = Math.min(y1, y2);
const maxX = Math.max(x1, x2);
const maxY = Math.max(y1, y2);
if (
midpoint[0] >= minX &&
midpoint[0] <= maxX &&
midpoint[1] >= minY &&
midpoint[1] <= maxY
) {
placementAngle = Math.atan2(y1 - y2, x2 - x1);
break;
}
}
}
}
}
}
if (type !== 2 || styleGeom) {
const iconSize = getValue(
layer,
'layout',
'icon-size',
zoom,
f,
functionCache,
featureState
);
const iconColor =
paint['icon-color'] !== undefined
? getValue(
layer,
'paint',
'icon-color',
zoom,
f,
functionCache,
featureState
)
: null;
if (!iconColor || iconColor.a !== 0) {
let icon_cache_key = icon + '.' + iconSize;
if (iconColor !== null) {
icon_cache_key += '.' + iconColor;
}
iconImg = iconImageCache[icon_cache_key];
if (!iconImg) {
const spriteImageData = spriteData[icon];
const declutterMode = getIconDeclutterMode(
layer,
zoom,
f,
functionCache
);
iconImg = new Icon({
color: iconColor
? [
iconColor.r * 255,
iconColor.g * 255,
iconColor.b * 255,
iconColor.a,
]
: undefined,
img: spriteImage,
imgSize: spriteImgSize,
size: [spriteImageData.width, spriteImageData.height],
offset: [spriteImageData.x, spriteImageData.y],
rotateWithView: iconRotationAlignment === 'map',
scale: iconSize / spriteImageData.pixelRatio,
displacement:
'icon-offset' in layout
? getValue(
layer,
'layout',
'icon-offset',
zoom,
f,
functionCache,
featureState
).map((v) => -v * spriteImageData.pixelRatio)
: undefined,
declutterMode: declutterMode,
});
iconImageCache[icon_cache_key] = iconImg;
}
}
if (iconImg) {
++stylesLength;
style = styles[stylesLength];
if (
!style ||
!style.getImage() ||
style.getFill() ||
style.getStroke()
) {
style = new Style();
styles[stylesLength] = style;
}
style.setGeometry(styleGeom);
iconImg.setRotation(
placementAngle +
deg2rad(
getValue(
layer,
'layout',
'icon-rotate',
zoom,
f,
functionCache,
featureState
)
)
);
iconImg.setOpacity(
getValue(
layer,
'paint',
'icon-opacity',
zoom,
f,
functionCache,
featureState
)
);
iconImg.setAnchor(
anchor[
getValue(
layer,
'layout',
'icon-anchor',
zoom,
f,
functionCache,
featureState
)
]
);
style.setImage(iconImg);
text = style.getText();
style.setText(undefined);
style.setZIndex(index);
hasImage = true;
skipLabel = false;
}
} else {
skipLabel = true;
}
}
}
}
if (type == 1 && layer.type === 'circle') {
++stylesLength;
style = styles[stylesLength];
if (
!style ||
!style.getImage() ||
style.getFill() ||
style.getStroke()
) {
style = new Style();
styles[stylesLength] = style;
}
const circleRadius =
'circle-radius' in paint
? getValue(
layer,
'paint',
'circle-radius',
zoom,
f,
functionCache,
featureState
)
: 5;
const circleStrokeColor = colorWithOpacity(
getValue(
layer,
'paint',
'circle-stroke-color',
zoom,
f,
functionCache,
featureState
),
getValue(
layer,
'paint',
'circle-stroke-opacity',
zoom,
f,
functionCache,
featureState
)
);
const circleColor = colorWithOpacity(
getValue(
layer,
'paint',
'circle-color',
zoom,
f,
functionCache,
featureState
),
getValue(
layer,
'paint',
'circle-opacity',
zoom,
f,
functionCache,
featureState
)
);
const circleStrokeWidth = getValue(
layer,
'paint',
'circle-stroke-width',
zoom,
f,
functionCache,
featureState
);
const cache_key =
circleRadius +
'.' +
circleStrokeColor +
'.' +
circleColor +
'.' +
circleStrokeWidth;
iconImg = iconImageCache[cache_key];
if (!iconImg) {
iconImg = new Circle({
radius: circleRadius,
stroke:
circleStrokeColor && circleStrokeWidth > 0
? new Stroke({
width: circleStrokeWidth,
color: circleStrokeColor,
})
: undefined,
fill: circleColor
? new Fill({
color: circleColor,
})
: undefined,
declutterMode: 'none',
});
iconImageCache[cache_key] = iconImg;
}
style.setImage(iconImg);
text = style.getText();
style.setText(undefined);
style.setGeometry(undefined);
style.setZIndex(index);
hasImage = true;
}
let label, font, textLineHeight, textSize, letterSpacing, maxTextWidth;
if ('text-field' in layout) {
textSize = Math.round(
getValue(
layer,
'layout',
'text-size',
zoom,
f,
functionCache,
featureState
)
);
const fontArray = getValue(
layer,
'layout',
'text-font',
zoom,
f,
functionCache,
featureState
);
textLineHeight = getValue(
layer,
'layout',
'text-line-height',
zoom,
f,
functionCache,
featureState
);
font = mb2css(
getFonts ? getFonts(fontArray) : fontArray,
textSize,
textLineHeight
);
if (!font.includes('sans-serif')) {
font += ',sans-serif';
}
letterSpacing = getValue(
layer,
'layout',
'text-letter-spacing',
zoom,
f,
functionCache,
featureState
);
maxTextWidth = getValue(
layer,
'layout',
'text-max-width',
zoom,
f,
functionCache,
featureState
);
const textField = getValue(
layer,
'layout',
'text-field',
zoom,
f,
functionCache,
featureState
);
if (typeof textField === 'object' && textField.sections) {
if (textField.sections.length === 1) {
label = textField.toString();
} else {
label = textField.sections.reduce((acc, chunk, i) => {
const fonts = chunk.fontStack
? chunk.fontStack.split(',')
: fontArray;
const chunkFont = mb2css(
getFonts ? getFonts(fonts) : fonts,
textSize * (chunk.scale || 1),
textLineHeight
);
let text = chunk.text;
if (text === '\n') {
acc.push('\n', '');
return acc;
}
if (type == 2) {
acc.push(applyLetterSpacing(text, letterSpacing), chunkFont);
return;
}
text = wrapText(
text,
chunkFont,
maxTextWidth,
letterSpacing
).split('\n');
for (let i = 0, ii = text.length; i < ii; ++i) {
if (i > 0) {
acc.push('\n', '');
}
acc.push(text[i], chunkFont);
}
return acc;
}, []);
}
} else {
label = fromTemplate(textField, properties).trim();
}
opacity = getValue(
layer,
'paint',
'text-opacity',
zoom,
f,
functionCache,
featureState
);
}
if (label && opacity && !skipLabel) {
if (!hasImage) {
++stylesLength;
style = styles[stylesLength];
if (
!style ||
!style.getText() ||
style.getFill() ||
style.getStroke()
) {
style = new Style();
styles[stylesLength] = style;
}
style.setImage(undefined);
style.setGeometry(undefined);
}
if (!style.getText()) {
style.setText(
text ||
new Text({
padding: [2, 2, 2, 2],
})
);
}
text = style.getText();
const textTransform = layout['text-transform'];
if (textTransform == 'uppercase') {
label = Array.isArray(label)
? label.map((t, i) => (i % 2 ? t : t.toUpperCase()))
: label.toUpperCase();
} else if (textTransform == 'lowercase') {
label = Array.isArray(label)
? label.map((t, i) => (i % 2 ? t : t.toLowerCase()))
: label.toLowerCase();
}
const wrappedLabel = Array.isArray(label)
? label
: type == 2
? applyLetterSpacing(label, letterSpacing)
: wrapText(label, font, maxTextWidth, letterSpacing);
text.setText(wrappedLabel);
text.setFont(font);
text.setRotation(
deg2rad(
getValue(
layer,
'layout',
'text-rotate',
zoom,
f,
functionCache,
featureState
)
)
);
const textAnchor = getValue(
layer,
'layout',
'text-anchor',
zoom,
f,
functionCache,
featureState
);
const placement =
hasImage || type == 1
? 'point'
: getValue(
layer,
'layout',
'symbol-placement',
zoom,
f,
functionCache,
featureState
);
text.setPlacement(placement);
text.setOverflow(placement === 'point');
let textHaloWidth = getValue(
layer,
'paint',
'text-halo-width',
zoom,
f,
functionCache,
featureState
);
const textOffset = getValue(
layer,
'layout',
'text-offset',
zoom,
f,
functionCache,
featureState
);
const textTranslate = getValue(
layer,
'paint',
'text-translate',
zoom,
f,
functionCache,
featureState
);
// Text offset has to take halo width and line height into account
let vOffset = 0;
let hOffset = 0;
if (placement == 'point') {
let textAlign = 'center';
if (textAnchor.indexOf('left') !== -1) {
textAlign = 'left';
hOffset = textHaloWidth;
} else if (textAnchor.indexOf('right') !== -1) {
textAlign = 'right';
hOffset = -textHaloWidth;
}
text.setTextAlign(textAlign);
const textRotationAlignment = getValue(
layer,
'layout',
'text-rotation-alignment',
zoom,
f,
functionCache,
featureState
);
text.setRotateWithView(textRotationAlignment == 'map');
} else {
text.setMaxAngle(
(deg2rad(
getValue(
layer,
'layout',
'text-max-angle',
zoom,
f,
functionCache,
featureState
)
) *
label.length) /
wrappedLabel.length
);
text.setTextAlign();
text.setRotateWithView(false);
}
let textBaseline = 'middle';
if (textAnchor.indexOf('bottom') == 0) {
textBaseline = 'bottom';
vOffset = -textHaloWidth - 0.5 * (textLineHeight - 1) * textSize;
} else if (textAnchor.indexOf('top') == 0) {
textBaseline = 'top';
vOffset = textHaloWidth + 0.5 * (textLineHeight - 1) * textSize;
}
text.setTextBaseline(textBaseline);
text.setOffsetX(
textOffset[0] * textSize + hOffset + textTranslate[0]
);
text.setOffsetY(
textOffset[1] * textSize + vOffset + textTranslate[1]
);
textColor.setColor(
colorWithOpacity(
getValue(
layer,
'paint',
'text-color',
zoom,
f,
functionCache,
featureState
),
opacity
)
);
text.setFill(textColor);
const haloColor = colorWithOpacity(
getValue(
layer,
'paint',
'text-halo-color',
zoom,
f,
functionCache,
featureState
),
opacity
);
if (haloColor) {
textHalo.setColor(haloColor);
// spec here : https://docs.mapbox.com/mapbox-gl-js/style-spec/#paint-symbol-text-halo-width
// Halo width must be doubled because it is applied around the center of the text outline
textHaloWidth *= 2;
// 1/4 of text size (spec) x 2
const halfTextSize = 0.5 * textSize;
textHalo.setWidth(
textHaloWidth <= halfTextSize ? textHaloWidth : halfTextSize
);
text.setStroke(textHalo);
} else {
text.setStroke(undefined);
}
const textPadding = getValue(
layer,
'layout',
'text-padding',
zoom,
f,
functionCache,
featureState
);
const padding = text.getPadding();
if (textPadding !== padding[0]) {
padding[0] = textPadding;
padding[1] = textPadding;
padding[2] = textPadding;
padding[3] = textPadding;
}
style.setZIndex(index);
}
}
}
if (stylesLength > -1) {
styles.length = stylesLength + 1;
if (recordLayer) {
if (typeof feature.set === 'function') {
// ol/Feature
feature.set('mapbox-layer', featureBelongsToLayer);
} else {
// ol/render/Feature
feature.getProperties()['mapbox-layer'] = featureBelongsToLayer;
}
}
return styles;
}
};
olLayer.setStyle(styleFunction);
olLayer.set('mapbox-source', mapboxSource);
olLayer.set('mapbox-layers', mapboxLayers);
olLayer.set('mapbox-featurestate', {});
return styleFunction;
}
export {
colorWithOpacity as _colorWithOpacity,
evaluateFilter as _evaluateFilter,
fromTemplate as _fromTemplate,
getValue as _getValue,
};