// @flow import Anchor from './anchor'; import {getAnchors, getCenterAnchor} from './get_anchors'; import clipLine from './clip_line'; import {shapeText, shapeIcon, WritingMode, fitIconToText} from './shaping'; import {getGlyphQuads, getIconQuads} from './quads'; import CollisionFeature from './collision_feature'; import {warnOnce} from '../util/util'; import { allowsVerticalWritingMode, allowsLetterSpacing } from '../util/script_detection'; import findPoleOfInaccessibility from '../util/find_pole_of_inaccessibility'; import classifyRings from '../util/classify_rings'; import EXTENT from '../data/extent'; import SymbolBucket from '../data/bucket/symbol_bucket'; import EvaluationParameters from '../style/evaluation_parameters'; import {SIZE_PACK_FACTOR} from './symbol_size'; import ONE_EM from './one_em'; import type {CanonicalTileID} from '../source/tile_id'; import type {Shaping, PositionedIcon, TextJustify} from './shaping'; import type {CollisionBoxArray} from '../data/array_types'; import type {SymbolFeature} from '../data/bucket/symbol_bucket'; import type {StyleImage} from '../style/style_image'; import type {StyleGlyph} from '../style/style_glyph'; import type SymbolStyleLayer from '../style/style_layer/symbol_style_layer'; import type {ImagePosition} from '../render/image_atlas'; import type {GlyphPosition} from '../render/glyph_atlas'; import type {PossiblyEvaluatedPropertyValue} from '../style/properties'; import Point from '@mapbox/point-geometry'; import murmur3 from 'murmurhash-js'; // The symbol layout process needs `text-size` evaluated at up to five different zoom levels, and // `icon-size` at up to three: // // 1. `text-size` at the zoom level of the bucket. Used to calculate a per-feature size for source `text-size` // expressions, and to calculate the box dimensions for icon-text-fit. // 2. `icon-size` at the zoom level of the bucket. Used to calculate a per-feature size for source `icon-size` // expressions. // 3. `text-size` and `icon-size` at the zoom level of the bucket, plus one. Used to calculate collision boxes. // 4. `text-size` at zoom level 18. Used for something line-symbol-placement-related. // 5. For composite `*-size` expressions: two zoom levels of curve stops that "cover" the zoom level of the // bucket. These go into a vertex buffer and are used by the shader to interpolate the size at render time. // // (1) and (2) are stored in `bucket.layers[0].layout`. The remainder are below. // type Sizes = { layoutTextSize: PossiblyEvaluatedPropertyValue, // (3) layoutIconSize: PossiblyEvaluatedPropertyValue, // (3) textMaxSize: PossiblyEvaluatedPropertyValue, // (4) compositeTextSizes: [PossiblyEvaluatedPropertyValue, PossiblyEvaluatedPropertyValue], // (5) compositeIconSizes: [PossiblyEvaluatedPropertyValue, PossiblyEvaluatedPropertyValue], // (5) }; export type TextAnchor = 'center' | 'left' | 'right' | 'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; // The radial offset is to the edge of the text box // In the horizontal direction, the edge of the text box is where glyphs start // But in the vertical direction, the glyphs appear to "start" at the baseline // We don't actually load baseline data, but we assume an offset of ONE_EM - 17 // (see "yOffset" in shaping.js) const baselineOffset = 7; const INVALID_TEXT_OFFSET = Number.POSITIVE_INFINITY; export function evaluateVariableOffset(anchor: TextAnchor, offset: [number, number]) { function fromRadialOffset(anchor: TextAnchor, radialOffset: number) { let x = 0, y = 0; if (radialOffset < 0) radialOffset = 0; // Ignore negative offset. // solve for r where r^2 + r^2 = radialOffset^2 const hypotenuse = radialOffset / Math.sqrt(2); switch (anchor) { case 'top-right': case 'top-left': y = hypotenuse - baselineOffset; break; case 'bottom-right': case 'bottom-left': y = -hypotenuse + baselineOffset; break; case 'bottom': y = -radialOffset + baselineOffset; break; case 'top': y = radialOffset - baselineOffset; break; } switch (anchor) { case 'top-right': case 'bottom-right': x = -hypotenuse; break; case 'top-left': case 'bottom-left': x = hypotenuse; break; case 'left': x = radialOffset; break; case 'right': x = -radialOffset; break; } return [x, y]; } function fromTextOffset(anchor: TextAnchor, offsetX: number, offsetY: number) { let x = 0, y = 0; // Use absolute offset values. offsetX = Math.abs(offsetX); offsetY = Math.abs(offsetY); switch (anchor) { case 'top-right': case 'top-left': case 'top': y = offsetY - baselineOffset; break; case 'bottom-right': case 'bottom-left': case 'bottom': y = -offsetY + baselineOffset; break; } switch (anchor) { case 'top-right': case 'bottom-right': case 'right': x = -offsetX; break; case 'top-left': case 'bottom-left': case 'left': x = offsetX; break; } return [x, y]; } return (offset[1] !== INVALID_TEXT_OFFSET) ? fromTextOffset(anchor, offset[0], offset[1]) : fromRadialOffset(anchor, offset[0]); } export function performSymbolLayout(bucket: SymbolBucket, glyphMap: {[_: string]: {[number]: ?StyleGlyph}}, glyphPositions: {[_: string]: {[number]: GlyphPosition}}, imageMap: {[_: string]: StyleImage}, imagePositions: {[_: string]: ImagePosition}, showCollisionBoxes: boolean, canonical: CanonicalTileID) { bucket.createArrays(); const tileSize = 512 * bucket.overscaling; bucket.tilePixelRatio = EXTENT / tileSize; bucket.compareText = {}; bucket.iconsNeedLinear = false; const layout = bucket.layers[0].layout; const unevaluatedLayoutValues = bucket.layers[0]._unevaluatedLayout._values; const sizes = {}; if (bucket.textSizeData.kind === 'composite') { const {minZoom, maxZoom} = bucket.textSizeData; sizes.compositeTextSizes = [ unevaluatedLayoutValues['text-size'].possiblyEvaluate(new EvaluationParameters(minZoom), canonical), unevaluatedLayoutValues['text-size'].possiblyEvaluate(new EvaluationParameters(maxZoom), canonical) ]; } if (bucket.iconSizeData.kind === 'composite') { const {minZoom, maxZoom} = bucket.iconSizeData; sizes.compositeIconSizes = [ unevaluatedLayoutValues['icon-size'].possiblyEvaluate(new EvaluationParameters(minZoom), canonical), unevaluatedLayoutValues['icon-size'].possiblyEvaluate(new EvaluationParameters(maxZoom), canonical) ]; } sizes.layoutTextSize = unevaluatedLayoutValues['text-size'].possiblyEvaluate(new EvaluationParameters(bucket.zoom + 1), canonical); sizes.layoutIconSize = unevaluatedLayoutValues['icon-size'].possiblyEvaluate(new EvaluationParameters(bucket.zoom + 1), canonical); sizes.textMaxSize = unevaluatedLayoutValues['text-size'].possiblyEvaluate(new EvaluationParameters(18)); const lineHeight = layout.get('text-line-height') * ONE_EM; const textAlongLine = layout.get('text-rotation-alignment') === 'map' && layout.get('symbol-placement') !== 'point'; const keepUpright = layout.get('text-keep-upright'); const textSize = layout.get('text-size'); for (const feature of bucket.features) { const fontstack = layout.get('text-font').evaluate(feature, {}, canonical).join(','); const layoutTextSizeThisZoom = textSize.evaluate(feature, {}, canonical); const layoutTextSize = sizes.layoutTextSize.evaluate(feature, {}, canonical); const layoutIconSize = sizes.layoutIconSize.evaluate(feature, {}, canonical); const shapedTextOrientations = { horizontal: {}, vertical: undefined }; const text = feature.text; let textOffset: [number, number] = [0, 0]; if (text) { const unformattedText = text.toString(); const spacing = layout.get('text-letter-spacing').evaluate(feature, {}, canonical) * ONE_EM; const spacingIfAllowed = allowsLetterSpacing(unformattedText) ? spacing : 0; const textAnchor = layout.get('text-anchor').evaluate(feature, {}, canonical); const variableTextAnchor = layout.get('text-variable-anchor'); if (!variableTextAnchor) { const radialOffset = layout.get('text-radial-offset').evaluate(feature, {}, canonical); // Layers with variable anchors use the `text-radial-offset` property and the [x, y] offset vector // is calculated at placement time instead of layout time if (radialOffset) { // The style spec says don't use `text-offset` and `text-radial-offset` together // but doesn't actually specify what happens if you use both. We go with the radial offset. textOffset = evaluateVariableOffset(textAnchor, [radialOffset * ONE_EM, INVALID_TEXT_OFFSET]); } else { textOffset = (layout.get('text-offset').evaluate(feature, {}, canonical).map(t => t * ONE_EM): any); } } let textJustify = textAlongLine ? "center" : layout.get('text-justify').evaluate(feature, {}, canonical); const symbolPlacement = layout.get('symbol-placement'); const maxWidth = symbolPlacement === 'point' ? layout.get('text-max-width').evaluate(feature, {}, canonical) * ONE_EM : 0; const addVerticalShapingForPointLabelIfNeeded = () => { if (bucket.allowVerticalPlacement && allowsVerticalWritingMode(unformattedText)) { // Vertical POI label placement is meant to be used for scripts that support vertical // writing mode, thus, default left justification is used. If Latin // scripts would need to be supported, this should take into account other justifications. shapedTextOrientations.vertical = shapeText(text, glyphMap, glyphPositions, imagePositions, fontstack, maxWidth, lineHeight, textAnchor, 'left', spacingIfAllowed, textOffset, WritingMode.vertical, true, symbolPlacement, layoutTextSize, layoutTextSizeThisZoom); } }; // If this layer uses text-variable-anchor, generate shapings for all justification possibilities. if (!textAlongLine && variableTextAnchor) { const justifications = textJustify === "auto" ? variableTextAnchor.map(a => getAnchorJustification(a)) : [textJustify]; let singleLine = false; for (let i = 0; i < justifications.length; i++) { const justification: TextJustify = justifications[i]; if (shapedTextOrientations.horizontal[justification]) continue; if (singleLine) { // If the shaping for the first justification was only a single line, we // can re-use it for the other justifications shapedTextOrientations.horizontal[justification] = shapedTextOrientations.horizontal[0]; } else { // If using text-variable-anchor for the layer, we use a center anchor for all shapings and apply // the offsets for the anchor in the placement step. const shaping = shapeText(text, glyphMap, glyphPositions, imagePositions, fontstack, maxWidth, lineHeight, 'center', justification, spacingIfAllowed, textOffset, WritingMode.horizontal, false, symbolPlacement, layoutTextSize, layoutTextSizeThisZoom); if (shaping) { shapedTextOrientations.horizontal[justification] = shaping; singleLine = shaping.positionedLines.length === 1; } } } addVerticalShapingForPointLabelIfNeeded(); } else { if (textJustify === "auto") { textJustify = getAnchorJustification(textAnchor); } // Horizontal point or line label. const shaping = shapeText(text, glyphMap, glyphPositions, imagePositions, fontstack, maxWidth, lineHeight, textAnchor, textJustify, spacingIfAllowed, textOffset, WritingMode.horizontal, false, symbolPlacement, layoutTextSize, layoutTextSizeThisZoom); if (shaping) shapedTextOrientations.horizontal[textJustify] = shaping; // Vertical point label (if allowVerticalPlacement is enabled). addVerticalShapingForPointLabelIfNeeded(); // Verticalized line label. if (allowsVerticalWritingMode(unformattedText) && textAlongLine && keepUpright) { shapedTextOrientations.vertical = shapeText(text, glyphMap, glyphPositions, imagePositions, fontstack, maxWidth, lineHeight, textAnchor, textJustify, spacingIfAllowed, textOffset, WritingMode.vertical, false, symbolPlacement, layoutTextSize, layoutTextSizeThisZoom); } } } let shapedIcon; let isSDFIcon = false; if (feature.icon && feature.icon.name) { const image = imageMap[feature.icon.name]; if (image) { shapedIcon = shapeIcon( imagePositions[feature.icon.name], layout.get('icon-offset').evaluate(feature, {}, canonical), layout.get('icon-anchor').evaluate(feature, {}, canonical)); isSDFIcon = image.sdf; if (bucket.sdfIcons === undefined) { bucket.sdfIcons = image.sdf; } else if (bucket.sdfIcons !== image.sdf) { warnOnce('Style sheet warning: Cannot mix SDF and non-SDF icons in one buffer'); } if (image.pixelRatio !== bucket.pixelRatio) { bucket.iconsNeedLinear = true; } else if (layout.get('icon-rotate').constantOr(1) !== 0) { bucket.iconsNeedLinear = true; } } } const shapedText = getDefaultHorizontalShaping(shapedTextOrientations.horizontal) || shapedTextOrientations.vertical; bucket.iconsInText = shapedText ? shapedText.iconsInText : false; if (shapedText || shapedIcon) { addFeature(bucket, feature, shapedTextOrientations, shapedIcon, imageMap, sizes, layoutTextSize, layoutIconSize, textOffset, isSDFIcon, canonical); } } if (showCollisionBoxes) { bucket.generateCollisionDebugBuffers(); } } // Choose the justification that matches the direction of the TextAnchor export function getAnchorJustification(anchor: TextAnchor): TextJustify { switch (anchor) { case 'right': case 'top-right': case 'bottom-right': return 'right'; case 'left': case 'top-left': case 'bottom-left': return 'left'; } return 'center'; } /** * Given a feature and its shaped text and icon data, add a 'symbol * instance' for each _possible_ placement of the symbol feature. * (At render timePlaceSymbols#place() selects which of these instances to * show or hide based on collisions with symbols in other layers.) * @private */ function addFeature(bucket: SymbolBucket, feature: SymbolFeature, shapedTextOrientations: any, shapedIcon: PositionedIcon | void, imageMap: {[_: string]: StyleImage}, sizes: Sizes, layoutTextSize: number, layoutIconSize: number, textOffset: [number, number], isSDFIcon: boolean, canonical: CanonicalTileID) { // To reduce the number of labels that jump around when zooming we need // to use a text-size value that is the same for all zoom levels. // bucket calculates text-size at a high zoom level so that all tiles can // use the same value when calculating anchor positions. let textMaxSize = sizes.textMaxSize.evaluate(feature, {}); if (textMaxSize === undefined) { textMaxSize = layoutTextSize; } const layout = bucket.layers[0].layout; const iconOffset = layout.get('icon-offset').evaluate(feature, {}, canonical); const defaultHorizontalShaping = getDefaultHorizontalShaping(shapedTextOrientations.horizontal); const glyphSize = 24, fontScale = layoutTextSize / glyphSize, textBoxScale = bucket.tilePixelRatio * fontScale, textMaxBoxScale = bucket.tilePixelRatio * textMaxSize / glyphSize, iconBoxScale = bucket.tilePixelRatio * layoutIconSize, symbolMinDistance = bucket.tilePixelRatio * layout.get('symbol-spacing'), textPadding = layout.get('text-padding') * bucket.tilePixelRatio, iconPadding = layout.get('icon-padding') * bucket.tilePixelRatio, textMaxAngle = layout.get('text-max-angle') / 180 * Math.PI, textAlongLine = layout.get('text-rotation-alignment') === 'map' && layout.get('symbol-placement') !== 'point', iconAlongLine = layout.get('icon-rotation-alignment') === 'map' && layout.get('symbol-placement') !== 'point', symbolPlacement = layout.get('symbol-placement'), textRepeatDistance = symbolMinDistance / 2; const iconTextFit = layout.get('icon-text-fit'); let verticallyShapedIcon; // Adjust shaped icon size when icon-text-fit is used. if (shapedIcon && iconTextFit !== 'none') { if (bucket.allowVerticalPlacement && shapedTextOrientations.vertical) { verticallyShapedIcon = fitIconToText(shapedIcon, shapedTextOrientations.vertical, iconTextFit, layout.get('icon-text-fit-padding'), iconOffset, fontScale); } if (defaultHorizontalShaping) { shapedIcon = fitIconToText(shapedIcon, defaultHorizontalShaping, iconTextFit, layout.get('icon-text-fit-padding'), iconOffset, fontScale); } } const addSymbolAtAnchor = (line, anchor) => { if (anchor.x < 0 || anchor.x >= EXTENT || anchor.y < 0 || anchor.y >= EXTENT) { // Symbol layers are drawn across tile boundaries, We filter out symbols // outside our tile boundaries (which may be included in vector tile buffers) // to prevent double-drawing symbols. return; } addSymbol(bucket, anchor, line, shapedTextOrientations, shapedIcon, imageMap, verticallyShapedIcon, bucket.layers[0], bucket.collisionBoxArray, feature.index, feature.sourceLayerIndex, bucket.index, textBoxScale, textPadding, textAlongLine, textOffset, iconBoxScale, iconPadding, iconAlongLine, iconOffset, feature, sizes, isSDFIcon, canonical, layoutTextSize); }; if (symbolPlacement === 'line') { for (const line of clipLine(feature.geometry, 0, 0, EXTENT, EXTENT)) { const anchors = getAnchors( line, symbolMinDistance, textMaxAngle, shapedTextOrientations.vertical || defaultHorizontalShaping, shapedIcon, glyphSize, textMaxBoxScale, bucket.overscaling, EXTENT ); for (const anchor of anchors) { const shapedText = defaultHorizontalShaping; if (!shapedText || !anchorIsTooClose(bucket, shapedText.text, textRepeatDistance, anchor)) { addSymbolAtAnchor(line, anchor); } } } } else if (symbolPlacement === 'line-center') { // No clipping, multiple lines per feature are allowed // "lines" with only one point are ignored as in clipLines for (const line of feature.geometry) { if (line.length > 1) { const anchor = getCenterAnchor( line, textMaxAngle, shapedTextOrientations.vertical || defaultHorizontalShaping, shapedIcon, glyphSize, textMaxBoxScale); if (anchor) { addSymbolAtAnchor(line, anchor); } } } } else if (feature.type === 'Polygon') { for (const polygon of classifyRings(feature.geometry, 0)) { // 16 here represents 2 pixels const poi = findPoleOfInaccessibility(polygon, 16); addSymbolAtAnchor(polygon[0], new Anchor(poi.x, poi.y, 0)); } } else if (feature.type === 'LineString') { // https://github.com/mapbox/mapbox-gl-js/issues/3808 for (const line of feature.geometry) { addSymbolAtAnchor(line, new Anchor(line[0].x, line[0].y, 0)); } } else if (feature.type === 'Point') { for (const points of feature.geometry) { for (const point of points) { addSymbolAtAnchor([point], new Anchor(point.x, point.y, 0)); } } } } const MAX_GLYPH_ICON_SIZE = 255; const MAX_PACKED_SIZE = MAX_GLYPH_ICON_SIZE * SIZE_PACK_FACTOR; export {MAX_PACKED_SIZE}; function addTextVertices(bucket: SymbolBucket, anchor: Point, shapedText: Shaping, imageMap: {[_: string]: StyleImage}, layer: SymbolStyleLayer, textAlongLine: boolean, feature: SymbolFeature, textOffset: [number, number], lineArray: {lineStartIndex: number, lineLength: number}, writingMode: number, placementTypes: Array<'vertical' | 'center' | 'left' | 'right'>, placedTextSymbolIndices: {[_: string]: number}, placedIconIndex: number, sizes: Sizes, canonical: CanonicalTileID) { const glyphQuads = getGlyphQuads(anchor, shapedText, textOffset, layer, textAlongLine, feature, imageMap, bucket.allowVerticalPlacement); const sizeData = bucket.textSizeData; let textSizeData = null; if (sizeData.kind === 'source') { textSizeData = [ SIZE_PACK_FACTOR * layer.layout.get('text-size').evaluate(feature, {}) ]; if (textSizeData[0] > MAX_PACKED_SIZE) { warnOnce(`${bucket.layerIds[0]}: Value for "text-size" is >= ${MAX_GLYPH_ICON_SIZE}. Reduce your "text-size".`); } } else if (sizeData.kind === 'composite') { textSizeData = [ SIZE_PACK_FACTOR * sizes.compositeTextSizes[0].evaluate(feature, {}, canonical), SIZE_PACK_FACTOR * sizes.compositeTextSizes[1].evaluate(feature, {}, canonical) ]; if (textSizeData[0] > MAX_PACKED_SIZE || textSizeData[1] > MAX_PACKED_SIZE) { warnOnce(`${bucket.layerIds[0]}: Value for "text-size" is >= ${MAX_GLYPH_ICON_SIZE}. Reduce your "text-size".`); } } bucket.addSymbols( bucket.text, glyphQuads, textSizeData, textOffset, textAlongLine, feature, writingMode, anchor, lineArray.lineStartIndex, lineArray.lineLength, placedIconIndex, canonical); // The placedSymbolArray is used at render time in drawTileSymbols // These indices allow access to the array at collision detection time for (const placementType of placementTypes) { placedTextSymbolIndices[placementType] = bucket.text.placedSymbolArray.length - 1; } return glyphQuads.length * 4; } function getDefaultHorizontalShaping(horizontalShaping: {[_: TextJustify]: Shaping}): Shaping | null { // We don't care which shaping we get because this is used for collision purposes // and all the justifications have the same collision box for (const justification: any in horizontalShaping) { return horizontalShaping[justification]; } return null; } /** * Add a single label & icon placement. * * @private */ function addSymbol(bucket: SymbolBucket, anchor: Anchor, line: Array, shapedTextOrientations: any, shapedIcon: PositionedIcon | void, imageMap: {[_: string]: StyleImage}, verticallyShapedIcon: PositionedIcon | void, layer: SymbolStyleLayer, collisionBoxArray: CollisionBoxArray, featureIndex: number, sourceLayerIndex: number, bucketIndex: number, textBoxScale: number, textPadding: number, textAlongLine: boolean, textOffset: [number, number], iconBoxScale: number, iconPadding: number, iconAlongLine: boolean, iconOffset: [number, number], feature: SymbolFeature, sizes: Sizes, isSDFIcon: boolean, canonical: CanonicalTileID, layoutTextSize: number) { const lineArray = bucket.addToLineVertexArray(anchor, line); let textCollisionFeature, iconCollisionFeature, verticalTextCollisionFeature, verticalIconCollisionFeature; let numIconVertices = 0; let numVerticalIconVertices = 0; let numHorizontalGlyphVertices = 0; let numVerticalGlyphVertices = 0; let placedIconSymbolIndex = -1; let verticalPlacedIconSymbolIndex = -1; const placedTextSymbolIndices = {}; let key = murmur3(''); let textOffset0 = 0; let textOffset1 = 0; if (layer._unevaluatedLayout.getValue('text-radial-offset') === undefined) { [textOffset0, textOffset1] = (layer.layout.get('text-offset').evaluate(feature, {}, canonical).map(t => t * ONE_EM): any); } else { textOffset0 = layer.layout.get('text-radial-offset').evaluate(feature, {}, canonical) * ONE_EM; textOffset1 = INVALID_TEXT_OFFSET; } if (bucket.allowVerticalPlacement && shapedTextOrientations.vertical) { const textRotation = layer.layout.get('text-rotate').evaluate(feature, {}, canonical); const verticalTextRotation = textRotation + 90.0; const verticalShaping = shapedTextOrientations.vertical; verticalTextCollisionFeature = new CollisionFeature(collisionBoxArray, anchor, featureIndex, sourceLayerIndex, bucketIndex, verticalShaping, textBoxScale, textPadding, textAlongLine, verticalTextRotation); if (verticallyShapedIcon) { verticalIconCollisionFeature = new CollisionFeature(collisionBoxArray, anchor, featureIndex, sourceLayerIndex, bucketIndex, verticallyShapedIcon, iconBoxScale, iconPadding, textAlongLine, verticalTextRotation); } } //Place icon first, so text can have a reference to its index in the placed symbol array. //Text symbols can lazily shift at render-time because of variable anchor placement. //If the style specifies an `icon-text-fit` then the icon would have to shift along with it. // For more info check `updateVariableAnchors` in `draw_symbol.js` . if (shapedIcon) { const iconRotate = layer.layout.get('icon-rotate').evaluate(feature, {}); const hasIconTextFit = layer.layout.get('icon-text-fit') !== 'none'; const iconQuads = getIconQuads(shapedIcon, iconRotate, isSDFIcon, hasIconTextFit); const verticalIconQuads = verticallyShapedIcon ? getIconQuads(verticallyShapedIcon, iconRotate, isSDFIcon, hasIconTextFit) : undefined; iconCollisionFeature = new CollisionFeature(collisionBoxArray, anchor, featureIndex, sourceLayerIndex, bucketIndex, shapedIcon, iconBoxScale, iconPadding, /*align boxes to line*/false, iconRotate); numIconVertices = iconQuads.length * 4; const sizeData = bucket.iconSizeData; let iconSizeData = null; if (sizeData.kind === 'source') { iconSizeData = [ SIZE_PACK_FACTOR * layer.layout.get('icon-size').evaluate(feature, {}) ]; if (iconSizeData[0] > MAX_PACKED_SIZE) { warnOnce(`${bucket.layerIds[0]}: Value for "icon-size" is >= ${MAX_GLYPH_ICON_SIZE}. Reduce your "icon-size".`); } } else if (sizeData.kind === 'composite') { iconSizeData = [ SIZE_PACK_FACTOR * sizes.compositeIconSizes[0].evaluate(feature, {}, canonical), SIZE_PACK_FACTOR * sizes.compositeIconSizes[1].evaluate(feature, {}, canonical) ]; if (iconSizeData[0] > MAX_PACKED_SIZE || iconSizeData[1] > MAX_PACKED_SIZE) { warnOnce(`${bucket.layerIds[0]}: Value for "icon-size" is >= ${MAX_GLYPH_ICON_SIZE}. Reduce your "icon-size".`); } } bucket.addSymbols( bucket.icon, iconQuads, iconSizeData, iconOffset, iconAlongLine, feature, false, anchor, lineArray.lineStartIndex, lineArray.lineLength, // The icon itself does not have an associated symbol since the text isnt placed yet -1, canonical); placedIconSymbolIndex = bucket.icon.placedSymbolArray.length - 1; if (verticalIconQuads) { numVerticalIconVertices = verticalIconQuads.length * 4; bucket.addSymbols( bucket.icon, verticalIconQuads, iconSizeData, iconOffset, iconAlongLine, feature, WritingMode.vertical, anchor, lineArray.lineStartIndex, lineArray.lineLength, // The icon itself does not have an associated symbol since the text isnt placed yet -1, canonical); verticalPlacedIconSymbolIndex = bucket.icon.placedSymbolArray.length - 1; } } for (const justification: any in shapedTextOrientations.horizontal) { const shaping = shapedTextOrientations.horizontal[justification]; if (!textCollisionFeature) { key = murmur3(shaping.text); const textRotate = layer.layout.get('text-rotate').evaluate(feature, {}, canonical); // As a collision approximation, we can use either the vertical or any of the horizontal versions of the feature // We're counting on all versions having similar dimensions textCollisionFeature = new CollisionFeature(collisionBoxArray, anchor, featureIndex, sourceLayerIndex, bucketIndex, shaping, textBoxScale, textPadding, textAlongLine, textRotate); } const singleLine = shaping.positionedLines.length === 1; numHorizontalGlyphVertices += addTextVertices( bucket, anchor, shaping, imageMap, layer, textAlongLine, feature, textOffset, lineArray, shapedTextOrientations.vertical ? WritingMode.horizontal : WritingMode.horizontalOnly, singleLine ? (Object.keys(shapedTextOrientations.horizontal): any) : [justification], placedTextSymbolIndices, placedIconSymbolIndex, sizes, canonical); if (singleLine) { break; } } if (shapedTextOrientations.vertical) { numVerticalGlyphVertices += addTextVertices( bucket, anchor, shapedTextOrientations.vertical, imageMap, layer, textAlongLine, feature, textOffset, lineArray, WritingMode.vertical, ['vertical'], placedTextSymbolIndices, verticalPlacedIconSymbolIndex, sizes, canonical); } const textBoxStartIndex = textCollisionFeature ? textCollisionFeature.boxStartIndex : bucket.collisionBoxArray.length; const textBoxEndIndex = textCollisionFeature ? textCollisionFeature.boxEndIndex : bucket.collisionBoxArray.length; const verticalTextBoxStartIndex = verticalTextCollisionFeature ? verticalTextCollisionFeature.boxStartIndex : bucket.collisionBoxArray.length; const verticalTextBoxEndIndex = verticalTextCollisionFeature ? verticalTextCollisionFeature.boxEndIndex : bucket.collisionBoxArray.length; const iconBoxStartIndex = iconCollisionFeature ? iconCollisionFeature.boxStartIndex : bucket.collisionBoxArray.length; const iconBoxEndIndex = iconCollisionFeature ? iconCollisionFeature.boxEndIndex : bucket.collisionBoxArray.length; const verticalIconBoxStartIndex = verticalIconCollisionFeature ? verticalIconCollisionFeature.boxStartIndex : bucket.collisionBoxArray.length; const verticalIconBoxEndIndex = verticalIconCollisionFeature ? verticalIconCollisionFeature.boxEndIndex : bucket.collisionBoxArray.length; // Check if runtime collision circles should be used for any of the collision features. // It is enough to choose the tallest feature shape as circles are always placed on a line. // All measurements are in glyph metrics and later converted into pixels using proper font size "layoutTextSize" let collisionCircleDiameter = -1; const getCollisionCircleHeight = (feature: ?CollisionFeature, prevHeight: number): number => { if (feature && feature.circleDiameter) return Math.max(feature.circleDiameter, prevHeight); return prevHeight; }; collisionCircleDiameter = getCollisionCircleHeight(textCollisionFeature, collisionCircleDiameter); collisionCircleDiameter = getCollisionCircleHeight(verticalTextCollisionFeature, collisionCircleDiameter); collisionCircleDiameter = getCollisionCircleHeight(iconCollisionFeature, collisionCircleDiameter); collisionCircleDiameter = getCollisionCircleHeight(verticalIconCollisionFeature, collisionCircleDiameter); const useRuntimeCollisionCircles = (collisionCircleDiameter > -1) ? 1 : 0; // Convert circle collision height into pixels if (useRuntimeCollisionCircles) collisionCircleDiameter *= layoutTextSize / ONE_EM; if (bucket.glyphOffsetArray.length >= SymbolBucket.MAX_GLYPHS) warnOnce( "Too many glyphs being rendered in a tile. See https://github.com/mapbox/mapbox-gl-js/issues/2907" ); if (feature.sortKey !== undefined) { bucket.addToSortKeyRanges(bucket.symbolInstances.length, feature.sortKey); } bucket.symbolInstances.emplaceBack( anchor.x, anchor.y, placedTextSymbolIndices.right >= 0 ? placedTextSymbolIndices.right : -1, placedTextSymbolIndices.center >= 0 ? placedTextSymbolIndices.center : -1, placedTextSymbolIndices.left >= 0 ? placedTextSymbolIndices.left : -1, placedTextSymbolIndices.vertical || -1, placedIconSymbolIndex, verticalPlacedIconSymbolIndex, key, textBoxStartIndex, textBoxEndIndex, verticalTextBoxStartIndex, verticalTextBoxEndIndex, iconBoxStartIndex, iconBoxEndIndex, verticalIconBoxStartIndex, verticalIconBoxEndIndex, featureIndex, numHorizontalGlyphVertices, numVerticalGlyphVertices, numIconVertices, numVerticalIconVertices, useRuntimeCollisionCircles, 0, textBoxScale, textOffset0, textOffset1, collisionCircleDiameter); } function anchorIsTooClose(bucket: any, text: string, repeatDistance: number, anchor: Point) { const compareText = bucket.compareText; if (!(text in compareText)) { compareText[text] = []; } else { const otherAnchors = compareText[text]; for (let k = otherAnchors.length - 1; k >= 0; k--) { if (anchor.dist(otherAnchors[k]) < repeatDistance) { // If it's within repeatDistance of one anchor, stop looking return true; } } } // If anchor is not within repeatDistance of any other anchor, add to array compareText[text].push(anchor); return false; }