UNPKG

ol-mapbox-style

Version:

Create OpenLayers maps from Mapbox Style objects

1,560 lines (1,526 loc) 53.4 kB
/* ol-mapbox-style - Use Mapbox/MapLibre Style objects with OpenLayers Copyright 2016-present ol-mapbox-style contributors License: https://raw.githubusercontent.com/openlayers/ol-mapbox-style/master/LICENSE */ import { Color, convertFunction, createPropertyExpression, derefLayers, featureFilter as createFilter, isExpression, isFunction, v8 as spec, } from '@maplibre/maplibre-gl-style-spec'; import mb2css from 'mapbox-to-css-font'; import {toPromise} from 'ol/functions.js'; import RenderFeature from 'ol/render/Feature.js'; import Circle from 'ol/style/Circle.js'; import Fill from 'ol/style/Fill.js'; import Icon from 'ol/style/Icon.js'; import Stroke from 'ol/style/Stroke.js'; import Style from 'ol/style/Style.js'; import Text from 'ol/style/Text.js'; import {applyLetterSpacing, wrapText} from './text.js'; import { clearFunctionCache, createCanvas, defaultResolutions, deg2rad, drawIconHalo, drawSDF, getFilterCache, getFunctionCache, getStyleFunctionKey, getZoomForResolution, } from './util.js'; /** * @typedef {import("ol/layer/Vector").default} VectorLayer * @typedef {import("ol/layer/VectorTile").default} VectorTileLayer * @typedef {import("ol/style/Style").StyleFunction} StyleFunction * @typedef {import('./util.js').ResourceType} ResourceType */ 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 {"icon"|"text"} prefix Style property prefix. * @param {Object} [functionCache] Function cache. * @return {"declutter"|"obstacle"|"none"} Value. */ function getDeclutterMode(layer, zoom, feature, prefix, functionCache) { const allowOverlap = getValue( layer, 'layout', `${prefix}-allow-overlap`, zoom, feature, functionCache, ); if (!allowOverlap) { return 'declutter'; } const ignorePlacement = getValue( layer, 'layout', `${prefix}-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) { if (enabled !== renderTransparentEnabled) { clearFunctionCache(); 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/MapLibre 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; } export const styleFunctionArgs = {}; /** * 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/MapLibre Style document's source that the * OpenLayers layer was created from. Usually `apply()` creates one * OpenLayers layer per Mapbox/MapLibre Style source, unless the layer stack has * layers from different sources in between. * * `mapbox-layers`: The `id`s of the Mapbox/MapLibre 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/MapLibre Style object. * @param {string|Array<string>} sourceOrLayers `source` key or an array of layer `id`s * from the Mapbox/MapLibre 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/MapLibre Style object's `sprite` property. Only required if a `sprite` * property is specified in the Mapbox/MapLibre Style object. * @param {string|Request|Response|Promise<string|Request|Response>} spriteImageUrl Sprite image url for the sprite * specified in the Mapbox/MapLibre Style object's `sprite` property. Only required if a * `sprite` property is specified in the Mapbox/MapLibre Style object. * @param {function(Array<string>, string=):Array<string>} getFonts Function that * receives a font stack and the url template from the GL style's `metadata['ol:webfonts']` * property (if set) as arguments, and returns a (modified) font stack that * is available. Font names are the names used in the Mapbox/MapLibre Style object. If * not provided, the font stack will be used as-is. This function can also be * used for loading web fonts. * @param {function(VectorLayer|VectorTileLayer, string):HTMLImageElement|HTMLCanvasElement|string|undefined} [getImage] * Function that returns an image or a URL for an image name. If the result is an HTMLImageElement, it must already be * loaded. The layer can be used to call layer.changed() when the loading and processing of the image has finished. * This function can be used for icons not in the sprite or to override sprite icons. * @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, getImage = undefined, ) { if (typeof glStyle == 'string') { glStyle = JSON.parse(glStyle); } if (glStyle.version != 8) { throw new Error('glStyle version 8 required.'); } styleFunctionArgs[getStyleFunctionKey(glStyle, olLayer)] = Array.from(arguments); let spriteImage, spriteImageSize; let spriteImageUnSDFed; if (spriteImageUrl) { toPromise(() => spriteImageUrl).then(async (spriteImageUrl) => { let blobUrl; if (typeof Image !== 'undefined') { const img = new Image(); if (typeof spriteImageUrl === 'string') { img.crossOrigin = 'anonymous'; img.src = spriteImageUrl; } else { let response; if (spriteImageUrl instanceof Request) { response = await fetch(spriteImageUrl); } else if (spriteImageUrl instanceof Response) { response = spriteImageUrl; } const blob = await response.blob(); blobUrl = URL.createObjectURL(blob); img.src = blobUrl; } img.addEventListener('load', function load() { img.removeEventListener('load', load); spriteImage = img; spriteImageSize = [img.width, img.height]; olLayer.changed(); if (blobUrl) { URL.revokeObjectURL(blobUrl); } }); img.addEventListener('error', function error() { URL.revokeObjectURL(blobUrl); img.removeEventListener('error', error); }); } 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; spriteImageSize = [spriteImage.width, spriteImage.height]; } }); } }); } const allLayers = derefLayers(glStyle.layers); const layersBySourceLayer = {}; const mapboxLayers = []; const iconImageCache = {}; const patternCache = {}; const functionCache = getFunctionCache(glStyle); const filterCache = getFilterCache(glStyle); 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) || (Array.isArray(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 = []; /** * @param {import("ol/Feature").default|import("ol/render/Feature").default} feature Feature. * @param {number} resolution Resolution. * @param {string} [onlyLayer] Calculate style for this layer only. * @return {Array<import("ol/style/Style").default>} Style. */ const styleFunction = function (feature, resolution, onlyLayer) { const properties = feature.getProperties(); const layers = layersBySourceLayer[properties.layer]; if (!layers) { return undefined; } let zoom = resolutions.indexOf(resolution); if (zoom == -1) { zoom = getZoomForResolution(resolution, resolutions); } const type = types[feature.getGeometry().getType()]; const f = { id: feature.getId(), 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; if (onlyLayer !== undefined && onlyLayer !== layerId) { continue; } 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') { if (!('line-pattern' in paint)) { color = colorWithOpacity( getValue( layer, 'paint', 'line-color', zoom, f, functionCache, featureState, ), getValue( layer, 'paint', 'line-opacity', zoom, f, functionCache, featureState, ), ); } else { color = 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; const imageElement = getImage ? getImage(olLayer, icon) : undefined; if ( (spriteImage && spriteData && spriteData[icon]) || imageElement ) { const iconRotationAlignment = getValue( layer, 'layout', 'icon-rotation-alignment', zoom, f, functionCache, featureState, ); if (type == 2) { const geom = /** @type {*} */ (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, [], 2, {}, undefined, ); } 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 maxX = Math.max(x1, x2); const xM = midpoint[0]; const yM = midpoint[1]; const dotProduct = (y2 - y1) * (xM - x1) - (x2 - x1) * (yM - y1); if ( Math.abs(dotProduct) < 0.001 && //midpoint is aligned with the segment xM <= maxX && xM >= minX //midpoint is on the segment and not outside it ) { 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) { const haloColor = getValue( layer, 'paint', 'icon-halo-color', zoom, f, functionCache, featureState, ); const haloWidth = getValue( layer, 'paint', 'icon-halo-width', zoom, f, functionCache, featureState, ); let iconCacheKey = `${icon}.${iconSize}.${haloWidth}.${haloColor}`; if (iconColor !== null) { iconCacheKey += `.${iconColor}`; } iconImg = iconImageCache[iconCacheKey]; if (!iconImg) { const declutterMode = getDeclutterMode( layer, zoom, f, 'icon', functionCache, ); let displacement; if ('icon-offset' in layout) { displacement = getValue( layer, 'layout', 'icon-offset', zoom, f, functionCache, featureState, ).slice(0); displacement[0] *= iconSize; displacement[1] *= -iconSize; } let color = iconColor ? [ iconColor.r * 255, iconColor.g * 255, iconColor.b * 255, iconColor.a, ] : undefined; if (imageElement) { const iconOptions = { color: color, rotateWithView: iconRotationAlignment === 'map', displacement: displacement, declutterMode: declutterMode, scale: iconSize, }; if (typeof imageElement === 'string') { // it is a src URL iconOptions.src = imageElement; } else { iconOptions.img = imageElement; iconOptions.imgSize = [ imageElement.width, imageElement.height, ]; } iconImg = new Icon(iconOptions); } else { const spriteImageData = spriteData[icon]; let img, size, offset; if (haloWidth) { if (spriteImageData.sdf) { img = drawIconHalo( drawSDF( spriteImage, spriteImageData, iconColor || [0, 0, 0, 1], ), { x: 0, y: 0, width: spriteImageData.width, height: spriteImageData.height, pixelRatio: spriteImageData.pixelRatio, }, haloWidth, haloColor, ); color = undefined; // do not tint haloed icons } else { img = drawIconHalo( spriteImage, spriteImageData, haloWidth, haloColor, ); } } else { if (spriteImageData.sdf) { if (!spriteImageUnSDFed) { spriteImageUnSDFed = drawSDF( spriteImage, { x: 0, y: 0, width: spriteImageSize[0], height: spriteImageSize[1], }, {r: 1, g: 1, b: 1, a: 1}, ); } img = spriteImageUnSDFed; } else { img = spriteImage; } size = [spriteImageData.width, spriteImageData.height]; offset = [spriteImageData.x, spriteImageData.y]; } iconImg = new Icon({ color: color, img: img, // @ts-ignore imgSize: spriteImageSize, size: size, offset: offset, rotateWithView: iconRotationAlignment === 'map', scale: iconSize / spriteImageData.pixelRatio, displacement: displacement, declutterMode: declutterMode, }); } iconImageCache[iconCacheKey] = 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 circleTranslate = getValue( layer, 'paint', 'circle-translate', 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 + '.' + circleTranslate[0] + '.' + circleTranslate[1]; iconImg = iconImageCache[cache_key]; if (!iconImg) { iconImg = new Circle({ radius: circleRadius, displacement: [circleTranslate[0], -circleTranslate[1]], 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, glStyle.metadata ? glStyle.metadata['ol:webfonts'] : undefined, ) : 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 acc; } 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); } const declutterMode = getDeclutterMode( layer, zoom, f, 'text', functionCache, ); if (!style.getText()) { style.setText(text); } text = style.getText(); if ( !text || ('getDeclutterMode' in text && text.getDeclutterMode() !== declutterMode) ) { text = new Text({ padding: [2, 2, 2, 2], // @ts-ignore declutterMode: declutterMode, }); style.setText(text); } const textTransform = getValue( layer, 'layout', 'text-transform', zoom, f, functionCache, featureState, ); 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, ), ), ); if (typeof text.setKeepUpright === 'function') { const keepUpright = getValue( layer, 'layout', 'text-keep-upright', zoom, f, functionCache, featureState, ); text.setKeepUpright(keepUpright); } 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, ); let textAlign; if (placement === 'line-center') { text.setPlacement('line'); textAlign = 'center'; } else { text.setPlacement(placement); } if (placement === 'line' && typeof text.setRepeat === 'function') { const symbolSpacing = getValue( layer, 'layout', 'symbol-spacing', zoom, f, functionCache, featureState, ); text.setRepeat(symbolSpacing * 2); } 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') { textAlign = 'center'; if (textAnchor.indexOf('left') !== -1) { textAlign = 'left'; hOffset = textHaloWidth; } else if (textAnchor.indexOf('right') !== -1) { textAlign = 'right'; hOffset = -textHaloWidth; } 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.setRotateWithView(false); } text.setTextAlign(textAlign); 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); const textJustify = getValue( layer, 'layout', 'text-justify', zoom, f, functionCache, featureState, ); text.setJustify(textJustify === 'auto' ? undefined : textJustify); text.setOffsetX( textOffset[0] * textSize + hOffset + textTranslate[0], ); text.setOffsetY( textOffset[1] * textSize + vOffset + textTranslate[1], );