mapbox-gl
Version:
A WebGL interactive maps library
802 lines (683 loc) • 33.5 kB
JavaScript
// @flow
import { symbolLayoutAttributes,
collisionVertexAttributes,
collisionBoxLayout,
collisionCircleLayout,
dynamicLayoutAttributes
} from './symbol_attributes';
import { SymbolLayoutArray,
SymbolDynamicLayoutArray,
SymbolOpacityArray,
CollisionBoxLayoutArray,
CollisionCircleLayoutArray,
CollisionVertexArray,
PlacedSymbolArray,
SymbolInstanceArray,
GlyphOffsetArray,
SymbolLineVertexArray
} from '../array_types';
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/mergelines';
import {allowsVerticalWritingMode} from '../../util/script_detection';
import loadGeometry from '../load_geometry';
import mvt from '@mapbox/vector-tile';
const vectorTileFeatureTypes = mvt.VectorTileFeature.types;
import {verticalizedCharacterMap} from '../../util/verticalize_punctuation';
import Anchor from '../../symbol/anchor';
import { getSizeData } from '../../symbol/symbol_size';
import { register } from '../../util/web_worker_transfer';
import EvaluationParameters from '../../style/evaluation_parameters';
import Formatted from '../../style-spec/expression/types/formatted';
import type {
Bucket,
BucketParameters,
IndexedFeature,
PopulateParameters
} from '../bucket';
import type {CollisionBoxArray, CollisionBox, SymbolInstance} from '../array_types';
import type { StructArray, StructArrayMember } 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';
export type SingleCollisionBox = {
x1: number;
y1: number;
x2: number;
y2: number;
anchorPointX: number;
anchorPointY: number;
};
export type CollisionArrays = {
textBox?: SingleCollisionBox;
iconBox?: SingleCollisionBox;
textCircles?: Array<number>;
textFeatureIndex?: number;
iconFeatureIndex?: number;
};
export type SymbolFeature = {|
sortKey: number | void,
text: Formatted | void,
icon: string | void,
index: number,
sourceLayerIndex: number,
geometry: Array<Array<Point>>,
properties: Object,
type: 'Point' | 'LineString' | 'Polygon',
id?: any
|};
// 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', offset: 0 }
];
function addVertex(array, anchorX, anchorY, ox, oy, tx, ty, sizeVertex) {
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
sizeVertex ? sizeVertex[0] : 0,
sizeVertex ? sizeVertex[1] : 0
);
}
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);
}
export class SymbolBuffers {
layoutVertexArray: SymbolLayoutArray;
layoutVertexBuffer: VertexBuffer;
indexArray: TriangleIndexArray;
indexBuffer: IndexBuffer;
programConfigurations: ProgramConfigurationSet<SymbolStyleLayer>;
segments: SegmentVector;
dynamicLayoutVertexArray: SymbolDynamicLayoutArray;
dynamicLayoutVertexBuffer: VertexBuffer;
opacityVertexArray: SymbolOpacityArray;
opacityVertexBuffer: VertexBuffer;
collisionVertexArray: CollisionVertexArray;
collisionVertexBuffer: VertexBuffer;
placedSymbolArray: PlacedSymbolArray;
constructor(programConfigurations: ProgramConfigurationSet<SymbolStyleLayer>) {
this.layoutVertexArray = new SymbolLayoutArray();
this.indexArray = new TriangleIndexArray();
this.programConfigurations = programConfigurations;
this.segments = new SegmentVector();
this.dynamicLayoutVertexArray = new SymbolDynamicLayoutArray();
this.opacityVertexArray = new SymbolOpacityArray();
this.placedSymbolArray = new PlacedSymbolArray();
}
upload(context: Context, dynamicIndexBuffer: boolean, upload?: boolean, update?: boolean) {
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<StructArrayMember>;
layoutVertexBuffer: VertexBuffer;
indexArray: TriangleIndexArray | LineIndexArray;
indexBuffer: IndexBuffer;
segments: SegmentVector;
collisionVertexArray: CollisionVertexArray;
collisionVertexBuffer: VertexBuffer;
constructor(LayoutArray: Class<StructArray>,
layoutAttributes: Array<StructArrayMember>,
IndexArray: Class<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);
/**
* 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.collisionBox`: Debug SymbolBuffers for collision boxes
* `this.collisionCircle`: Debug SymbolBuffers for collision circles
* The results are sent to the foreground for rendering
*
* 4. performSymbolPlacement(bucket, collisionIndex) 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".
*
* @private
*/
class SymbolBucket implements Bucket {
static MAX_GLYPHS: number;
static addDynamicAttributes: typeof addDynamicAttributes;
collisionBoxArray: CollisionBoxArray;
zoom: number;
overscaling: number;
layers: Array<SymbolStyleLayer>;
layerIds: Array<string>;
stateDependentLayers: Array<SymbolStyleLayer>;
stateDependentLayerIds: Array<string>;
index: number;
sdfIcons: boolean;
iconsNeedLinear: boolean;
bucketInstanceId: number;
justReloaded: boolean;
hasPattern: boolean;
textSizeData: SizeData;
iconSizeData: SizeData;
glyphOffsetArray: GlyphOffsetArray;
lineVertexArray: SymbolLineVertexArray;
features: Array<SymbolFeature>;
symbolInstances: SymbolInstanceArray;
collisionArrays: Array<CollisionArrays>;
pixelRatio: number;
tilePixelRatio: number;
compareText: {[string]: Array<Point>};
fadeStartTime: number;
sortFeaturesByKey: boolean;
sortFeaturesByY: boolean;
sortedAngle: number;
featureSortOrder: Array<number>;
text: SymbolBuffers;
icon: SymbolBuffers;
collisionBox: CollisionBuffers;
collisionCircle: CollisionBuffers;
uploaded: boolean;
sourceLayerIndex: number;
sourceID: string;
symbolInstanceIndexes: Array<number>;
constructor(options: BucketParameters<SymbolStyleLayer>) {
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;
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.sortFeaturesByKey = zOrder !== 'viewport-y' && sortKey.constantOr(1) !== undefined;
const zOrderByViewportY = zOrder === 'viewport-y' || (zOrder === 'auto' && !this.sortFeaturesByKey);
this.sortFeaturesByY = zOrderByViewportY && (layout.get('text-allow-overlap') || layout.get('icon-allow-overlap') ||
layout.get('text-ignore-placement') || layout.get('icon-ignore-placement'));
this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id);
this.sourceID = options.sourceID;
}
createArrays() {
this.text = new SymbolBuffers(new ProgramConfigurationSet(symbolLayoutAttributes.members, this.layers, this.zoom, property => /^text/.test(property)));
this.icon = new SymbolBuffers(new ProgramConfigurationSet(symbolLayoutAttributes.members, this.layers, this.zoom, property => /^icon/.test(property)));
this.collisionBox = new CollisionBuffers(CollisionBoxLayoutArray, collisionBoxLayout.members, LineIndexArray);
this.collisionCircle = new CollisionBuffers(CollisionCircleLayoutArray, collisionCircleLayout.members, TriangleIndexArray);
this.glyphOffsetArray = new GlyphOffsetArray();
this.lineVertexArray = new SymbolLineVertexArray();
this.symbolInstances = new SymbolInstanceArray();
}
calculateGlyphDependencies(text: string, stack: {[number]: boolean}, textAlongLine: boolean, doesAllowVerticalWritingMode: boolean) {
for (let i = 0; i < text.length; i++) {
stack[text.charCodeAt(i)] = true;
if (textAlongLine && doesAllowVerticalWritingMode) {
const verticalChar = verticalizedCharacterMap[text.charAt(i)];
if (verticalChar) {
stack[verticalChar.charCodeAt(0)] = true;
}
}
}
}
populate(features: Array<IndexedFeature>, options: PopulateParameters) {
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.toString().length > 0) &&
(textFont.value.kind !== 'constant' || textFont.value.value.length > 0);
const hasIcon = iconImage.value.kind !== 'constant' || iconImage.value.value && iconImage.value.value.length > 0;
const symbolSortKey = layout.get('symbol-sort-key');
this.features = [];
if (!hasText && !hasIcon) {
return;
}
const icons = options.iconDependencies;
const stacks = options.glyphDependencies;
const globalProperties = new EvaluationParameters(this.zoom);
for (const {feature, index, sourceLayerIndex} of features) {
if (!layer._featureFilter(globalProperties, feature)) {
continue;
}
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', feature);
text = transformText(resolvedTokens instanceof Formatted ?
resolvedTokens :
Formatted.fromString(resolvedTokens),
layer, feature);
}
let icon;
if (hasIcon) {
icon = layer.getValueAndResolveTokens('icon-image', feature);
}
if (!text && !icon) {
continue;
}
const sortKey = this.sortFeaturesByKey ?
symbolSortKey.evaluate(feature, {}) :
undefined;
const symbolFeature: SymbolFeature = {
text,
icon,
index,
sourceLayerIndex,
geometry: loadGeometry(feature),
properties: feature.properties,
type: vectorTileFeatureTypes[feature.type],
sortKey
};
if (typeof feature.id !== 'undefined') {
symbolFeature.id = feature.id;
}
this.features.push(symbolFeature);
if (icon) {
icons[icon] = true;
}
if (text) {
const fontStack = textFont.evaluate(feature, {}).join(',');
const textAlongLine = layout.get('text-rotation-alignment') === 'map' && layout.get('symbol-placement') !== 'point';
for (const section of text.sections) {
const doesAllowVerticalWritingMode = allowsVerticalWritingMode(text.toString());
const sectionFont = section.fontStack || fontStack;
const sectionStack = stacks[sectionFont] = stacks[sectionFont] || {};
this.calculateGlyphDependencies(section.text, sectionStack, textAlongLine, doesAllowVerticalWritingMode);
}
}
}
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: any): number) - ((b.sortKey: any): 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() {
return this.symbolInstances.length === 0;
}
uploadPending() {
return !this.uploaded || this.text.programConfigurations.needsUpload || this.icon.programConfigurations.needsUpload;
}
upload(context: Context) {
if (!this.uploaded) {
this.collisionBox.upload(context);
this.collisionCircle.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;
}
destroy() {
this.text.destroy();
this.icon.destroy();
this.collisionBox.destroy();
this.collisionCircle.destroy();
}
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<SymbolQuad>,
sizeVertex: any,
lineOffset: [number, number],
alongLine: boolean,
feature: SymbolFeature,
writingMode: any,
labelAnchor: Anchor,
lineStartIndex: number,
lineLength: number) {
const indexArray = arrays.indexArray;
const layoutVertexArray = arrays.layoutVertexArray;
const dynamicLayoutVertexArray = arrays.dynamicLayoutVertexArray;
const segment = arrays.segments.prepareSegment(4 * quads.length, arrays.layoutVertexArray, arrays.indexArray, feature.sortKey);
const glyphOffsetArrayStart = this.glyphOffsetArray.length;
const vertexStartIndex = segment.vertexLength;
for (const symbol of quads) {
const tl = symbol.tl,
tr = symbol.tr,
bl = symbol.bl,
br = symbol.br,
tex = symbol.tex;
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);
indexArray.emplaceBack(index, index + 1, index + 2);
indexArray.emplaceBack(index + 1, index + 2, index + 3);
segment.vertexLength += 4;
segment.primitiveLength += 2;
this.glyphOffsetArray.emplaceBack(symbol.glyphOffset[0]);
}
arrays.placedSymbolArray.emplaceBack(labelAnchor.x, labelAnchor.y,
glyphOffsetArrayStart, this.glyphOffsetArray.length - glyphOffsetArrayStart, vertexStartIndex,
lineStartIndex, lineLength, (labelAnchor.segment: any),
sizeVertex ? sizeVertex[0] : 0, sizeVertex ? sizeVertex[1] : 0,
lineOffset[0], lineOffset[1],
writingMode, (false: any),
// The crossTileID is only filled/used on the foreground for dynamic text anchors
0);
arrays.programConfigurations.populatePaintArrays(arrays.layoutVertexArray.length, feature, feature.index, {});
}
_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, isCircle: boolean) {
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;
if (isCircle) {
const indexArray: TriangleIndexArray = (arrays.indexArray: any);
indexArray.emplaceBack(index, index + 1, index + 2);
indexArray.emplaceBack(index, index + 2, index + 3);
segment.primitiveLength += 2;
} else {
const indexArray: LineIndexArray = (arrays.indexArray: any);
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) {
for (let b = startIndex; b < endIndex; b++) {
const box: CollisionBox = (this.collisionBoxArray.get(b): any);
const x1 = box.x1;
const y1 = box.y1;
const x2 = box.x2;
const y2 = box.y2;
// If the radius > 0, this collision box is actually a circle
// The data we add to the buffers is exactly the same, but we'll render with a different shader.
const isCircle = box.radius > 0;
this.addCollisionDebugVertices(x1, y1, x2, y2, isCircle ? this.collisionCircle : this.collisionBox, box.anchorPoint, symbolInstance, isCircle);
}
}
generateCollisionDebugBuffers() {
for (let i = 0; i < this.symbolInstances.length; i++) {
const symbolInstance = this.symbolInstances.get(i);
this.addDebugCollisionBoxes(symbolInstance.textBoxStartIndex, symbolInstance.textBoxEndIndex, symbolInstance);
this.addDebugCollisionBoxes(symbolInstance.iconBoxStartIndex, symbolInstance.iconBoxEndIndex, symbolInstance);
}
}
// These flat arrays are meant to be quicker to iterate over than the source
// CollisionBoxArray
_deserializeCollisionBoxesForSymbol(collisionBoxArray: CollisionBoxArray, textStartIndex: number, textEndIndex: number, iconStartIndex: number, iconEndIndex: number): CollisionArrays {
const collisionArrays = {};
for (let k = textStartIndex; k < textEndIndex; k++) {
const box: CollisionBox = (collisionBoxArray.get(k): any);
if (box.radius === 0) {
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
} else {
if (!collisionArrays.textCircles) {
collisionArrays.textCircles = [];
collisionArrays.textFeatureIndex = box.featureIndex;
}
const used = 1; // May be updated at collision detection time
collisionArrays.textCircles.push(box.anchorPointX, box.anchorPointY, box.radius, box.signedDistanceFromAnchor, used);
}
}
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): any);
if (box.radius === 0) {
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
}
}
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.iconBoxStartIndex,
symbolInstance.iconBoxEndIndex
));
}
}
hasTextData() {
return this.text.segments.get().length > 0;
}
hasIconData() {
return this.icon.segments.get().length > 0;
}
hasCollisionBoxData() {
return this.collisionBox.segments.get().length > 0;
}
hasCollisionCircleData() {
return this.collisionCircle.segments.get().length > 0;
}
addIndicesForPlacedTextSymbol(placedTextSymbolIndex: number) {
const placedSymbol = this.text.placedSymbolArray.get(placedTextSymbolIndex);
const endIndex = placedSymbol.vertexStartIndex + placedSymbol.numGlyphs * 4;
for (let vertexIndex = placedSymbol.vertexStartIndex; vertexIndex < endIndex; vertexIndex += 4) {
this.text.indexArray.emplaceBack(vertexIndex, vertexIndex + 1, vertexIndex + 2);
this.text.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;
}
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.addIndicesForPlacedTextSymbol(index);
}
});
if (symbolInstance.verticalPlacedTextSymbolIndex >= 0) {
this.addIndicesForPlacedTextSymbol(symbolInstance.verticalPlacedTextSymbolIndex);
}
const placedIcon = this.icon.placedSymbolArray.get(i);
if (placedIcon.numGlyphs) {
const vertexIndex = placedIcon.vertexStartIndex;
this.icon.indexArray.emplaceBack(vertexIndex, vertexIndex + 1, vertexIndex + 2);
this.icon.indexArray.emplaceBack(vertexIndex + 1, vertexIndex + 2, vertexIndex + 3);
}
}
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 default SymbolBucket;
export { addDynamicAttributes };