/* 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 { clearFunctionCache, createCanvas, defaultResolutions, deg2rad, drawIconHalo, drawSDF, getFilterCache, getFunctionCache, getStyleFunctionKey, 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) { 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 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 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} 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} 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} 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 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=undefined] * 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) { if (typeof Image !== 'undefined') { const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = function () { spriteImage = img; spriteImageSize = [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; 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) || 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} 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') { 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; 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, [], {}, 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) { 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 = getIconDeclutterMode( layer, zoom, f, functionCache ); let displacement; if ('icon-offset' in layout) { displacement = getValue( layer, 'layout', 'icon-offset', zoom, f, functionCache, featureState ).slice(0); displacement[1] *= -1; } 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, }; 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, imgSize, size, offset; if (haloWidth) { if (spriteImageData.sdf) { img = drawIconHalo( drawSDF(spriteImage, spriteImageData, iconColor), { 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 ); } imgSize = [img.width, img.height]; } 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; } imgSize = spriteImageSize; size = [spriteImageData.width, spriteImageData.height]; offset = [spriteImageData.x, spriteImageData.y]; } iconImg = new Icon({ color: color, img: img, imgSize: imgSize, 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 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, 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; } 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 ); 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] ); 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 ('set' in feature) { // ol/Feature feature.set('mapbox-layer', featureBelongsToLayer); } else { // ol/render/Feature feature.getProperties()['mapbox-layer'] = featureBelongsToLayer; } } return styles; } return undefined; }; olLayer.setStyle(styleFunction); olLayer.set('mapbox-source', mapboxSource); olLayer.set('mapbox-layers', mapboxLayers); olLayer.set('mapbox-featurestate', olLayer.get('mapbox-featurestate') || {}); return styleFunction; } /** * Get the the style for a specific Mapbox layer only. This can be useful for creating a legend. * @param {import("ol/Feature").default|import("ol/render/Feature").default} feature OpenLayers feature. * @param {number} resolution View resolution. * @param {import("ol/layer").Vector|import("ol/layer").VectorTile} olLayer OpenLayers layer. * @param {string} layerId Id of the Mapbox layer to get the style for * @return {Array} Styles for the provided Mapbox layer. */ export function getStyleForLayer(feature, resolution, olLayer, layerId) { const evaluateStyle = olLayer.getStyleFunction(); if (evaluateStyle.length === 3) { // @ts-ignore return evaluateStyle(feature, resolution, layerId); } return undefined; } export { colorWithOpacity as _colorWithOpacity, evaluateFilter as _evaluateFilter, fromTemplate as _fromTemplate, getValue as _getValue, };