import { symbolLayoutAttributes, collisionVertexAttributes, collisionBoxLayout, dynamicLayoutAttributes, } from './symbol_attributes'; import {SymbolLayoutArray, SymbolDynamicLayoutArray, SymbolOpacityArray, CollisionBoxLayoutArray, CollisionVertexArray, PlacedSymbolArray, SymbolInstanceArray, GlyphOffsetArray, SymbolLineVertexArray, TextAnchorOffsetArray } from '../array_types.g'; import Point from '@mapbox/point-geometry'; import {SegmentVector} from '../segment'; import {ProgramConfigurationSet} from '../program_configuration'; import {TriangleIndexArray, LineIndexArray} from '../index_array_type'; import {transformText} from '../../symbol/transform_text'; import {mergeLines} from '../../symbol/merge_lines'; import {allowsVerticalWritingMode, stringContainsRTLText} from '../../util/script_detection'; import {WritingMode} from '../../symbol/shaping'; import {loadGeometry} from '../load_geometry'; import {toEvaluationFeature} from '../evaluation_feature'; import mvt from '@mapbox/vector-tile'; const vectorTileFeatureTypes = mvt.VectorTileFeature.types; import {verticalizedCharacterMap} from '../../util/verticalize_punctuation'; import {Anchor} from '../../symbol/anchor'; import {getSizeData, MAX_PACKED_SIZE} from '../../symbol/symbol_size'; import {register} from '../../util/web_worker_transfer'; import {EvaluationParameters} from '../../style/evaluation_parameters'; import {Formatted, ResolvedImage} from '@maplibre/maplibre-gl-style-spec'; import {rtlWorkerPlugin} from '../../source/rtl_text_plugin_worker'; import {mat4} from 'gl-matrix'; import {getOverlapMode} from '../../style/style_layer/overlap_mode'; import type {CanonicalTileID} from '../../source/tile_id'; import type { Bucket, BucketParameters, IndexedFeature, PopulateParameters } from '../bucket'; import type {CollisionBoxArray, CollisionBox, SymbolInstance} from '../array_types.g'; import type {StructArray, StructArrayMember, ViewType} from '../../util/struct_array'; import type {SymbolStyleLayer} from '../../style/style_layer/symbol_style_layer'; import type {Context} from '../../gl/context'; import type {IndexBuffer} from '../../gl/index_buffer'; import type {VertexBuffer} from '../../gl/vertex_buffer'; import type {SymbolQuad} from '../../symbol/quads'; import type {SizeData} from '../../symbol/symbol_size'; import type {FeatureStates} from '../../source/source_state'; import type {ImagePosition} from '../../render/image_atlas'; import type {VectorTileLayer} from '@mapbox/vector-tile'; export type SingleCollisionBox = { x1: number; y1: number; x2: number; y2: number; anchorPointX: number; anchorPointY: number; }; export type CollisionArrays = { textBox?: SingleCollisionBox; verticalTextBox?: SingleCollisionBox; iconBox?: SingleCollisionBox; verticalIconBox?: SingleCollisionBox; textFeatureIndex?: number; verticalTextFeatureIndex?: number; iconFeatureIndex?: number; verticalIconFeatureIndex?: number; }; export type SymbolFeature = { sortKey: number | void; text: Formatted | void; icon: ResolvedImage; index: number; sourceLayerIndex: number; geometry: Array>; properties: any; type: 'Unknown' | 'Point' | 'LineString' | 'Polygon'; id?: any; }; export type SortKeyRange = { sortKey: number; symbolInstanceStart: number; symbolInstanceEnd: number; }; // Opacity arrays are frequently updated but don't contain a lot of information, so we pack them // tight. Each Uint32 is actually four duplicate Uint8s for the four corners of a glyph // 7 bits are for the current opacity, and the lowest bit is the target opacity // actually defined in symbol_attributes.js // const placementOpacityAttributes = [ // { name: 'a_fade_opacity', components: 1, type: 'Uint32' } // ]; const shaderOpacityAttributes = [ {name: 'a_fade_opacity', components: 1, type: 'Uint8' as ViewType, offset: 0} ]; function addVertex( array: StructArray, anchorX: number, anchorY: number, ox: number, oy: number, tx: number, ty: number, sizeVertex: number, isSDF: boolean, pixelOffsetX: number, pixelOffsetY: number, minFontScaleX: number, minFontScaleY: number ) { const aSizeX = sizeVertex ? Math.min(MAX_PACKED_SIZE, Math.round(sizeVertex[0])) : 0; const aSizeY = sizeVertex ? Math.min(MAX_PACKED_SIZE, Math.round(sizeVertex[1])) : 0; array.emplaceBack( // a_pos_offset anchorX, anchorY, Math.round(ox * 32), Math.round(oy * 32), // a_data tx, // x coordinate of symbol on glyph atlas texture ty, // y coordinate of symbol on glyph atlas texture (aSizeX << 1) + (isSDF ? 1 : 0), aSizeY, pixelOffsetX * 16, pixelOffsetY * 16, minFontScaleX * 256, minFontScaleY * 256 ); } function addDynamicAttributes(dynamicLayoutVertexArray: StructArray, p: Point, angle: number) { dynamicLayoutVertexArray.emplaceBack(p.x, p.y, angle); dynamicLayoutVertexArray.emplaceBack(p.x, p.y, angle); dynamicLayoutVertexArray.emplaceBack(p.x, p.y, angle); dynamicLayoutVertexArray.emplaceBack(p.x, p.y, angle); } function containsRTLText(formattedText: Formatted): boolean { for (const section of formattedText.sections) { if (stringContainsRTLText(section.text)) { return true; } } return false; } export class SymbolBuffers { layoutVertexArray: SymbolLayoutArray; layoutVertexBuffer: VertexBuffer; indexArray: TriangleIndexArray; indexBuffer: IndexBuffer; programConfigurations: ProgramConfigurationSet; segments: SegmentVector; dynamicLayoutVertexArray: SymbolDynamicLayoutArray; dynamicLayoutVertexBuffer: VertexBuffer; opacityVertexArray: SymbolOpacityArray; opacityVertexBuffer: VertexBuffer; hasVisibleVertices: boolean; collisionVertexArray: CollisionVertexArray; collisionVertexBuffer: VertexBuffer; placedSymbolArray: PlacedSymbolArray; constructor(programConfigurations: ProgramConfigurationSet) { this.layoutVertexArray = new SymbolLayoutArray(); this.indexArray = new TriangleIndexArray(); this.programConfigurations = programConfigurations; this.segments = new SegmentVector(); this.dynamicLayoutVertexArray = new SymbolDynamicLayoutArray(); this.opacityVertexArray = new SymbolOpacityArray(); this.hasVisibleVertices = false; this.placedSymbolArray = new PlacedSymbolArray(); } isEmpty() { return this.layoutVertexArray.length === 0 && this.indexArray.length === 0 && this.dynamicLayoutVertexArray.length === 0 && this.opacityVertexArray.length === 0; } upload(context: Context, dynamicIndexBuffer: boolean, upload?: boolean, update?: boolean) { if (this.isEmpty()) { return; } if (upload) { this.layoutVertexBuffer = context.createVertexBuffer(this.layoutVertexArray, symbolLayoutAttributes.members); this.indexBuffer = context.createIndexBuffer(this.indexArray, dynamicIndexBuffer); this.dynamicLayoutVertexBuffer = context.createVertexBuffer(this.dynamicLayoutVertexArray, dynamicLayoutAttributes.members, true); this.opacityVertexBuffer = context.createVertexBuffer(this.opacityVertexArray, shaderOpacityAttributes, true); // This is a performance hack so that we can write to opacityVertexArray with uint32s // even though the shaders read uint8s this.opacityVertexBuffer.itemSize = 1; } if (upload || update) { this.programConfigurations.upload(context); } } destroy() { if (!this.layoutVertexBuffer) return; this.layoutVertexBuffer.destroy(); this.indexBuffer.destroy(); this.programConfigurations.destroy(); this.segments.destroy(); this.dynamicLayoutVertexBuffer.destroy(); this.opacityVertexBuffer.destroy(); } } register('SymbolBuffers', SymbolBuffers); class CollisionBuffers { layoutVertexArray: StructArray; layoutAttributes: Array; layoutVertexBuffer: VertexBuffer; indexArray: TriangleIndexArray | LineIndexArray; indexBuffer: IndexBuffer; segments: SegmentVector; collisionVertexArray: CollisionVertexArray; collisionVertexBuffer: VertexBuffer; constructor(LayoutArray: { new (...args: any): StructArray; }, layoutAttributes: Array, IndexArray: { new (...args: any): TriangleIndexArray | LineIndexArray; }) { this.layoutVertexArray = new LayoutArray(); this.layoutAttributes = layoutAttributes; this.indexArray = new IndexArray(); this.segments = new SegmentVector(); this.collisionVertexArray = new CollisionVertexArray(); } upload(context: Context) { this.layoutVertexBuffer = context.createVertexBuffer(this.layoutVertexArray, this.layoutAttributes); this.indexBuffer = context.createIndexBuffer(this.indexArray); this.collisionVertexBuffer = context.createVertexBuffer(this.collisionVertexArray, collisionVertexAttributes.members, true); } destroy() { if (!this.layoutVertexBuffer) return; this.layoutVertexBuffer.destroy(); this.indexBuffer.destroy(); this.segments.destroy(); this.collisionVertexBuffer.destroy(); } } register('CollisionBuffers', CollisionBuffers); /** * @internal * Unlike other buckets, which simply implement #addFeature with type-specific * logic for (essentially) triangulating feature geometries, SymbolBucket * requires specialized behavior: * * 1. WorkerTile#parse(), the logical owner of the bucket creation process, * calls SymbolBucket#populate(), which resolves text and icon tokens on * each feature, adds each glyphs and symbols needed to the passed-in * collections options.glyphDependencies and options.iconDependencies, and * stores the feature data for use in subsequent step (this.features). * * 2. WorkerTile asynchronously requests from the main thread all of the glyphs * and icons needed (by this bucket and any others). When glyphs and icons * have been received, the WorkerTile creates a CollisionIndex and invokes: * * 3. performSymbolLayout(bucket, stacks, icons) perform texts shaping and * layout on a Symbol Bucket. This step populates: * `this.symbolInstances`: metadata on generated symbols * `this.collisionBoxArray`: collision data for use by foreground * `this.text`: SymbolBuffers for text symbols * `this.icons`: SymbolBuffers for icons * `this.iconCollisionBox`: Debug SymbolBuffers for icon collision boxes * `this.textCollisionBox`: Debug SymbolBuffers for text collision boxes * The results are sent to the foreground for rendering * * 4. placement.ts is run on the foreground, * and uses the CollisionIndex along with current camera settings to determine * which symbols can actually show on the map. Collided symbols are hidden * using a dynamic "OpacityVertexArray". */ export class SymbolBucket implements Bucket { static MAX_GLYPHS: number; static addDynamicAttributes: typeof addDynamicAttributes; collisionBoxArray: CollisionBoxArray; zoom: number; overscaling: number; layers: Array; layerIds: Array; stateDependentLayers: Array; stateDependentLayerIds: Array; index: number; sdfIcons: boolean; iconsInText: boolean; iconsNeedLinear: boolean; bucketInstanceId: number; justReloaded: boolean; hasPattern: boolean; textSizeData: SizeData; iconSizeData: SizeData; glyphOffsetArray: GlyphOffsetArray; lineVertexArray: SymbolLineVertexArray; features: Array; symbolInstances: SymbolInstanceArray; textAnchorOffsets: TextAnchorOffsetArray; collisionArrays: Array; sortKeyRanges: Array; pixelRatio: number; tilePixelRatio: number; compareText: {[_: string]: Array}; fadeStartTime: number; sortFeaturesByKey: boolean; sortFeaturesByY: boolean; canOverlap: boolean; sortedAngle: number; featureSortOrder: Array; collisionCircleArray: Array; placementInvProjMatrix: mat4; placementViewportMatrix: mat4; text: SymbolBuffers; icon: SymbolBuffers; textCollisionBox: CollisionBuffers; iconCollisionBox: CollisionBuffers; uploaded: boolean; sourceLayerIndex: number; sourceID: string; symbolInstanceIndexes: Array; writingModes: WritingMode[]; allowVerticalPlacement: boolean; hasRTLText: boolean; constructor(options: BucketParameters) { this.collisionBoxArray = options.collisionBoxArray; this.zoom = options.zoom; this.overscaling = options.overscaling; this.layers = options.layers; this.layerIds = this.layers.map(layer => layer.id); this.index = options.index; this.pixelRatio = options.pixelRatio; this.sourceLayerIndex = options.sourceLayerIndex; this.hasPattern = false; this.hasRTLText = false; this.sortKeyRanges = []; this.collisionCircleArray = []; this.placementInvProjMatrix = mat4.identity([] as any); this.placementViewportMatrix = mat4.identity([] as any); const layer = this.layers[0]; const unevaluatedLayoutValues = layer._unevaluatedLayout._values; this.textSizeData = getSizeData(this.zoom, unevaluatedLayoutValues['text-size']); this.iconSizeData = getSizeData(this.zoom, unevaluatedLayoutValues['icon-size']); const layout = this.layers[0].layout; const sortKey = layout.get('symbol-sort-key'); const zOrder = layout.get('symbol-z-order'); this.canOverlap = getOverlapMode(layout, 'text-overlap', 'text-allow-overlap') !== 'never' || getOverlapMode(layout, 'icon-overlap', 'icon-allow-overlap') !== 'never' || layout.get('text-ignore-placement') || layout.get('icon-ignore-placement'); this.sortFeaturesByKey = zOrder !== 'viewport-y' && !sortKey.isConstant(); const zOrderByViewportY = zOrder === 'viewport-y' || (zOrder === 'auto' && !this.sortFeaturesByKey); this.sortFeaturesByY = zOrderByViewportY && this.canOverlap; if (layout.get('symbol-placement') === 'point') { this.writingModes = layout.get('text-writing-mode').map(wm => WritingMode[wm]); } this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id); this.sourceID = options.sourceID; } createArrays() { this.text = new SymbolBuffers(new ProgramConfigurationSet(this.layers, this.zoom, property => /^text/.test(property))); this.icon = new SymbolBuffers(new ProgramConfigurationSet(this.layers, this.zoom, property => /^icon/.test(property))); this.glyphOffsetArray = new GlyphOffsetArray(); this.lineVertexArray = new SymbolLineVertexArray(); this.symbolInstances = new SymbolInstanceArray(); this.textAnchorOffsets = new TextAnchorOffsetArray(); } private calculateGlyphDependencies( text: string, stack: {[_: number]: boolean}, textAlongLine: boolean, allowVerticalPlacement: boolean, doesAllowVerticalWritingMode: boolean) { for (let i = 0; i < text.length; i++) { stack[text.charCodeAt(i)] = true; if ((textAlongLine || allowVerticalPlacement) && doesAllowVerticalWritingMode) { const verticalChar = verticalizedCharacterMap[text.charAt(i)]; if (verticalChar) { stack[verticalChar.charCodeAt(0)] = true; } } } } populate(features: Array, options: PopulateParameters, canonical: CanonicalTileID) { const layer = this.layers[0]; const layout = layer.layout; const textFont = layout.get('text-font'); const textField = layout.get('text-field'); const iconImage = layout.get('icon-image'); const hasText = (textField.value.kind !== 'constant' || (textField.value.value instanceof Formatted && !textField.value.value.isEmpty()) || textField.value.value.toString().length > 0) && (textFont.value.kind !== 'constant' || textFont.value.value.length > 0); // we should always resolve the icon-image value if the property was defined in the style // this allows us to fire the styleimagemissing event if image evaluation returns null // the only way to distinguish between null returned from a coalesce statement with no valid images // and null returned because icon-image wasn't defined is to check whether or not iconImage.parameters is an empty object const hasIcon = iconImage.value.kind !== 'constant' || !!iconImage.value.value || Object.keys(iconImage.parameters).length > 0; const symbolSortKey = layout.get('symbol-sort-key'); this.features = []; if (!hasText && !hasIcon) { return; } const icons = options.iconDependencies; const stacks = options.glyphDependencies; const availableImages = options.availableImages; const globalProperties = new EvaluationParameters(this.zoom); for (const {feature, id, index, sourceLayerIndex} of features) { const needGeometry = layer._featureFilter.needGeometry; const evaluationFeature = toEvaluationFeature(feature, needGeometry); if (!layer._featureFilter.filter(globalProperties, evaluationFeature, canonical)) { continue; } if (!needGeometry) evaluationFeature.geometry = loadGeometry(feature); let text: Formatted | void; if (hasText) { // Expression evaluation will automatically coerce to Formatted // but plain string token evaluation skips that pathway so do the // conversion here. const resolvedTokens = layer.getValueAndResolveTokens('text-field', evaluationFeature, canonical, availableImages); const formattedText = Formatted.factory(resolvedTokens); // on this instance: if hasRTLText is already true, all future calls to containsRTLText can be skipped. const bucketHasRTLText = this.hasRTLText = (this.hasRTLText || containsRTLText(formattedText)); if ( !bucketHasRTLText || // non-rtl text so can proceed safely rtlWorkerPlugin.getRTLTextPluginStatus() === 'unavailable' || // We don't intend to lazy-load the rtl text plugin, so proceed with incorrect shaping bucketHasRTLText && rtlWorkerPlugin.isParsed() // Use the rtlText plugin to shape text ) { text = transformText(formattedText, layer, evaluationFeature); } } let icon: ResolvedImage; if (hasIcon) { // Expression evaluation will automatically coerce to Image // but plain string token evaluation skips that pathway so do the // conversion here. const resolvedTokens = layer.getValueAndResolveTokens('icon-image', evaluationFeature, canonical, availableImages); if (resolvedTokens instanceof ResolvedImage) { icon = resolvedTokens; } else { icon = ResolvedImage.fromString(resolvedTokens); } } if (!text && !icon) { continue; } const sortKey = this.sortFeaturesByKey ? symbolSortKey.evaluate(evaluationFeature, {}, canonical) : undefined; const symbolFeature: SymbolFeature = { id, text, icon, index, sourceLayerIndex, geometry: evaluationFeature.geometry, properties: feature.properties, type: vectorTileFeatureTypes[feature.type], sortKey }; this.features.push(symbolFeature); if (icon) { icons[icon.name] = true; } if (text) { const fontStack = textFont.evaluate(evaluationFeature, {}, canonical).join(','); const textAlongLine = layout.get('text-rotation-alignment') !== 'viewport' && layout.get('symbol-placement') !== 'point'; this.allowVerticalPlacement = this.writingModes && this.writingModes.indexOf(WritingMode.vertical) >= 0; for (const section of text.sections) { if (!section.image) { const doesAllowVerticalWritingMode = allowsVerticalWritingMode(text.toString()); const sectionFont = section.fontStack || fontStack; const sectionStack = stacks[sectionFont] = stacks[sectionFont] || {}; this.calculateGlyphDependencies(section.text, sectionStack, textAlongLine, this.allowVerticalPlacement, doesAllowVerticalWritingMode); } else { // Add section image to the list of dependencies. icons[section.image.name] = true; } } } } if (layout.get('symbol-placement') === 'line') { // Merge adjacent lines with the same text to improve labelling. // It's better to place labels on one long line than on many short segments. this.features = mergeLines(this.features); } if (this.sortFeaturesByKey) { this.features.sort((a, b) => { // a.sortKey is always a number when sortFeaturesByKey is true return (a.sortKey as number) - (b.sortKey as number); }); } } update(states: FeatureStates, vtLayer: VectorTileLayer, imagePositions: {[_: string]: ImagePosition}) { if (!this.stateDependentLayers.length) return; this.text.programConfigurations.updatePaintArrays(states, vtLayer, this.layers, imagePositions); this.icon.programConfigurations.updatePaintArrays(states, vtLayer, this.layers, imagePositions); } isEmpty() { // When the bucket encounters only rtl-text but the plugin isn't loaded, no symbol instances will be created. // In order for the bucket to be serialized, and not discarded as an empty bucket both checks are necessary. return this.symbolInstances.length === 0 && !this.hasRTLText; } uploadPending() { return !this.uploaded || this.text.programConfigurations.needsUpload || this.icon.programConfigurations.needsUpload; } upload(context: Context) { if (!this.uploaded && this.hasDebugData()) { this.textCollisionBox.upload(context); this.iconCollisionBox.upload(context); } this.text.upload(context, this.sortFeaturesByY, !this.uploaded, this.text.programConfigurations.needsUpload); this.icon.upload(context, this.sortFeaturesByY, !this.uploaded, this.icon.programConfigurations.needsUpload); this.uploaded = true; } destroyDebugData() { this.textCollisionBox.destroy(); this.iconCollisionBox.destroy(); } destroy() { this.text.destroy(); this.icon.destroy(); if (this.hasDebugData()) { this.destroyDebugData(); } } addToLineVertexArray(anchor: Anchor, line: any) { const lineStartIndex = this.lineVertexArray.length; if (anchor.segment !== undefined) { let sumForwardLength = anchor.dist(line[anchor.segment + 1]); let sumBackwardLength = anchor.dist(line[anchor.segment]); const vertices = {}; for (let i = anchor.segment + 1; i < line.length; i++) { vertices[i] = {x: line[i].x, y: line[i].y, tileUnitDistanceFromAnchor: sumForwardLength}; if (i < line.length - 1) { sumForwardLength += line[i + 1].dist(line[i]); } } for (let i = anchor.segment || 0; i >= 0; i--) { vertices[i] = {x: line[i].x, y: line[i].y, tileUnitDistanceFromAnchor: sumBackwardLength}; if (i > 0) { sumBackwardLength += line[i - 1].dist(line[i]); } } for (let i = 0; i < line.length; i++) { const vertex = vertices[i]; this.lineVertexArray.emplaceBack(vertex.x, vertex.y, vertex.tileUnitDistanceFromAnchor); } } return { lineStartIndex, lineLength: this.lineVertexArray.length - lineStartIndex }; } addSymbols(arrays: SymbolBuffers, quads: Array, sizeVertex: any, lineOffset: [number, number], alongLine: boolean, feature: SymbolFeature, writingMode: WritingMode, labelAnchor: Anchor, lineStartIndex: number, lineLength: number, associatedIconIndex: number, canonical: CanonicalTileID) { const indexArray = arrays.indexArray; const layoutVertexArray = arrays.layoutVertexArray; const segment = arrays.segments.prepareSegment(4 * quads.length, layoutVertexArray, indexArray, this.canOverlap ? feature.sortKey as number : undefined); const glyphOffsetArrayStart = this.glyphOffsetArray.length; const vertexStartIndex = segment.vertexLength; const angle = (this.allowVerticalPlacement && writingMode === WritingMode.vertical) ? Math.PI / 2 : 0; const sections = feature.text && feature.text.sections; for (let i = 0; i < quads.length; i++) { const {tl, tr, bl, br, tex, pixelOffsetTL, pixelOffsetBR, minFontScaleX, minFontScaleY, glyphOffset, isSDF, sectionIndex} = quads[i]; const index = segment.vertexLength; const y = glyphOffset[1]; addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, tl.x, y + tl.y, tex.x, tex.y, sizeVertex, isSDF, pixelOffsetTL.x, pixelOffsetTL.y, minFontScaleX, minFontScaleY); addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, tr.x, y + tr.y, tex.x + tex.w, tex.y, sizeVertex, isSDF, pixelOffsetBR.x, pixelOffsetTL.y, minFontScaleX, minFontScaleY); addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, bl.x, y + bl.y, tex.x, tex.y + tex.h, sizeVertex, isSDF, pixelOffsetTL.x, pixelOffsetBR.y, minFontScaleX, minFontScaleY); addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, br.x, y + br.y, tex.x + tex.w, tex.y + tex.h, sizeVertex, isSDF, pixelOffsetBR.x, pixelOffsetBR.y, minFontScaleX, minFontScaleY); addDynamicAttributes(arrays.dynamicLayoutVertexArray, labelAnchor, angle); indexArray.emplaceBack(index, index + 1, index + 2); indexArray.emplaceBack(index + 1, index + 2, index + 3); segment.vertexLength += 4; segment.primitiveLength += 2; this.glyphOffsetArray.emplaceBack(glyphOffset[0]); if (i === quads.length - 1 || sectionIndex !== quads[i + 1].sectionIndex) { arrays.programConfigurations.populatePaintArrays(layoutVertexArray.length, feature, feature.index, {}, canonical, sections && sections[sectionIndex]); } } arrays.placedSymbolArray.emplaceBack( labelAnchor.x, labelAnchor.y, glyphOffsetArrayStart, this.glyphOffsetArray.length - glyphOffsetArrayStart, vertexStartIndex, lineStartIndex, lineLength, labelAnchor.segment, sizeVertex ? sizeVertex[0] : 0, sizeVertex ? sizeVertex[1] : 0, lineOffset[0], lineOffset[1], writingMode, // placedOrientation is null initially; will be updated to horizontal(1)/vertical(2) if placed 0, false as unknown as number, // The crossTileID is only filled/used on the foreground for dynamic text anchors 0, associatedIconIndex ); } _addCollisionDebugVertex(layoutVertexArray: StructArray, collisionVertexArray: StructArray, point: Point, anchorX: number, anchorY: number, extrude: Point) { collisionVertexArray.emplaceBack(0, 0); return layoutVertexArray.emplaceBack( // pos point.x, point.y, // a_anchor_pos anchorX, anchorY, // extrude Math.round(extrude.x), Math.round(extrude.y)); } addCollisionDebugVertices(x1: number, y1: number, x2: number, y2: number, arrays: CollisionBuffers, boxAnchorPoint: Point, symbolInstance: SymbolInstance) { const segment = arrays.segments.prepareSegment(4, arrays.layoutVertexArray, arrays.indexArray); const index = segment.vertexLength; const layoutVertexArray = arrays.layoutVertexArray; const collisionVertexArray = arrays.collisionVertexArray; const anchorX = symbolInstance.anchorX; const anchorY = symbolInstance.anchorY; this._addCollisionDebugVertex(layoutVertexArray, collisionVertexArray, boxAnchorPoint, anchorX, anchorY, new Point(x1, y1)); this._addCollisionDebugVertex(layoutVertexArray, collisionVertexArray, boxAnchorPoint, anchorX, anchorY, new Point(x2, y1)); this._addCollisionDebugVertex(layoutVertexArray, collisionVertexArray, boxAnchorPoint, anchorX, anchorY, new Point(x2, y2)); this._addCollisionDebugVertex(layoutVertexArray, collisionVertexArray, boxAnchorPoint, anchorX, anchorY, new Point(x1, y2)); segment.vertexLength += 4; const indexArray = arrays.indexArray as LineIndexArray; indexArray.emplaceBack(index, index + 1); indexArray.emplaceBack(index + 1, index + 2); indexArray.emplaceBack(index + 2, index + 3); indexArray.emplaceBack(index + 3, index); segment.primitiveLength += 4; } addDebugCollisionBoxes(startIndex: number, endIndex: number, symbolInstance: SymbolInstance, isText: boolean) { for (let b = startIndex; b < endIndex; b++) { const box: CollisionBox = this.collisionBoxArray.get(b); const x1 = box.x1; const y1 = box.y1; const x2 = box.x2; const y2 = box.y2; this.addCollisionDebugVertices(x1, y1, x2, y2, isText ? this.textCollisionBox : this.iconCollisionBox, box.anchorPoint, symbolInstance); } } generateCollisionDebugBuffers() { if (this.hasDebugData()) { this.destroyDebugData(); } this.textCollisionBox = new CollisionBuffers(CollisionBoxLayoutArray, collisionBoxLayout.members, LineIndexArray); this.iconCollisionBox = new CollisionBuffers(CollisionBoxLayoutArray, collisionBoxLayout.members, LineIndexArray); for (let i = 0; i < this.symbolInstances.length; i++) { const symbolInstance = this.symbolInstances.get(i); this.addDebugCollisionBoxes(symbolInstance.textBoxStartIndex, symbolInstance.textBoxEndIndex, symbolInstance, true); this.addDebugCollisionBoxes(symbolInstance.verticalTextBoxStartIndex, symbolInstance.verticalTextBoxEndIndex, symbolInstance, true); this.addDebugCollisionBoxes(symbolInstance.iconBoxStartIndex, symbolInstance.iconBoxEndIndex, symbolInstance, false); this.addDebugCollisionBoxes(symbolInstance.verticalIconBoxStartIndex, symbolInstance.verticalIconBoxEndIndex, symbolInstance, false); } } // These flat arrays are meant to be quicker to iterate over than the source // CollisionBoxArray _deserializeCollisionBoxesForSymbol( collisionBoxArray: CollisionBoxArray, textStartIndex: number, textEndIndex: number, verticalTextStartIndex: number, verticalTextEndIndex: number, iconStartIndex: number, iconEndIndex: number, verticalIconStartIndex: number, verticalIconEndIndex: number ): CollisionArrays { const collisionArrays = {} as CollisionArrays; for (let k = textStartIndex; k < textEndIndex; k++) { const box: CollisionBox = collisionBoxArray.get(k); collisionArrays.textBox = {x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY}; collisionArrays.textFeatureIndex = box.featureIndex; break; // Only one box allowed per instance } for (let k = verticalTextStartIndex; k < verticalTextEndIndex; k++) { const box: CollisionBox = collisionBoxArray.get(k); collisionArrays.verticalTextBox = {x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY}; collisionArrays.verticalTextFeatureIndex = box.featureIndex; break; // Only one box allowed per instance } for (let k = iconStartIndex; k < iconEndIndex; k++) { // An icon can only have one box now, so this indexing is a bit vestigial... const box: CollisionBox = collisionBoxArray.get(k); collisionArrays.iconBox = {x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY}; collisionArrays.iconFeatureIndex = box.featureIndex; break; // Only one box allowed per instance } for (let k = verticalIconStartIndex; k < verticalIconEndIndex; k++) { // An icon can only have one box now, so this indexing is a bit vestigial... const box: CollisionBox = collisionBoxArray.get(k); collisionArrays.verticalIconBox = {x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY}; collisionArrays.verticalIconFeatureIndex = box.featureIndex; break; // Only one box allowed per instance } return collisionArrays; } deserializeCollisionBoxes(collisionBoxArray: CollisionBoxArray) { this.collisionArrays = []; for (let i = 0; i < this.symbolInstances.length; i++) { const symbolInstance = this.symbolInstances.get(i); this.collisionArrays.push(this._deserializeCollisionBoxesForSymbol( collisionBoxArray, symbolInstance.textBoxStartIndex, symbolInstance.textBoxEndIndex, symbolInstance.verticalTextBoxStartIndex, symbolInstance.verticalTextBoxEndIndex, symbolInstance.iconBoxStartIndex, symbolInstance.iconBoxEndIndex, symbolInstance.verticalIconBoxStartIndex, symbolInstance.verticalIconBoxEndIndex )); } } hasTextData() { return this.text.segments.get().length > 0; } hasIconData() { return this.icon.segments.get().length > 0; } hasDebugData() { return this.textCollisionBox && this.iconCollisionBox; } hasTextCollisionBoxData() { return this.hasDebugData() && this.textCollisionBox.segments.get().length > 0; } hasIconCollisionBoxData() { return this.hasDebugData() && this.iconCollisionBox.segments.get().length > 0; } addIndicesForPlacedSymbol(iconOrText: SymbolBuffers, placedSymbolIndex: number) { const placedSymbol = iconOrText.placedSymbolArray.get(placedSymbolIndex); const endIndex = placedSymbol.vertexStartIndex + placedSymbol.numGlyphs * 4; for (let vertexIndex = placedSymbol.vertexStartIndex; vertexIndex < endIndex; vertexIndex += 4) { iconOrText.indexArray.emplaceBack(vertexIndex, vertexIndex + 1, vertexIndex + 2); iconOrText.indexArray.emplaceBack(vertexIndex + 1, vertexIndex + 2, vertexIndex + 3); } } getSortedSymbolIndexes(angle: number) { if (this.sortedAngle === angle && this.symbolInstanceIndexes !== undefined) { return this.symbolInstanceIndexes; } const sin = Math.sin(angle); const cos = Math.cos(angle); const rotatedYs = []; const featureIndexes = []; const result = []; for (let i = 0; i < this.symbolInstances.length; ++i) { result.push(i); const symbolInstance = this.symbolInstances.get(i); rotatedYs.push(Math.round(sin * symbolInstance.anchorX + cos * symbolInstance.anchorY) | 0); featureIndexes.push(symbolInstance.featureIndex); } result.sort((aIndex, bIndex) => { return (rotatedYs[aIndex] - rotatedYs[bIndex]) || (featureIndexes[bIndex] - featureIndexes[aIndex]); }); return result; } addToSortKeyRanges(symbolInstanceIndex: number, sortKey: number) { const last = this.sortKeyRanges[this.sortKeyRanges.length - 1]; if (last && last.sortKey === sortKey) { last.symbolInstanceEnd = symbolInstanceIndex + 1; } else { this.sortKeyRanges.push({ sortKey, symbolInstanceStart: symbolInstanceIndex, symbolInstanceEnd: symbolInstanceIndex + 1 }); } } sortFeatures(angle: number) { if (!this.sortFeaturesByY) return; if (this.sortedAngle === angle) return; // The current approach to sorting doesn't sort across segments so don't try. // Sorting within segments separately seemed not to be worth the complexity. if (this.text.segments.get().length > 1 || this.icon.segments.get().length > 1) return; // If the symbols are allowed to overlap sort them by their vertical screen position. // The index array buffer is rewritten to reference the (unchanged) vertices in the // sorted order. // To avoid sorting the actual symbolInstance array we sort an array of indexes. this.symbolInstanceIndexes = this.getSortedSymbolIndexes(angle); this.sortedAngle = angle; this.text.indexArray.clear(); this.icon.indexArray.clear(); this.featureSortOrder = []; for (const i of this.symbolInstanceIndexes) { const symbolInstance = this.symbolInstances.get(i); this.featureSortOrder.push(symbolInstance.featureIndex); [ symbolInstance.rightJustifiedTextSymbolIndex, symbolInstance.centerJustifiedTextSymbolIndex, symbolInstance.leftJustifiedTextSymbolIndex ].forEach((index, i, array) => { // Only add a given index the first time it shows up, // to avoid duplicate opacity entries when multiple justifications // share the same glyphs. if (index >= 0 && array.indexOf(index) === i) { this.addIndicesForPlacedSymbol(this.text, index); } }); if (symbolInstance.verticalPlacedTextSymbolIndex >= 0) { this.addIndicesForPlacedSymbol(this.text, symbolInstance.verticalPlacedTextSymbolIndex); } if (symbolInstance.placedIconSymbolIndex >= 0) { this.addIndicesForPlacedSymbol(this.icon, symbolInstance.placedIconSymbolIndex); } if (symbolInstance.verticalPlacedIconSymbolIndex >= 0) { this.addIndicesForPlacedSymbol(this.icon, symbolInstance.verticalPlacedIconSymbolIndex); } } if (this.text.indexBuffer) this.text.indexBuffer.updateData(this.text.indexArray); if (this.icon.indexBuffer) this.icon.indexBuffer.updateData(this.icon.indexArray); } } register('SymbolBucket', SymbolBucket, { omit: ['layers', 'collisionBoxArray', 'features', 'compareText'] }); // this constant is based on the size of StructArray indexes used in a symbol // bucket--namely, glyphOffsetArrayStart // eg the max valid UInt16 is 65,535 // See https://github.com/mapbox/mapbox-gl-js/issues/2907 for motivation // lineStartIndex and textBoxStartIndex could potentially be concerns // but we expect there to be many fewer boxes/lines than glyphs SymbolBucket.MAX_GLYPHS = 65535; SymbolBucket.addDynamicAttributes = addDynamicAttributes; export {addDynamicAttributes};