UNPKG

mapbox-gl

Version:
994 lines (859 loc) 44 kB
// @flow const Point = require('point-geometry'); const ArrayGroup = require('../array_group'); const BufferGroup = require('../buffer_group'); const createElementArrayType = require('../element_array_type'); const EXTENT = require('../extent'); const packUint8ToFloat = require('../../shaders/encode_attribute').packUint8ToFloat; const Anchor = require('../../symbol/anchor'); const getAnchors = require('../../symbol/get_anchors'); const resolveTokens = require('../../util/token'); const Quads = require('../../symbol/quads'); const Shaping = require('../../symbol/shaping'); const transformText = require('../../symbol/transform_text'); const mergeLines = require('../../symbol/mergelines'); const clipLine = require('../../symbol/clip_line'); const util = require('../../util/util'); const scriptDetection = require('../../util/script_detection'); const loadGeometry = require('../load_geometry'); const CollisionFeature = require('../../symbol/collision_feature'); const findPoleOfInaccessibility = require('../../util/find_pole_of_inaccessibility'); const classifyRings = require('../../util/classify_rings'); const vectorTileFeatureTypes = require('vector-tile').VectorTileFeature.types; const createStructArrayType = require('../../util/struct_array'); const verticalizePunctuation = require('../../util/verticalize_punctuation'); const shapeText = Shaping.shapeText; const shapeIcon = Shaping.shapeIcon; const WritingMode = Shaping.WritingMode; const getGlyphQuads = Quads.getGlyphQuads; const getIconQuads = Quads.getIconQuads; import type {BucketParameters, IndexedFeature, PopulateParameters} from '../bucket'; import type {ProgramInterface} from '../program_configuration'; type SymbolBucketParameters = BucketParameters & { sdfIcons: boolean, iconsNeedLinear: boolean, fontstack: any, textSizeData: any, iconSizeData: any, placedGlyphArray: any, placedIconArray: any, glyphOffsetArray: any, lineVertexArray: any, } const PlacedSymbolArray = createStructArrayType({ members: [ { type: 'Int16', name: 'anchorX' }, { type: 'Int16', name: 'anchorY' }, { type: 'Uint16', name: 'glyphStartIndex' }, { type: 'Uint16', name: 'numGlyphs' }, { type: 'Uint32', name: 'lineStartIndex' }, { type: 'Uint32', name: 'lineLength' }, { type: 'Uint16', name: 'segment' }, { type: 'Uint16', name: 'lowerSize' }, { type: 'Uint16', name: 'upperSize' }, { type: 'Float32', name: 'lineOffsetX' }, { type: 'Float32', name: 'lineOffsetY' }, { type: 'Float32', name: 'placementZoom' }, { type: 'Uint8', name: 'vertical' } ] }); const GlyphOffsetArray = createStructArrayType({ members: [ { type: 'Float32', name: 'offsetX' } ] }); const LineVertexArray = createStructArrayType({ members: [ { type: 'Int16', name: 'x' }, { type: 'Int16', name: 'y' } ]}); const elementArrayType = createElementArrayType(); const layoutAttributes = [ {name: 'a_pos_offset', components: 4, type: 'Int16'}, {name: 'a_data', components: 4, type: 'Uint16'} ]; const dynamicLayoutAttributes = [ { name: 'a_projected_pos', components: 3, type: 'Float32' } ]; const symbolInterfaces = { glyph: { layoutAttributes: layoutAttributes, dynamicLayoutAttributes: dynamicLayoutAttributes, elementArrayType: elementArrayType, paintAttributes: [ {property: 'text-color', name: 'fill_color'}, {property: 'text-halo-color', name: 'halo_color'}, {property: 'text-halo-width', name: 'halo_width'}, {property: 'text-halo-blur', name: 'halo_blur'}, {property: 'text-opacity', name: 'opacity'} ] }, icon: { layoutAttributes: layoutAttributes, dynamicLayoutAttributes: dynamicLayoutAttributes, elementArrayType: elementArrayType, paintAttributes: [ {property: 'icon-color', name: 'fill_color'}, {property: 'icon-halo-color', name: 'halo_color'}, {property: 'icon-halo-width', name: 'halo_width'}, {property: 'icon-halo-blur', name: 'halo_blur'}, {property: 'icon-opacity', name: 'opacity'} ] }, collisionBox: { // used to render collision boxes for debugging purposes layoutAttributes: [ {name: 'a_pos', components: 2, type: 'Int16'}, {name: 'a_anchor_pos', components: 2, type: 'Int16'}, {name: 'a_extrude', components: 2, type: 'Int16'}, {name: 'a_data', components: 2, type: 'Uint8'} ], elementArrayType: createElementArrayType(2) } }; function addVertex(array, anchorX, anchorY, ox, oy, tx, ty, sizeVertex) { array.emplaceBack( // a_pos_offset anchorX, anchorY, Math.round(ox * 64), Math.round(oy * 64), // a_data tx, // x coordinate of symbol on glyph atlas texture ty, // y coordinate of symbol on glyph atlas texture sizeVertex ? sizeVertex[0] : undefined, sizeVertex ? sizeVertex[1] : undefined ); } function addDynamicAttributes(dynamicLayoutVertexArray, p, angle, placementZoom) { const twoPi = Math.PI * 2; const angleAndZoom = packUint8ToFloat( ((angle + twoPi) % twoPi) / twoPi * 255, placementZoom * 10); dynamicLayoutVertexArray.emplaceBack(p.x, p.y, angleAndZoom); dynamicLayoutVertexArray.emplaceBack(p.x, p.y, angleAndZoom); dynamicLayoutVertexArray.emplaceBack(p.x, p.y, angleAndZoom); dynamicLayoutVertexArray.emplaceBack(p.x, p.y, angleAndZoom); } function addCollisionBoxVertex(layoutVertexArray, point, anchor, extrude, maxZoom, placementZoom) { return layoutVertexArray.emplaceBack( // pos point.x, point.y, // a_anchor_pos anchor.x, anchor.y, // extrude Math.round(extrude.x), Math.round(extrude.y), // data maxZoom * 10, placementZoom * 10); } /** * 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 CollisionTile and invokes: * * 3. SymbolBucket#prepare(stacks, icons) to perform text shaping and layout, populating `this.symbolInstances` and `this.collisionBoxArray`. * * 4. SymbolBucket#place(collisionTile): taking collisions into account, decide on which labels and icons to actually draw and at which scale, populating the vertex arrays (`this.arrays.glyph`, `this.arrays.icon`) and thus completing the parsing / buffer population process. * * The reason that `prepare` and `place` are separate methods is that * `prepare`, being independent of pitch and orientation, only needs to happen * at tile load time, whereas `place` must be invoked on already-loaded tiles * when the pitch/orientation are changed. (See `redoPlacement`.) * * @private */ class SymbolBucket { static programInterfaces: { glyph: ProgramInterface, icon: ProgramInterface, collisionBox: ProgramInterface }; static MAX_INSTANCES: number; static addDynamicAttributes: any; collisionBoxArray: any; zoom: number; overscaling: number; layers: any; index: any; sdfIcons: boolean; iconsNeedLinear: boolean; fontstack: any; symbolInterfaces: any; buffers: any; textSizeData: any; iconSizeData: any; placedGlyphArray: any; placedIconArray: any; glyphOffsetArray: any; lineVertexArray: any; features: any; arrays: any; symbolInstances: any; tilePixelRatio: any; compareText: any; constructor(options: SymbolBucketParameters) { this.collisionBoxArray = options.collisionBoxArray; this.zoom = options.zoom; this.overscaling = options.overscaling; this.layers = options.layers; this.index = options.index; this.sdfIcons = options.sdfIcons; this.iconsNeedLinear = options.iconsNeedLinear; this.fontstack = options.fontstack; // Set up 'program interfaces' dynamically based on the layer's style // properties (specifically its text-size properties). const layer = this.layers[0]; this.symbolInterfaces = symbolInterfaces; // deserializing a bucket created on a worker thread if (options.arrays) { this.buffers = {}; for (const id in options.arrays) { if (options.arrays[id]) { this.buffers[id] = new BufferGroup(this.symbolInterfaces[id], options.layers, options.zoom, options.arrays[id]); } } this.textSizeData = options.textSizeData; this.iconSizeData = options.iconSizeData; this.placedGlyphArray = new PlacedSymbolArray(options.placedGlyphArray); this.placedIconArray = new PlacedSymbolArray(options.placedIconArray); this.glyphOffsetArray = new GlyphOffsetArray(options.glyphOffsetArray); this.lineVertexArray = new LineVertexArray(options.lineVertexArray); } else { this.textSizeData = getSizeData(this.zoom, layer, 'text-size'); this.iconSizeData = getSizeData(this.zoom, layer, 'icon-size'); } } populate(features: Array<IndexedFeature>, options: PopulateParameters) { const layer = this.layers[0]; const layout = layer.layout; const textFont = layout['text-font']; const hasText = (!layer.isLayoutValueFeatureConstant('text-field') || layout['text-field']) && textFont; const hasIcon = (!layer.isLayoutValueFeatureConstant('icon-image') || layout['icon-image']); this.features = []; if (!hasText && !hasIcon) { return; } const icons = options.iconDependencies; const stacks = options.glyphDependencies; const stack = stacks[textFont] = stacks[textFont] || {}; const globalProperties = {zoom: this.zoom}; for (const {feature, index, sourceLayerIndex} of features) { if (!layer.filter(feature)) { continue; } let text; if (hasText) { text = layer.getLayoutValue('text-field', globalProperties, feature.properties); if (layer.isLayoutValueFeatureConstant('text-field')) { text = resolveTokens(feature.properties, text); } text = transformText(text, layer, globalProperties, feature.properties); } let icon; if (hasIcon) { icon = layer.getLayoutValue('icon-image', globalProperties, feature.properties); if (layer.isLayoutValueFeatureConstant('icon-image')) { icon = resolveTokens(feature.properties, icon); } } if (!text && !icon) { continue; } this.features.push({ text, icon, index, sourceLayerIndex, geometry: loadGeometry(feature), properties: feature.properties, type: vectorTileFeatureTypes[feature.type] }); if (icon) { icons[icon] = true; } if (text) { const textAlongLine = layout['text-rotation-alignment'] === 'map' && layout['symbol-placement'] === 'line'; const allowsVerticalWritingMode = scriptDetection.allowsVerticalWritingMode(text); for (let i = 0; i < text.length; i++) { stack[text.charCodeAt(i)] = true; if (textAlongLine && allowsVerticalWritingMode) { const verticalChar = verticalizePunctuation.lookup[text.charAt(i)]; if (verticalChar) { stack[verticalChar.charCodeAt(0)] = true; } } } } } if (layout['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); } } isEmpty() { return this.arrays.icon.isEmpty() && this.arrays.glyph.isEmpty() && this.arrays.collisionBox.isEmpty(); } getPaintPropertyStatistics() { const statistics = {}; for (const layer of this.layers) { statistics[layer.id] = util.extend({}, this.arrays.icon.layerData[layer.id].paintPropertyStatistics, this.arrays.glyph.layerData[layer.id].paintPropertyStatistics ); } return statistics; } serialize(transferables?: Array<Transferable>) { return { zoom: this.zoom, layerIds: this.layers.map((l) => l.id), sdfIcons: this.sdfIcons, iconsNeedLinear: this.iconsNeedLinear, textSizeData: this.textSizeData, iconSizeData: this.iconSizeData, fontstack: this.fontstack, placedGlyphArray: this.placedGlyphArray.serialize(transferables), placedIconArray: this.placedIconArray.serialize(transferables), glyphOffsetArray: this.glyphOffsetArray.serialize(transferables), lineVertexArray: this.lineVertexArray.serialize(transferables), arrays: util.mapObject(this.arrays, (a) => a.isEmpty() ? null : a.serialize(transferables)) }; } destroy() { if (this.buffers) { if (this.buffers.icon) this.buffers.icon.destroy(); if (this.buffers.glyph) this.buffers.glyph.destroy(); if (this.buffers.collisionBox) this.buffers.collisionBox.destroy(); this.buffers = null; } } createArrays() { this.arrays = util.mapObject(this.symbolInterfaces, (programInterface) => { return new ArrayGroup(programInterface, this.layers, this.zoom); }); } prepare(stacks: any, icons: any) { this.symbolInstances = []; const tileSize = 512 * this.overscaling; this.tilePixelRatio = EXTENT / tileSize; this.compareText = {}; this.iconsNeedLinear = false; const layout = this.layers[0].layout; const oneEm = 24; const lineHeight = layout['text-line-height'] * oneEm; const maxWidth = layout['symbol-placement'] !== 'line' ? layout['text-max-width'] * oneEm : 0; const spacing = layout['text-letter-spacing'] * oneEm; const fontstack = this.fontstack = layout['text-font'].join(','); const textAlongLine = layout['text-rotation-alignment'] === 'map' && layout['symbol-placement'] === 'line'; for (const feature of this.features) { let shapedTextOrientations; if (feature.text) { const allowsVerticalWritingMode = scriptDetection.allowsVerticalWritingMode(feature.text); const textOffset = this.layers[0].getLayoutValue('text-offset', {zoom: this.zoom}, feature.properties).map((t)=> t * oneEm); const spacingIfAllowed = scriptDetection.allowsLetterSpacing(feature.text) ? spacing : 0; const textAnchor = this.layers[0].getLayoutValue('text-anchor', {zoom: this.zoom}, feature.properties); const textJustify = this.layers[0].getLayoutValue('text-justify', {zoom: this.zoom}, feature.properties); shapedTextOrientations = { [WritingMode.horizontal]: shapeText(feature.text, stacks[fontstack], maxWidth, lineHeight, textAnchor, textJustify, spacingIfAllowed, textOffset, oneEm, WritingMode.horizontal), [WritingMode.vertical]: allowsVerticalWritingMode && textAlongLine && shapeText(feature.text, stacks[fontstack], maxWidth, lineHeight, textAnchor, textJustify, spacingIfAllowed, textOffset, oneEm, WritingMode.vertical) }; } else { shapedTextOrientations = {}; } let shapedIcon; if (feature.icon) { const image = icons[feature.icon]; if (image) { shapedIcon = shapeIcon(image, this.layers[0].getLayoutValue('icon-offset', {zoom: this.zoom}, feature.properties)); if (this.sdfIcons === undefined) { this.sdfIcons = image.sdf; } else if (this.sdfIcons !== image.sdf) { util.warnOnce('Style sheet warning: Cannot mix SDF and non-SDF icons in one buffer'); } if (!image.isNativePixelRatio) { this.iconsNeedLinear = true; } else if (layout['icon-rotate'] !== 0 || !this.layers[0].isLayoutValueFeatureConstant('icon-rotate')) { this.iconsNeedLinear = true; } } } if (shapedTextOrientations[WritingMode.horizontal] || shapedIcon) { this.addFeature(feature, shapedTextOrientations, shapedIcon); } } } /** * Given a feature and its shaped text and icon data, add a 'symbol * instance' for each _possible_ placement of the symbol feature. * (SymbolBucket#place() selects which of these instances to send to the * renderer based on collisions with symbols in other layers from the same * source.) * @private */ addFeature(feature: any, shapedTextOrientations: any, shapedIcon: any) { const layoutTextSize = this.layers[0].getLayoutValue('text-size', {zoom: this.zoom + 1}, feature.properties); const layoutIconSize = this.layers[0].getLayoutValue('icon-size', {zoom: this.zoom + 1}, feature.properties); const textOffset = this.layers[0].getLayoutValue('text-offset', {zoom: this.zoom }, feature.properties); const iconOffset = this.layers[0].getLayoutValue('icon-offset', {zoom: this.zoom }, feature.properties); // 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. // This calculates text-size at a high zoom level so that all tiles can // use the same value when calculating anchor positions. let textMaxSize = this.layers[0].getLayoutValue('text-size', {zoom: 18}, feature.properties); if (textMaxSize === undefined) { textMaxSize = layoutTextSize; } const layout = this.layers[0].layout, glyphSize = 24, fontScale = layoutTextSize / glyphSize, textBoxScale = this.tilePixelRatio * fontScale, textMaxBoxScale = this.tilePixelRatio * textMaxSize / glyphSize, iconBoxScale = this.tilePixelRatio * layoutIconSize, symbolMinDistance = this.tilePixelRatio * layout['symbol-spacing'], avoidEdges = layout['symbol-avoid-edges'], textPadding = layout['text-padding'] * this.tilePixelRatio, iconPadding = layout['icon-padding'] * this.tilePixelRatio, textMaxAngle = layout['text-max-angle'] / 180 * Math.PI, textAlongLine = layout['text-rotation-alignment'] === 'map' && layout['symbol-placement'] === 'line', iconAlongLine = layout['icon-rotation-alignment'] === 'map' && layout['symbol-placement'] === 'line', mayOverlap = layout['text-allow-overlap'] || layout['icon-allow-overlap'] || layout['text-ignore-placement'] || layout['icon-ignore-placement'], symbolPlacement = layout['symbol-placement'], textRepeatDistance = symbolMinDistance / 2; const addSymbolInstance = (line, anchor) => { const inside = !(anchor.x < 0 || anchor.x > EXTENT || anchor.y < 0 || anchor.y > EXTENT); if (avoidEdges && !inside) return; // Normally symbol layers are drawn across tile boundaries. Only symbols // with their anchors within the tile boundaries are added to the buffers // to prevent symbols from being drawn twice. // // Symbols in layers with overlap are sorted in the y direction so that // symbols lower on the canvas are drawn on top of symbols near the top. // To preserve this order across tile boundaries these symbols can't // be drawn across tile boundaries. Instead they need to be included in // the buffers for both tiles and clipped to tile boundaries at draw time. const addToBuffers = inside || mayOverlap; this.addSymbolInstance(anchor, line, shapedTextOrientations, shapedIcon, this.layers[0], addToBuffers, this.collisionBoxArray, feature.index, feature.sourceLayerIndex, this.index, textBoxScale, textPadding, textAlongLine, textOffset, iconBoxScale, iconPadding, iconAlongLine, iconOffset, {zoom: this.zoom}, feature.properties); }; if (symbolPlacement === 'line') { for (const line of clipLine(feature.geometry, 0, 0, EXTENT, EXTENT)) { const anchors = getAnchors( line, symbolMinDistance, textMaxAngle, shapedTextOrientations[WritingMode.vertical] || shapedTextOrientations[WritingMode.horizontal], shapedIcon, glyphSize, textMaxBoxScale, this.overscaling, EXTENT ); for (const anchor of anchors) { const shapedText = shapedTextOrientations[WritingMode.horizontal]; if (!shapedText || !this.anchorIsTooClose(shapedText.text, textRepeatDistance, anchor)) { addSymbolInstance(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); addSymbolInstance(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) { addSymbolInstance(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) { addSymbolInstance([point], new Anchor(point.x, point.y, 0)); } } } } anchorIsTooClose(text: any, repeatDistance: any, anchor: any) { const compareText = this.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; } place(collisionTile: any, showCollisionBoxes: any) { // Calculate which labels can be shown and when they can be shown and // create the bufers used for rendering. this.createArrays(); this.placedGlyphArray = new PlacedSymbolArray(); this.placedIconArray = new PlacedSymbolArray(); this.glyphOffsetArray = new GlyphOffsetArray(); this.lineVertexArray = new LineVertexArray(); const layer = this.layers[0]; const layout = layer.layout; // Symbols that don't show until greater than the CollisionTile's maxScale won't even be added // to the buffers. Even though pan operations on a tilted map might cause the symbol to be // displayable, we have to stay conservative here because the CollisionTile didn't consider // this scale range. const maxScale = collisionTile.maxScale; const textAlongLine = layout['text-rotation-alignment'] === 'map' && layout['symbol-placement'] === 'line'; const iconAlongLine = layout['icon-rotation-alignment'] === 'map' && layout['symbol-placement'] === 'line'; const mayOverlap = layout['text-allow-overlap'] || layout['icon-allow-overlap'] || layout['text-ignore-placement'] || layout['icon-ignore-placement']; // Sort symbols by their y position on the canvas so that the lower symbols // are drawn on top of higher symbols. // Don't sort symbols that won't overlap because it isn't necessary and // because it causes more labels to pop in and out when rotating. if (mayOverlap) { const angle = collisionTile.angle; const sin = Math.sin(angle), cos = Math.cos(angle); this.symbolInstances.sort((a, b) => { const aRotated = (sin * a.anchor.x + cos * a.anchor.y) | 0; const bRotated = (sin * b.anchor.x + cos * b.anchor.y) | 0; return (aRotated - bRotated) || (b.featureIndex - a.featureIndex); }); } for (const symbolInstance of this.symbolInstances) { const textCollisionFeature = { boxStartIndex: symbolInstance.textBoxStartIndex, boxEndIndex: symbolInstance.textBoxEndIndex }; const iconCollisionFeature = { boxStartIndex: symbolInstance.iconBoxStartIndex, boxEndIndex: symbolInstance.iconBoxEndIndex }; const hasText = !(symbolInstance.textBoxStartIndex === symbolInstance.textBoxEndIndex); const hasIcon = !(symbolInstance.iconBoxStartIndex === symbolInstance.iconBoxEndIndex); const iconWithoutText = layout['text-optional'] || !hasText, textWithoutIcon = layout['icon-optional'] || !hasIcon; // Calculate the scales at which the text and icon can be placed without collision. let glyphScale = hasText ? collisionTile.placeCollisionFeature(textCollisionFeature, layout['text-allow-overlap'], layout['symbol-avoid-edges']) : collisionTile.minScale; let iconScale = hasIcon ? collisionTile.placeCollisionFeature(iconCollisionFeature, layout['icon-allow-overlap'], layout['symbol-avoid-edges']) : collisionTile.minScale; // Combine the scales for icons and text. if (!iconWithoutText && !textWithoutIcon) { iconScale = glyphScale = Math.max(iconScale, glyphScale); } else if (!textWithoutIcon && glyphScale) { glyphScale = Math.max(iconScale, glyphScale); } else if (!iconWithoutText && iconScale) { iconScale = Math.max(iconScale, glyphScale); } // Insert final placement into collision tree and add glyphs/icons to buffers if (!hasText && !hasIcon) continue; const line = symbolInstance.line; const lineStartIndex = this.lineVertexArray.length; for (let i = 0; i < line.length; i++) { this.lineVertexArray.emplaceBack(line[i].x, line[i].y); } const lineLength = this.lineVertexArray.length - lineStartIndex; if (hasText) { collisionTile.insertCollisionFeature(textCollisionFeature, glyphScale, layout['text-ignore-placement']); if (glyphScale <= maxScale) { const textSizeData = getSizeVertexData(layer, this.zoom, this.textSizeData.coveringZoomRange, 'text-size', symbolInstance.featureProperties); this.addSymbols( this.arrays.glyph, symbolInstance.glyphQuads, glyphScale, textSizeData, layout['text-keep-upright'], symbolInstance.textOffset, textAlongLine, collisionTile.angle, symbolInstance.featureProperties, symbolInstance.writingModes, symbolInstance.anchor, lineStartIndex, lineLength, this.placedGlyphArray); } } if (hasIcon) { collisionTile.insertCollisionFeature(iconCollisionFeature, iconScale, layout['icon-ignore-placement']); if (iconScale <= maxScale) { const iconSizeData = getSizeVertexData( layer, this.zoom, this.iconSizeData.coveringZoomRange, 'icon-size', symbolInstance.featureProperties); this.addSymbols( this.arrays.icon, symbolInstance.iconQuads, iconScale, iconSizeData, layout['icon-keep-upright'], symbolInstance.iconOffset, iconAlongLine, collisionTile.angle, symbolInstance.featureProperties, null, symbolInstance.anchor, lineStartIndex, lineLength, this.placedIconArray ); } } } if (showCollisionBoxes) this.addToDebugBuffers(collisionTile); } addSymbols(arrays: any, quads: any, scale: any, sizeVertex: any, keepUpright: any, lineOffset: any, alongLine: any, placementAngle: any, featureProperties: any, writingModes: any, labelAnchor: any, lineStartIndex: any, lineLength: any, placedSymbolArray: any) { const elementArray = arrays.elementArray; const layoutVertexArray = arrays.layoutVertexArray; const dynamicLayoutVertexArray = arrays.dynamicLayoutVertexArray; const zoom = this.zoom; const placementZoom = Math.max(Math.log(scale) / Math.LN2 + zoom, 0); const glyphOffsetArrayStart = this.glyphOffsetArray.length; const labelAngle = ((labelAnchor.angle + placementAngle) + 2 * Math.PI) % (2 * Math.PI); const inVerticalRange = ( (labelAngle > Math.PI * 1 / 4 && labelAngle <= Math.PI * 3 / 4) || (labelAngle > Math.PI * 5 / 4 && labelAngle <= Math.PI * 7 / 4)); const useVerticalMode = Boolean(writingModes & WritingMode.vertical) && inVerticalRange; for (const symbol of quads) { if (alongLine && keepUpright) { // drop incorrectly oriented glyphs if ((symbol.writingMode === WritingMode.vertical) !== useVerticalMode) continue; } const tl = symbol.tl, tr = symbol.tr, bl = symbol.bl, br = symbol.br, tex = symbol.tex; const segment = arrays.prepareSegment(4); const index = segment.vertexLength; const y = symbol.glyphOffset[1]; addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, tl.x, y + tl.y, tex.x, tex.y, sizeVertex); addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, tr.x, y + tr.y, tex.x + tex.w, tex.y, sizeVertex); addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, bl.x, y + bl.y, tex.x, tex.y + tex.h, sizeVertex); addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, br.x, y + br.y, tex.x + tex.w, tex.y + tex.h, sizeVertex); addDynamicAttributes(dynamicLayoutVertexArray, labelAnchor, 0, placementZoom); elementArray.emplaceBack(index, index + 1, index + 2); elementArray.emplaceBack(index + 1, index + 2, index + 3); segment.vertexLength += 4; segment.primitiveLength += 2; this.glyphOffsetArray.emplaceBack(symbol.glyphOffset[0]); } placedSymbolArray.emplaceBack(labelAnchor.x, labelAnchor.y, glyphOffsetArrayStart, this.glyphOffsetArray.length - glyphOffsetArrayStart, lineStartIndex, lineLength, labelAnchor.segment, sizeVertex ? sizeVertex[0] : 0, sizeVertex ? sizeVertex[1] : 0, lineOffset[0], lineOffset[1], placementZoom, useVerticalMode); arrays.populatePaintArrays(featureProperties); } addToDebugBuffers(collisionTile: any) { const arrays = this.arrays.collisionBox; const layoutVertexArray = arrays.layoutVertexArray; const elementArray = arrays.elementArray; const angle = -collisionTile.angle; const yStretch = collisionTile.yStretch; for (const symbolInstance of this.symbolInstances) { symbolInstance.textCollisionFeature = {boxStartIndex: symbolInstance.textBoxStartIndex, boxEndIndex: symbolInstance.textBoxEndIndex}; symbolInstance.iconCollisionFeature = {boxStartIndex: symbolInstance.iconBoxStartIndex, boxEndIndex: symbolInstance.iconBoxEndIndex}; for (let i = 0; i < 2; i++) { const feature = symbolInstance[i === 0 ? 'textCollisionFeature' : 'iconCollisionFeature']; if (!feature) continue; for (let b = feature.boxStartIndex; b < feature.boxEndIndex; b++) { const box = this.collisionBoxArray.get(b); if (collisionTile.perspectiveRatio === 1 && box.maxScale < 1) { // These boxes aren't used on unpitched maps // See CollisionTile#insertCollisionFeature continue; } const boxAnchorPoint = box.anchorPoint; const tl = new Point(box.x1, box.y1 * yStretch)._rotate(angle); const tr = new Point(box.x2, box.y1 * yStretch)._rotate(angle); const bl = new Point(box.x1, box.y2 * yStretch)._rotate(angle); const br = new Point(box.x2, box.y2 * yStretch)._rotate(angle); const maxZoom = Math.max(0, Math.min(25, this.zoom + Math.log(box.maxScale) / Math.LN2)); const placementZoom = Math.max(0, Math.min(25, this.zoom + Math.log(box.placementScale) / Math.LN2)); const segment = arrays.prepareSegment(4); const index = segment.vertexLength; addCollisionBoxVertex(layoutVertexArray, boxAnchorPoint, symbolInstance.anchor, tl, maxZoom, placementZoom); addCollisionBoxVertex(layoutVertexArray, boxAnchorPoint, symbolInstance.anchor, tr, maxZoom, placementZoom); addCollisionBoxVertex(layoutVertexArray, boxAnchorPoint, symbolInstance.anchor, br, maxZoom, placementZoom); addCollisionBoxVertex(layoutVertexArray, boxAnchorPoint, symbolInstance.anchor, bl, maxZoom, placementZoom); elementArray.emplaceBack(index, index + 1); elementArray.emplaceBack(index + 1, index + 2); elementArray.emplaceBack(index + 2, index + 3); elementArray.emplaceBack(index + 3, index); segment.vertexLength += 4; segment.primitiveLength += 4; } } } } /** * Add a single label & icon placement. * * Note that in the case of `symbol-placement: line`, the symbol instance's * array of glyph 'quads' may include multiple copies of each glyph, * corresponding to the different orientations it might take at different * zoom levels as the text goes around bends in the line. * * As such, each glyph quad includes a minzoom and maxzoom at which it * should be rendered. This zoom range is calculated based on the 'layout' * {text,icon} size -- i.e. text/icon-size at `z: tile.zoom + 1`. If the * size is zoom-dependent, then the zoom range is adjusted at render time * to account for the difference. * * @private */ addSymbolInstance(anchor: any, line: any, shapedTextOrientations: any, shapedIcon: any, layer: any, addToBuffers: any, collisionBoxArray: any, featureIndex: any, sourceLayerIndex: any, bucketIndex: any, textBoxScale: any, textPadding: any, textAlongLine: any, textOffset: any, iconBoxScale: any, iconPadding: any, iconAlongLine: any, iconOffset: any, globalProperties: any, featureProperties: any) { let textCollisionFeature, iconCollisionFeature; let iconQuads = []; let glyphQuads = []; for (const writingModeString in shapedTextOrientations) { const writingMode = parseInt(writingModeString, 10); if (!shapedTextOrientations[writingMode]) continue; glyphQuads = glyphQuads.concat(addToBuffers ? getGlyphQuads(anchor, shapedTextOrientations[writingMode], layer, textAlongLine, globalProperties, featureProperties) : []); textCollisionFeature = new CollisionFeature(collisionBoxArray, line, anchor, featureIndex, sourceLayerIndex, bucketIndex, shapedTextOrientations[writingMode], textBoxScale, textPadding, textAlongLine, false); } const textBoxStartIndex = textCollisionFeature ? textCollisionFeature.boxStartIndex : this.collisionBoxArray.length; const textBoxEndIndex = textCollisionFeature ? textCollisionFeature.boxEndIndex : this.collisionBoxArray.length; if (shapedIcon) { iconQuads = addToBuffers ? getIconQuads(anchor, shapedIcon, layer, iconAlongLine, shapedTextOrientations[WritingMode.horizontal], globalProperties, featureProperties) : []; iconCollisionFeature = new CollisionFeature(collisionBoxArray, line, anchor, featureIndex, sourceLayerIndex, bucketIndex, shapedIcon, iconBoxScale, iconPadding, iconAlongLine, true); } const iconBoxStartIndex = iconCollisionFeature ? iconCollisionFeature.boxStartIndex : this.collisionBoxArray.length; const iconBoxEndIndex = iconCollisionFeature ? iconCollisionFeature.boxEndIndex : this.collisionBoxArray.length; if (textBoxEndIndex > SymbolBucket.MAX_INSTANCES) util.warnOnce("Too many symbols being rendered in a tile. See https://github.com/mapbox/mapbox-gl-js/issues/2907"); if (iconBoxEndIndex > SymbolBucket.MAX_INSTANCES) util.warnOnce("Too many glyphs being rendered in a tile. See https://github.com/mapbox/mapbox-gl-js/issues/2907"); const writingModes = ( (shapedTextOrientations[WritingMode.vertical] ? WritingMode.vertical : 0) | (shapedTextOrientations[WritingMode.horizontal] ? WritingMode.horizontal : 0) ); this.symbolInstances.push({ textBoxStartIndex, textBoxEndIndex, iconBoxStartIndex, iconBoxEndIndex, glyphQuads, iconQuads, textOffset, iconOffset, anchor, line, featureIndex, featureProperties, writingModes }); } } // For {text,icon}-size, get the bucket-level data that will be needed by // the painter to set symbol-size-related uniforms function getSizeData(tileZoom, layer, sizeProperty) { const sizeData = {}; sizeData.isFeatureConstant = layer.isLayoutValueFeatureConstant(sizeProperty); sizeData.isZoomConstant = layer.isLayoutValueZoomConstant(sizeProperty); if (sizeData.isFeatureConstant) { sizeData.layoutSize = layer.getLayoutValue(sizeProperty, {zoom: tileZoom + 1}); } // calculate covering zoom stops for zoom-dependent values if (!sizeData.isZoomConstant) { const levels = layer.getLayoutValueStopZoomLevels(sizeProperty); let lower = 0; while (lower < levels.length && levels[lower] <= tileZoom) lower++; lower = Math.max(0, lower - 1); let upper = lower; while (upper < levels.length && levels[upper] < tileZoom + 1) upper++; upper = Math.min(levels.length - 1, upper); sizeData.coveringZoomRange = [levels[lower], levels[upper]]; if (layer.isLayoutValueFeatureConstant(sizeProperty)) { // for camera functions, also save off the function values // evaluated at the covering zoom levels sizeData.coveringStopValues = [ layer.getLayoutValue(sizeProperty, {zoom: levels[lower]}), layer.getLayoutValue(sizeProperty, {zoom: levels[upper]}) ]; } // also store the function's base for use in calculating the // interpolation factor each frame sizeData.functionBase = layer.getLayoutProperty(sizeProperty).base; if (typeof sizeData.functionBase === 'undefined') { sizeData.functionBase = 1; } sizeData.functionType = layer.getLayoutProperty(sizeProperty).type || 'exponential'; } return sizeData; } function getSizeVertexData(layer, tileZoom, stopZoomLevels, sizeProperty, featureProperties) { if ( layer.isLayoutValueZoomConstant(sizeProperty) && !layer.isLayoutValueFeatureConstant(sizeProperty) ) { // source function return [ 10 * layer.getLayoutValue(sizeProperty, {}, featureProperties) ]; } else if ( !layer.isLayoutValueZoomConstant(sizeProperty) && !layer.isLayoutValueFeatureConstant(sizeProperty) ) { // composite function return [ 10 * layer.getLayoutValue(sizeProperty, {zoom: stopZoomLevels[0]}, featureProperties), 10 * layer.getLayoutValue(sizeProperty, {zoom: stopZoomLevels[1]}, featureProperties) ]; } // camera function or constant return null; } SymbolBucket.programInterfaces = symbolInterfaces; // this constant is based on the size of StructArray indexes used in a symbol // bucket--namely, iconBoxEndIndex and textBoxEndIndex // eg the max valid UInt16 is 65,535 SymbolBucket.MAX_INSTANCES = 65535; SymbolBucket.addDynamicAttributes = addDynamicAttributes; module.exports = SymbolBucket;