UNPKG

ol-mapbox-style

Version:

Create OpenLayers layers or maps from Mapbox/MapLibre styles

1,524 lines (1,485 loc) 59 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, featureFilter as createFilter, createPropertyExpression, derefLayers, isExpression, isFunction, v8 as spec, } from '@maplibre/maplibre-gl-style-spec'; import mb2css from 'mapbox-to-css-font'; import Map from 'ol/Map.js'; import {distance} from 'ol/coordinate.js'; import {getCenter} from 'ol/extent.js'; 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 {cameraObj, styleConfig, wrapImageExtraArgs} from './expressions.js'; import {applyLetterSpacing, wrapText} from './text.js'; import { clearFunctionCache, createCanvas, defaultResolutions, deg2rad, drawIconHalo, drawSDF, emptyObj, getFilterCache, getFunctionCache, getStyleFunctionKey, getZoomForResolution, } from './util.js'; /** * @typedef {import("ol/layer/Vector.js").default} VectorLayer * @typedef {import("ol/layer/VectorTile.js").default} VectorTileLayer * @typedef {import("ol/style/Style.js").StyleFunction} StyleFunction * @typedef {import('./util.js').ResourceType} ResourceType */ /** @typedef {string|Request|Response|Promise<string|Request|Response>|Object<string, string|Request|Response|Promise<string|Request|Response>>} SpriteImageUrl */ /** * @typedef {Object} SpriteImage * @property {HTMLImageElement|HTMLCanvasElement} image Image * @property {Array<number>} size Size * @property {boolean} [unSDFed] Image has been unSDFed */ /** @typedef {Object<string, SpriteImage>} SpriteImages */ 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) { let compiledExpression = createPropertyExpression( rawExpression, propertySpec, ); if (compiledExpression.result === 'error') { const wrappedExpression = wrapImageExtraArgs(rawExpression); if (wrappedExpression !== rawExpression) { compiledExpression = createPropertyExpression( wrappedExpression, propertySpec, ); } } if (compiledExpression.result === 'error') { const err = compiledExpression.value[0]; // eslint-disable-next-line no-console console.error( 'Error parsing expression:', rawExpression, err.key, err.message, ); // fallback to default value return { evaluate: () => { return propertySpec.default; }, }; } return compiledExpression.value; }; let renderFeatureCoordinates, renderFeature; /** * @private * @param {Object} layer Gl object layer. * @param {string} layoutOrPaint 'layout' or 'paint'. * @param {string} property Feature property. * @param {Object} feature Gl feature. * @param {Object} [functionCache] Function cache. * @param {Object} [featureState] Feature state. * @return {?} Value. */ export function getValue( layer, layoutOrPaint, property, 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}`] && spec[`${layoutOrPaint}_${layer.type}`][property]; if (value === undefined) { if (propertySpec) { 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 { const type = propertySpec ? propertySpec.type : typeof value; if (type === 'color' || type === 'colorArray') { value = Color.parse(value); } let hasExpr = false; if (type === 'array') { for (let i = 0; i < value.length; ++i) { const item = value[i]; if (isExpression(item) || isFunction(item)) { hasExpr = true; break; } } } if (hasExpr) { const itemPropertySpec = Object.assign({}, propertySpec, { type: propertySpec.value, }); const itemExpressions = []; for (let i = 0; i < value.length; ++i) { let item = value[i]; if (!isExpression(item) && isFunction(item)) { item = convertFunction(item, itemPropertySpec); } if (isExpression(item)) { const compiledExpression = expressionData(item, itemPropertySpec); itemExpressions.push( compiledExpression.evaluate.bind(compiledExpression), ); } else { itemExpressions.push(function () { return item; }); } } functions[property] = function ( globalProperties, feature, featureState, ) { const result = []; for (let i = 0; i < itemExpressions.length; ++i) { result[i] = itemExpressions[i]( globalProperties, feature, featureState, ); } return result; }; } else { functions[property] = function () { return value; }; } } } return functions[property](cameraObj, feature, featureState); } /** * @private * @param {Object} layer Gl object layer. * @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, feature, prefix, functionCache) { const allowOverlap = getValue( layer, 'layout', `${prefix}-allow-overlap`, feature, functionCache, ); if (!allowOverlap) { return 'declutter'; } const ignorePlacement = getValue( layer, 'layout', `${prefix}-ignore-placement`, feature, functionCache, ); if (!ignorePlacement) { return 'obstacle'; } return 'none'; } /** * @private * @param {string} layerId Layer id. * @param {?} filter Filter. * @param {Object} feature Feature. * @param {Object} [filterCache] Filter cache. * @return {boolean} Filter result. */ function evaluateFilter(layerId, filter, feature, filterCache) { if (!filterCache) { console.warn('No filterCache provided to evaluateFilter()'); // eslint-disable-line no-console } if (!(layerId in filterCache)) { try { filterCache[layerId] = createFilter(filter).filter; } catch (e) { // eslint-disable-next-line no-console console.warn( 'Filter will evaluate to false: ' + /** @type {Error} */ (e).message, ); filterCache[layerId] = function () { return false; }; } } return filterCache[layerId](cameraObj, 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)] || ''; }); } /** * @private * @param {string} icon Icon identifier (with prefix if not 'default') * @param {SpriteImages} spriteImages Sprite images. * @return {SpriteImage} Image. */ export function getSpriteImageForIcon(icon, spriteImages) { let prefix = icon.split(':')[0]; if (prefix === icon) { prefix = 'default'; } return spriteImages[prefix]; } 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 = {}; /** * **Caution**: This is a low level API, which is only useful for advanced use cases. * If you want to crete a map or layer group from an entire Mapbox/MapLibre style, use * the `apply()` function. If you want to create a vector layer from a single * source of a Mapbox/MapLibre style, use the `applyStyle()` function. If you want to * create a vector tile layer from a single source of a Mapbox/MapLibre style, use either * the `applyStyle()` function or the `MapboxVectorLayer` constructor. * * 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 {SpriteImageUrl} 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. If multiple `sprite`s * are defined in the style object, this has to be an object with the sprite id as key and the * sprite image URL as value. * @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.schema) { for (const key in glStyle.schema) { const config = glStyle.schema[key]; if ('default' in config) { styleConfig[key] = config.default; } } } if (glStyle.version != 8) { throw new Error('glStyle version 8 required.'); } styleFunctionArgs[getStyleFunctionKey(glStyle, olLayer)] = Array.from(arguments); /** @type {SpriteImages} */ const spriteImages = {}; if ( typeof spriteImageUrl === 'string' || spriteImageUrl instanceof Request || spriteImageUrl instanceof Response || spriteImageUrl instanceof Promise ) { spriteImageUrl = {'default': spriteImageUrl}; } for (const prefix in spriteImageUrl) { const imageUrl = spriteImageUrl[prefix]; toPromise(() => imageUrl).then(async (imageUrl) => { let blobUrl; if (typeof Image !== 'undefined') { const img = new Image(); if (typeof imageUrl === 'string') { img.crossOrigin = 'anonymous'; img.src = imageUrl; } else { let response; if (imageUrl instanceof Request) { response = await fetch(imageUrl); } else if (imageUrl instanceof Response) { response = imageUrl; } const blob = await response.blob(); blobUrl = URL.createObjectURL(blob); img.src = blobUrl; } img.addEventListener('load', function load() { img.removeEventListener('load', load); spriteImages[prefix] = { image: img, size: [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: imageUrl, }); worker.addEventListener('message', function handler(event) { if ( event.data.action === 'imageLoaded' && event.data.src === imageUrl ) { spriteImages[prefix] = { image: event.data.image, size: [event.data.image.width, event.data.image.height], }; } }); } }); } /** @type {*} */ 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 styles = []; /** * @param {import("ol/Feature.js").default|import("ol/render/Feature.js").default} feature Feature. * @param {number} resolution Resolution. * @param {string} [onlyLayer] Calculate style for this layer only. * @return {Array<import("ol/style/Style.js").default>} Style. */ const styleFunction = function (feature, resolution, onlyLayer) { const layerProperty = //@ts-ignore olLayer.getSource?.()?.format_?.layerName_ ?? 'mvt:layer'; const properties = feature.getProperties(); const layers = layersBySourceLayer[properties[layerProperty]]; if (!layers) { return undefined; } let zoom = resolutions.indexOf(resolution); if (zoom == -1) { zoom = getZoomForResolution(resolution, resolutions); } cameraObj.zoom = zoom; cameraObj.distanceFromCenter = 0; const featureGeometry = feature.getGeometry(); const type = types[featureGeometry.getType()]; const map = olLayer.get('map'); if (map && map instanceof Map && type === 1) { const size = map.getSize(); if (size) { const mapCenter = map.getView().getCenter(); const featureCenter = getCenter(featureGeometry.getExtent()); cameraObj.distanceFromCenter = distance(mapCenter, featureCenter) / resolution / size[1]; } } 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; const visibility = getValue( layer, 'layout', 'visibility', f, functionCache, featureState, ); if ( 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, 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', f, functionCache, featureState, ); if (layer.type + '-pattern' in paint) { const fillIcon = getValue( layer, 'paint', layer.type + '-pattern', f, functionCache, featureState, ); if (fillIcon) { const icon = typeof fillIcon === 'string' ? fromTemplate(fillIcon, properties) : fillIcon.toString(); const spriteImage = getSpriteImageForIcon(icon, spriteImages); if (spriteData && spriteData[icon] && spriteImage) { ++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.image, 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', f, functionCache, featureState, ), opacity, ); if (layer.type + '-outline-color' in paint) { strokeColor = colorWithOpacity( getValue( layer, 'paint', layer.type + '-outline-color', 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 (layer.type === 'fill-extrusion') { const height = getValue( layer, 'paint', 'fill-extrusion-height', f, functionCache, featureState, ); // For fill-extrusion, we darken the stroke color based on height // This gives a pseudo-3D effect if (height > 0) { // Darken factor: clamps between 0.1 and 0.9 based on height // Higher extrusion = darker outline const darkenFactor = Math.max( 0.1, 0.9 - Math.min(height, 225) / 280, ); if (strokeColor && strokeColor !== 'transparent') { const rgba = Color.parse(strokeColor); strokeColor = `rgba(${Math.round(rgba.r * 255 * darkenFactor)},${Math.round(rgba.g * 255 * darkenFactor)},${Math.round(rgba.b * 255 * darkenFactor)},${rgba.a})`; } } } 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', f, functionCache, featureState, ), getValue( layer, 'paint', 'line-opacity', f, functionCache, featureState, ), ); } else { color = undefined; } const width = getValue( layer, 'paint', 'line-width', 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', f, functionCache, featureState, ), ); stroke.setLineJoin( getValue( layer, 'layout', 'line-join', f, functionCache, featureState, ), ); stroke.setMiterLimit( getValue( layer, 'layout', 'line-miter-limit', f, functionCache, featureState, ), ); stroke.setColor(color); stroke.setWidth(width); stroke.setLineDash( paint['line-dasharray'] ? getValue( layer, 'paint', 'line-dasharray', f, functionCache, featureState, ).map(function (x) { return x * width; }) : null, ); if (typeof stroke.setOffset === 'function') { stroke.setOffset( getValue( layer, 'paint', 'line-offset', f, functionCache, featureState, ), ); } 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', f, functionCache, featureState, ); if (iconImage) { icon = typeof iconImage === 'string' ? fromTemplate(iconImage, properties) : iconImage.toString(); let styleGeom = undefined; const imageElement = getImage ? getImage(olLayer, icon) : undefined; const spriteImage = getSpriteImageForIcon(icon, spriteImages); if ( (spriteData && spriteData[icon] && spriteImage) || imageElement ) { const iconRotationAlignment = getValue( layer, 'layout', 'icon-rotation-alignment', 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', 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', f, functionCache, featureState, ); const iconColor = paint['icon-color'] !== undefined ? getValue( layer, 'paint', 'icon-color', f, functionCache, featureState, ) : null; if (!iconColor || iconColor.a !== 0) { const haloColor = getValue( layer, 'paint', 'icon-halo-color', f, functionCache, featureState, ); const haloWidth = getValue( layer, 'paint', 'icon-halo-width', f, functionCache, featureState, ); let iconCacheKey = `${icon}.${iconSize}.${haloWidth}.${haloColor}`; if (iconColor !== null) { iconCacheKey += `.${iconColor}`; } iconImg = iconImageCache[iconCacheKey]; if (!iconImg) { const declutterMode = getDeclutterMode( layer, f, 'icon', functionCache, ); let displacement; if ('icon-offset' in layout) { displacement = getValue( layer, 'layout', 'icon-offset', 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.image, 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.image, spriteImageData, haloWidth, haloColor, ); } } else { if (spriteImageData.sdf) { if (!spriteImage.unSDFed) { const spriteImageUnSDFed = drawSDF( spriteImage.image, { x: 0, y: 0, width: spriteImage.size[0], height: spriteImage.size[1], }, {r: 1, g: 1, b: 1, a: 1}, ); spriteImage.image = spriteImageUnSDFed; spriteImage.unSDFed = true; } } img = spriteImage.image; size = [spriteImageData.width, spriteImageData.height]; offset = [spriteImageData.x, spriteImageData.y]; } iconImg = new Icon({ color: color, img: img, // @ts-ignore imgSize: spriteImage.size, 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', f, functionCache, featureState, ), ), ); iconImg.setOpacity( getValue( layer, 'paint', 'icon-opacity', f, functionCache, featureState, ), ); iconImg.setAnchor( anchor[ getValue( layer, 'layout', 'icon-anchor', 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', f, functionCache, featureState, ) : 5; const circleStrokeColor = colorWithOpacity( getValue( layer, 'paint', 'circle-stroke-color', f, functionCache, featureState, ), getValue( layer, 'paint', 'circle-stroke-opacity', f, functionCache, featureState, ), ); const circleTranslate = getValue( layer, 'paint', 'circle-translate', f, functionCache, featureState, ); const circleColor = colorWithOpacity( getValue( layer, 'paint', 'circle-color', f, functionCache, featureState, ), getValue( layer, 'paint', 'circle-opacity', f, functionCache, featureState, ), ); const circleStrokeWidth = getValue( layer, 'paint', 'circle-stroke-width', 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', f, functionCache, featureState, ), ); const fontArray = getValue( layer, 'layout', 'text-font', f, functionCache, featureState, ); textLineHeight = getValue( layer, 'layout', 'text-line-height', 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', f, functionCache, featureState, ); maxTextWidth = getValue( layer, 'layout', 'text-max-width', f, functionCache, featureState, ); const textField = getValue( layer, 'layout', 'text-field', 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', 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, f, 'text', functionCache, ); if (!style.getText()) { style.setText(text); } text = style.getText(); if ( !text || ('getDeclutterMode' in text && text.getDeclutterMode() !== declutterMode) ) {