mapbox-gl
Version:
A WebGL interactive maps library
1,066 lines (924 loc) • 57.5 kB
JavaScript
// @flow
import {symbolLayoutAttributes,
symbolGlobeExtAttributes,
collisionVertexAttributes,
collisionVertexAttributesExt,
collisionBoxLayout,
dynamicLayoutAttributes,
iconTransitioningAttributes,
zOffsetAttributes
} from './symbol_attributes.js';
import {SymbolLayoutArray,
SymbolGlobeExtArray,
SymbolDynamicLayoutArray,
SymbolOpacityArray,
CollisionBoxLayoutArray,
CollisionVertexExtArray,
CollisionVertexArray,
PlacedSymbolArray,
SymbolInstanceArray,
GlyphOffsetArray,
SymbolLineVertexArray,
SymbolIconTransitioningArray,
ZOffsetVertexArray
} from '../array_types.js';
import ONE_EM from '../../symbol/one_em.js';
import * as symbolSize from '../../symbol/symbol_size.js';
import Point from '@mapbox/point-geometry';
import SegmentVector from '../segment.js';
import {ProgramConfigurationSet} from '../program_configuration.js';
import {TriangleIndexArray, LineIndexArray} from '../index_array_type.js';
import transformText from '../../symbol/transform_text.js';
import mergeLines from '../../symbol/mergelines.js';
import {allowsVerticalWritingMode, stringContainsRTLText} from '../../util/script_detection.js';
import {WritingMode} from '../../symbol/shaping.js';
import loadGeometry from '../load_geometry.js';
import toEvaluationFeature from '../evaluation_feature.js';
import {VectorTileFeature} from '@mapbox/vector-tile';
const vectorTileFeatureTypes = VectorTileFeature.types;
import {verticalizedCharacterMap} from '../../util/verticalize_punctuation.js';
import Anchor from '../../symbol/anchor.js';
import {getSizeData} from '../../symbol/symbol_size.js';
import {MAX_PACKED_SIZE} from '../../symbol/symbol_layout.js';
import {register} from '../../util/web_worker_transfer.js';
import EvaluationParameters from '../../style/evaluation_parameters.js';
import Formatted from '../../style-spec/expression/types/formatted.js';
import ResolvedImage from '../../style-spec/expression/types/resolved_image.js';
import {plugin as globalRTLTextPlugin, getRTLTextPluginStatus} from '../../source/rtl_text_plugin.js';
import {resamplePred} from '../../geo/projection/resample.js';
import {tileCoordToECEF} from '../../geo/projection/globe_util.js';
import {getProjection} from '../../geo/projection/index.js';
import {mat4, vec3} from 'gl-matrix';
import assert from 'assert';
import SymbolStyleLayer from '../../style/style_layer/symbol_style_layer.js';
import type {ProjectionSpecification} from '../../style-spec/types.js';
import type Projection from '../../geo/projection/projection.js';
import type {CanonicalTileID, OverscaledTileID} from '../../source/tile_id.js';
import type {
Bucket,
BucketParameters,
IndexedFeature,
PopulateParameters
} from '../bucket.js';
import type {CollisionBoxArray, CollisionBox, SymbolInstance, StructArrayLayout1f4} from '../array_types.js';
import type {StructArray, StructArrayMember} from '../../util/struct_array.js';
import type Context from '../../gl/context.js';
import type IndexBuffer from '../../gl/index_buffer.js';
import type VertexBuffer from '../../gl/vertex_buffer.js';
import type {SymbolQuad} from '../../symbol/quads.js';
import type {SizeData} from '../../symbol/symbol_size.js';
import type {FeatureStates} from '../../source/source_state.js';
import type {TileTransform} from '../../geo/projection/tile_transform.js';
export type SingleCollisionBox = {
x1: number;
y1: number;
x2: number;
y2: number;
padding: number;
projectedAnchorX: number;
projectedAnchorY: number;
projectedAnchorZ: number;
tileAnchorX: number;
tileAnchorY: number;
elevation?: number;
tileID?: OverscaledTileID;
};
import type {Mat4, Vec3} from 'gl-matrix';
import type {SpritePositions} from '../../util/image.js';
import type {IVectorTileLayer} from '@mapbox/vector-tile';
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<Array<Point>>,
properties: Object,
type: 'Point' | 'LineString' | 'Polygon',
id?: any
|};
export type SortKeyRange = {
sortKey: number,
symbolInstanceStart: number,
symbolInstanceEnd: number
};
type LineVertexRange = {|
lineLength: number,
lineStartIndex: 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', offset: 0}
];
function addVertex(array: SymbolLayoutArray, tileAnchorX: number, tileAnchorY: number, ox: number, oy: number, tx: number, ty: number, sizeVertex: any, 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
tileAnchorX,
tileAnchorY,
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 addTransitioningVertex(array: SymbolIconTransitioningArray, tx: number, ty: number) {
array.emplaceBack(tx, ty);
}
function addGlobeVertex(array: SymbolGlobeExtArray, projAnchorX: number, projAnchorY: number, projAnchorZ: number, normX: number, normY: number, normZ: number) {
array.emplaceBack(
// a_globe_anchor
projAnchorX,
projAnchorY,
projAnchorZ,
// a_globe_normal
normX,
normY,
normZ
);
}
function updateGlobeVertexNormal(array: SymbolGlobeExtArray, vertexIdx: number, normX: number, normY: number, normZ: number) {
// Modify float32 array directly. 20 bytes per entry, 3xInt16 for position, 3xfloat32 for normal
const offset = vertexIdx * 5 + 2;
array.float32[offset + 0] = normX;
array.float32[offset + 1] = normY;
array.float32[offset + 2] = normZ;
}
function addDynamicAttributes(dynamicLayoutVertexArray: StructArray, x: number, y: number, z: number, angle: number) {
dynamicLayoutVertexArray.emplaceBack(x, y, z, angle);
dynamicLayoutVertexArray.emplaceBack(x, y, z, angle);
dynamicLayoutVertexArray.emplaceBack(x, y, z, angle);
dynamicLayoutVertexArray.emplaceBack(x, y, z, 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<SymbolStyleLayer>;
segments: SegmentVector;
dynamicLayoutVertexArray: SymbolDynamicLayoutArray;
dynamicLayoutVertexBuffer: VertexBuffer;
opacityVertexArray: SymbolOpacityArray;
opacityVertexBuffer: VertexBuffer;
zOffsetVertexArray: ZOffsetVertexArray;
zOffsetVertexBuffer: VertexBuffer;
iconTransitioningVertexArray: SymbolIconTransitioningArray;
iconTransitioningVertexBuffer: ?VertexBuffer;
globeExtVertexArray: SymbolGlobeExtArray;
globeExtVertexBuffer: ?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();
this.iconTransitioningVertexArray = new SymbolIconTransitioningArray();
this.globeExtVertexArray = new SymbolGlobeExtArray();
this.zOffsetVertexArray = new ZOffsetVertexArray();
}
isEmpty(): boolean {
return this.layoutVertexArray.length === 0 &&
this.indexArray.length === 0 &&
this.dynamicLayoutVertexArray.length === 0 &&
this.opacityVertexArray.length === 0 &&
this.iconTransitioningVertexArray.length === 0;
}
upload(context: Context, dynamicIndexBuffer: boolean, upload?: boolean, update?: boolean, createZOffsetBuffer?: 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);
if (this.iconTransitioningVertexArray.length > 0) {
this.iconTransitioningVertexBuffer = context.createVertexBuffer(this.iconTransitioningVertexArray, iconTransitioningAttributes.members, true);
}
if (this.globeExtVertexArray.length > 0) {
this.globeExtVertexBuffer = context.createVertexBuffer(this.globeExtVertexArray, symbolGlobeExtAttributes.members, true);
}
if (!this.zOffsetVertexBuffer && (this.zOffsetVertexArray.length > 0 || !!createZOffsetBuffer)) {
this.zOffsetVertexBuffer = context.createVertexBuffer(this.zOffsetVertexArray, zOffsetAttributes.members, 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();
if (this.iconTransitioningVertexBuffer) {
this.iconTransitioningVertexBuffer.destroy();
}
if (this.globeExtVertexBuffer) {
this.globeExtVertexBuffer.destroy();
}
if (this.zOffsetVertexBuffer) {
this.zOffsetVertexBuffer.destroy();
}
}
}
register(SymbolBuffers, 'SymbolBuffers');
class CollisionBuffers {
layoutVertexArray: StructArray;
layoutAttributes: Array<StructArrayMember>;
layoutVertexBuffer: VertexBuffer;
indexArray: TriangleIndexArray | LineIndexArray;
indexBuffer: IndexBuffer;
segments: SegmentVector;
collisionVertexArray: CollisionVertexArray;
collisionVertexBuffer: VertexBuffer;
collisionVertexArrayExt: CollisionVertexExtArray;
collisionVertexBufferExt: 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();
this.collisionVertexArrayExt = new CollisionVertexExtArray();
}
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);
this.collisionVertexBufferExt = context.createVertexBuffer(this.collisionVertexArrayExt, collisionVertexAttributesExt.members, true);
}
destroy() {
if (!this.layoutVertexBuffer) return;
this.layoutVertexBuffer.destroy();
this.indexBuffer.destroy();
this.segments.destroy();
this.collisionVertexBuffer.destroy();
this.collisionVertexBufferExt.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
* `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.updateBucketOpacities() 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;
iconsInText: boolean;
iconsNeedLinear: boolean;
bucketInstanceId: number;
justReloaded: boolean;
hasPattern: boolean;
fullyClipped: boolean;
textSizeData: SizeData;
iconSizeData: SizeData;
glyphOffsetArray: GlyphOffsetArray;
lineVertexArray: SymbolLineVertexArray;
features: Array<SymbolFeature>;
symbolInstances: SymbolInstanceArray;
collisionArrays: Array<CollisionArrays>;
sortKeyRanges: Array<SortKeyRange>;
pixelRatio: number;
tilePixelRatio: number;
compareText: {[_: string]: Array<Point>};
fadeStartTime: number;
sortFeaturesByKey: boolean;
sortFeaturesByY: boolean;
canOverlap: boolean;
sortedAngle: number;
featureSortOrder: Array<number>;
collisionCircleArray: Array<number>;
placementInvProjMatrix: Mat4;
placementViewportMatrix: Mat4;
text: SymbolBuffers;
icon: SymbolBuffers;
textCollisionBox: CollisionBuffers;
iconCollisionBox: CollisionBuffers;
uploaded: boolean;
sourceLayerIndex: number;
sourceID: string;
symbolInstanceIndexes: Array<number>;
writingModes: Array<number>;
allowVerticalPlacement: boolean;
hasRTLText: boolean;
projection: ProjectionSpecification;
projectionInstance: ?Projection;
hasAnyIconTextFit: boolean;
hasAnyZOffset: boolean;
symbolInstanceIndexesSortedZOffset: Array<number>;
zOffsetSortDirty: boolean;
zOffsetBuffersNeedUpload: boolean;
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.fqid);
this.index = options.index;
this.pixelRatio = options.pixelRatio;
this.sourceLayerIndex = options.sourceLayerIndex;
this.hasPattern = false;
this.hasRTLText = false;
this.fullyClipped = false;
this.hasAnyIconTextFit = false;
this.sortKeyRanges = [];
this.collisionCircleArray = [];
this.placementInvProjMatrix = mat4.identity([]);
this.placementViewportMatrix = mat4.identity([]);
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 =
layout.get('text-allow-overlap') ||
layout.get('icon-allow-overlap') ||
layout.get('text-ignore-placement') ||
layout.get('icon-ignore-placement');
this.sortFeaturesByKey = zOrder !== 'viewport-y' && sortKey.constantOr(1) !== undefined;
const zOrderByViewportY = zOrder === 'viewport-y' || (zOrder === 'auto' && !this.sortFeaturesByKey);
this.sortFeaturesByY = zOrderByViewportY && this.canOverlap;
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;
this.projection = options.projection;
this.hasAnyZOffset = false;
this.zOffsetSortDirty = false;
this.zOffsetBuffersNeedUpload = layout.get('symbol-z-elevate');
}
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();
}
calculateGlyphDependencies(text: string, stack: {[_: number]: boolean}, textAlongLine: boolean, allowVerticalPlacement: boolean, doesAllowVerticalWritingMode: boolean) {
for (let i = 0; i < text.length; i++) {
const codePoint = text.codePointAt(i);
if (codePoint === undefined) break;
stack[codePoint] = true;
if (allowVerticalPlacement && doesAllowVerticalWritingMode && codePoint <= 65535) {
const verticalChar = verticalizedCharacterMap[text.charAt(i)];
if (verticalChar) {
stack[verticalChar.charCodeAt(0)] = true;
}
}
}
}
populate(features: Array<IndexedFeature>, options: PopulateParameters, canonical: CanonicalTileID, tileTransform: TileTransform) {
const layer = this.layers[0];
const layout = layer.layout;
const isGlobe = this.projection.name === 'globe';
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);
// $FlowFixMe[method-unbinding]
if (!layer._featureFilter.filter(globalProperties, evaluationFeature, canonical)) {
continue;
}
if (!needGeometry) evaluationFeature.geometry = loadGeometry(feature, canonical, tileTransform);
if (isGlobe && feature.type !== 1 && canonical.z <= 5) {
// Resample long lines and polygons in globe view so that their length wont exceed ~0.19 radians (360/32 degrees).
// Otherwise lines could clip through the globe as the resolution is not enough to represent curved paths.
// The threshold value follows subdivision size used with fill extrusions
const geom = evaluationFeature.geometry;
// cos(11.25 degrees) = 0.98078528056
const cosAngleThreshold = 0.98078528056;
const predicate = (a: Point, b: Point) => {
const v0 = tileCoordToECEF(a.x, a.y, canonical, 1);
const v1 = tileCoordToECEF(b.x, b.y, canonical, 1);
return vec3.dot(v0, v1) < cosAngleThreshold;
};
for (let i = 0; i < geom.length; i++) {
geom[i] = resamplePred(geom[i], predicate);
}
}
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);
if (containsRTLText(formattedText)) {
this.hasRTLText = true;
}
if (
!this.hasRTLText || // non-rtl text so can proceed safely
getRTLTextPluginStatus() === 'unavailable' || // We don't intend to lazy-load the rtl text plugin, so proceed with incorrect shaping
(this.hasRTLText && globalRTLTextPlugin.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.namePrimary] = true;
if (icon.nameSecondary) {
icons[icon.nameSecondary] = true;
}
}
if (text) {
const fontStack = textFont.evaluate(evaluationFeature, {}, canonical).join(',');
const textAlongLine = layout.get('text-rotation-alignment') === 'map' && 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.namePrimary] = 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: any): number) - ((b.sortKey: any): number);
});
}
}
update(states: FeatureStates, vtLayer: IVectorTileLayer, availableImages: Array<string>, imagePositions: SpritePositions, brightness: ?number) {
const withStateUpdates = Object.keys(states).length !== 0;
if (withStateUpdates && !this.stateDependentLayers.length) return;
const layers = withStateUpdates ? this.stateDependentLayers : this.layers;
this.text.programConfigurations.updatePaintArrays(states, vtLayer, layers, availableImages, imagePositions, brightness);
this.icon.programConfigurations.updatePaintArrays(states, vtLayer, layers, availableImages, imagePositions, brightness);
}
updateZOffset() {
// z offset is expected to change less frequently than the placement opacity and, if values are the same,
// avoid uploading arrays to buffers.
const addZOffsetTextVertex = (array: StructArrayLayout1f4, numVertices: number, value: number) => {
currentTextZOffsetVertex += numVertices;
if (currentTextZOffsetVertex > array.length) {
array.resize(currentTextZOffsetVertex);
}
for (let i = -numVertices; i < 0; i++) {
array.emplace(i + currentTextZOffsetVertex, value);
}
};
const addZOffsetIconVertex = (array: StructArrayLayout1f4, numVertices: number, value: number) => {
currentIconZOffsetVertex += numVertices;
if (currentIconZOffsetVertex > array.length) {
array.resize(currentIconZOffsetVertex);
}
for (let i = -numVertices; i < 0; i++) {
array.emplace(i + currentIconZOffsetVertex, value);
}
};
const updateZOffset = this.zOffsetBuffersNeedUpload;
if (!updateZOffset) return;
this.zOffsetBuffersNeedUpload = false;
let currentTextZOffsetVertex = 0;
let currentIconZOffsetVertex = 0;
for (let s = 0; s < this.symbolInstances.length; s++) {
const symbolInstance = this.symbolInstances.get(s);
const {
numHorizontalGlyphVertices,
numVerticalGlyphVertices,
numIconVertices
} = symbolInstance;
const zOffset = symbolInstance.zOffset;
const hasText = numHorizontalGlyphVertices > 0 || numVerticalGlyphVertices > 0;
const hasIcon = numIconVertices > 0;
if (hasText) {
addZOffsetTextVertex(this.text.zOffsetVertexArray, numHorizontalGlyphVertices, zOffset);
addZOffsetTextVertex(this.text.zOffsetVertexArray, numVerticalGlyphVertices, zOffset);
}
if (hasIcon) {
const {placedIconSymbolIndex, verticalPlacedIconSymbolIndex} = symbolInstance;
if (placedIconSymbolIndex >= 0) {
addZOffsetIconVertex(this.icon.zOffsetVertexArray, numIconVertices, zOffset);
}
if (verticalPlacedIconSymbolIndex >= 0) {
addZOffsetIconVertex(this.icon.zOffsetVertexArray, symbolInstance.numVerticalIconVertices, zOffset);
}
}
}
if (this.text.zOffsetVertexBuffer) {
this.text.zOffsetVertexBuffer.updateData(this.text.zOffsetVertexArray);
assert(this.text.zOffsetVertexBuffer.length === this.text.layoutVertexArray.length);
}
if (this.icon.zOffsetVertexBuffer) {
this.icon.zOffsetVertexBuffer.updateData(this.icon.zOffsetVertexArray);
assert(this.icon.zOffsetVertexBuffer.length === this.icon.layoutVertexArray.length);
}
}
isEmpty(): boolean {
// 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(): boolean {
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.zOffsetBuffersNeedUpload);
this.icon.upload(context, this.sortFeaturesByY, !this.uploaded, this.icon.programConfigurations.needsUpload, this.zOffsetBuffersNeedUpload);
this.uploaded = true;
}
destroyDebugData() {
this.textCollisionBox.destroy();
this.iconCollisionBox.destroy();
}
getProjection(): Projection {
if (!this.projectionInstance) {
this.projectionInstance = getProjection(this.projection);
}
return this.projectionInstance;
}
destroy() {
this.text.destroy();
this.icon.destroy();
if (this.hasDebugData()) {
this.destroyDebugData();
}
}
addToLineVertexArray(anchor: Anchor, line: Array<Point>): LineVertexRange {
const lineStartIndex = this.lineVertexArray.length;
if (anchor.segment !== undefined) {
for (const {x, y} of line) {
this.lineVertexArray.emplaceBack(x, y);
}
}
return {
lineStartIndex,
lineLength: this.lineVertexArray.length - lineStartIndex
};
}
addSymbols(arrays: SymbolBuffers,
quads: Array<SymbolQuad>,
sizeVertex: any,
lineOffset: [number, number],
alongLine: boolean,
feature: SymbolFeature,
writingMode: any,
globe: ?{ anchor: Anchor, up: Vec3 },
tileAnchor: Anchor,
lineStartIndex: number,
lineLength: number,
associatedIconIndex: number,
availableImages: Array<string>,
canonical: CanonicalTileID,
brightness: ?number,
hasAnySecondaryIcon: boolean) {
const indexArray = arrays.indexArray;
const layoutVertexArray = arrays.layoutVertexArray;
const globeExtVertexArray = arrays.globeExtVertexArray;
const segment = arrays.segments.prepareSegment(4 * quads.length, layoutVertexArray, indexArray, this.canOverlap ? feature.sortKey : 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, texPrimary, texSecondary, pixelOffsetTL, pixelOffsetBR, minFontScaleX, minFontScaleY, glyphOffset, isSDF, sectionIndex} = quads[i];
const index = segment.vertexLength;
const y = glyphOffset[1];
addVertex(layoutVertexArray, tileAnchor.x, tileAnchor.y, tl.x, y + tl.y, texPrimary.x, texPrimary.y, sizeVertex, isSDF, pixelOffsetTL.x, pixelOffsetTL.y, minFontScaleX, minFontScaleY);
addVertex(layoutVertexArray, tileAnchor.x, tileAnchor.y, tr.x, y + tr.y, texPrimary.x + texPrimary.w, texPrimary.y, sizeVertex, isSDF, pixelOffsetBR.x, pixelOffsetTL.y, minFontScaleX, minFontScaleY);
addVertex(layoutVertexArray, tileAnchor.x, tileAnchor.y, bl.x, y + bl.y, texPrimary.x, texPrimary.y + texPrimary.h, sizeVertex, isSDF, pixelOffsetTL.x, pixelOffsetBR.y, minFontScaleX, minFontScaleY);
addVertex(layoutVertexArray, tileAnchor.x, tileAnchor.y, br.x, y + br.y, texPrimary.x + texPrimary.w, texPrimary.y + texPrimary.h, sizeVertex, isSDF, pixelOffsetBR.x, pixelOffsetBR.y, minFontScaleX, minFontScaleY);
if (globe) {
const {x, y, z} = globe.anchor;
const [ux, uy, uz] = globe.up;
addGlobeVertex(globeExtVertexArray, x, y, z, ux, uy, uz);
addGlobeVertex(globeExtVertexArray, x, y, z, ux, uy, uz);
addGlobeVertex(globeExtVertexArray, x, y, z, ux, uy, uz);
addGlobeVertex(globeExtVertexArray, x, y, z, ux, uy, uz);
addDynamicAttributes(arrays.dynamicLayoutVertexArray, x, y, z, angle);
} else {
addDynamicAttributes(arrays.dynamicLayoutVertexArray, tileAnchor.x, tileAnchor.y, tileAnchor.z, angle);
}
// For data-driven cases if at least of one the icon has a transitionable variant
// we have to load the main variant in cases where the secondary image is not specified
if (hasAnySecondaryIcon) {
const tex = texSecondary ? texSecondary : texPrimary;
addTransitioningVertex(arrays.iconTransitioningVertexArray, tex.x, tex.y);
addTransitioningVertex(arrays.iconTransitioningVertexArray, tex.x + tex.w, tex.y);
addTransitioningVertex(arrays.iconTransitioningVertexArray, tex.x, tex.y + tex.h);
addTransitioningVertex(arrays.iconTransitioningVertexArray, tex.x + tex.w, tex.y + tex.h);
}
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, {}, availableImages, canonical, brightness, sections && sections[sectionIndex]);
}
}
const projectedAnchor = globe ? globe.anchor : tileAnchor;
arrays.placedSymbolArray.emplaceBack(projectedAnchor.x, projectedAnchor.y, projectedAnchor.z, tileAnchor.x, tileAnchor.y,
glyphOffsetArrayStart, this.glyphOffsetArray.length - glyphOffsetArrayStart, vertexStartIndex,
lineStartIndex, lineLength, (tileAnchor.segment: any),
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: any),
// The crossTileID is only filled/used on the foreground for dynamic text anchors
0,
associatedIconIndex,
// flipState is unknown initially; will be updated to flipRequired(1)/flipNotRequired(2) during line label reprojection
0
);
}
_commitLayoutVertex(array: StructArray, boxTileAnchorX: number, boxTileAnchorY: number, boxTileAnchorZ: number, tileAnchorX: number, tileAnchorY: number, extrude: Point) {
array.emplaceBack(
// pos
boxTileAnchorX,
boxTileAnchorY,
boxTileAnchorZ,
// a_anchor_pos
tileAnchorX,
tileAnchorY,
// extrude
Math.round(extrude.x),
Math.round(extrude.y));
}
_addCollisionDebugVertices(box: CollisionBox, scale: number, arrays: CollisionBuffers, boxTileAnchorX: number, boxTileAnchorY: number, boxTileAnchorZ: number, symbolInstance: SymbolInstance) {
const segment = arrays.segments.prepareSegment(4, arrays.layoutVertexArray, arrays.indexArray);
const index = segment.vertexLength;
const symbolTileAnchorX = symbolInstance.tileAnchorX;
const symbolTileAnchorY = symbolInstance.tileAnchorY;
for (let i = 0; i < 4; i++) {
arrays.collisionVertexArray.emplaceBack(0, 0, 0, 0);
}
this._commitDebugCollisionVertexUpdate(arrays.collisionVertexArrayExt, scale, box.padding, symbolInstance.zOffset);
this._commitLayoutVertex(arrays.layoutVertexArray, boxTileAnchorX, boxTileAnchorY, boxTileAnchorZ, symbolTileAnchorX, symbolTileAnchorY, new Point(box.x1, box.y1));
this._commitLayoutVertex(arrays.layoutVertexArray, boxTileAnchorX, boxTileAnchorY, boxTileAnchorZ, symbolTileAnchorX, symbolTileAnchorY, new Point(box.x2, box.y1));
this._commitLayoutVertex(arrays.layoutVertexArray, boxTileAnchorX, boxTileAnchorY, boxTileAnchorZ, symbolTileAnchorX, symbolTileAnchorY, new Point(box.x2, box.y2));
this._commitLayoutVertex(arrays.layoutVertexArray, boxTileAnchorX, boxTileAnchorY, boxTileAnchorZ, symbolTileAnchorX, symbolTileAnchorY, new Point(box.x1, box.y2));
segment.vertexLength += 4;
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;
}
_addTextDebugCollisionBoxes(size: any, zoom: number, collisionBoxArray: CollisionBoxArray, startIndex: number, endIndex: number, instance: SymbolInstance) {
for (let b = startIndex; b < endIndex; b++) {
const box: CollisionBox = (collisionBoxArray.get(b): any);
const scale = this.getSymbolInstanceTextSize(size, instance, zoom, b);
this._addCollisionDebugVertices(box, scale, this.textCollisionBox, box.projectedAnchorX, box.projectedAnchorY, box.projectedAnchorZ, instance);
}
}
_addIconDebugCollisionBoxes(size: any, zoom: number, collisionBoxArray: CollisionBoxArray, startIndex: number, endIndex: number, instance: SymbolInstance) {
for (let b = startIndex; b < endIndex; b++) {
const box: CollisionBox = (collisionBoxArray.get(b): any);
const scale = this.getSymbolInstanceIconSize(size, zoom, instance.placedIconSymbolIndex);
this._addCollisionDebugVertices(box, scale, this.iconCollisionBox, box.projectedAnchorX, box.projectedAnchorY, box.projectedAnchorZ, instance);
}
}
generateCollisionDebugBuffers(zoom: number, collisionBoxArray: CollisionBoxArray) {
if (this.hasDebugData()) {
this.destroyDebugData();
}
this.textCollisionBox = new CollisionBuffers(CollisionBoxLayoutArray, collisionBoxLayout.members, LineIndexArray);
this.iconCollisionBox = new CollisionBuffers(CollisionBoxLayoutArray, collisionBoxLayout.members, LineIndexArray);
const iconSize = symbolSize.evaluateSizeForZoom(this.iconSizeData, zoom);
const textSize = symbolSize.evaluateSizeForZoom(this.textSizeData, zoom);
for (let i = 0; i < this.symbolInstances.length; i++) {
const symbolInstance = this.symbolInstances.get(i);
this._addTextDebugCollisionBoxes(textSize, zoom, collisionBoxArray, symbolInstance.textBoxStartIndex, symbolInstance.textBoxEndIndex, symbolInstance);
this._addTextDebugCollisionBoxes(textSize, zoom, collisionBoxArray, symbolInstance.verticalTextBoxStartIndex, symbolInstance.verticalTextBoxEndIndex, symbolInstance);
this._addIconDebugCollisionBoxes(iconSize, zoom, collisionBoxArray, symbolInstance.iconBoxStartIndex, symbolInstance.iconBoxEndIndex, symbolInstance);
this._addIconDebugCollisionBoxes(iconSize, zoom, collisionBoxArray, symbolInstance.verticalIconBoxStartIndex, symbolInstance.verticalIconBoxEndIndex, symbolInstance);
}
}
getSymbolInstanceTextSize(textSize: any, instance: SymbolInstance, zoom: number, boxIndex: number): number {
const symbolIndex = instance.rightJustifiedTextSymbolIndex >= 0 ?
instance.rightJustifiedTextSymbolIndex : instance.centerJustifiedTextSymbolIndex >= 0 ?
instance.centerJustifiedTextSymbolIndex : instance.leftJustifiedTextSymbolIndex >= 0 ?
instance.leftJustifiedTextSymbolIndex : instance.verticalPlacedTextSymbolIndex >= 0 ?
instance.verticalPlacedTextSymbolIndex : boxIndex;
const symbol = this.text.placedSymbolArray.get(symbolIndex);
const featureSize = symbolSize.evaluateSizeForFeature(this.textSizeData, textSize, symbol) / ONE_EM;
return this.tilePixelRatio * featureSize;
}
getSymbolInstanceIconSize(iconSize: any, zoom: number, iconIndex: number): number {
const symbol = this.icon.placedSymbolArray.get(iconIndex);
const featureSize = symbolSize.evaluateSizeForFeature(this.iconSizeData, iconSize, symbol);
return this.tilePixelRatio * featureSize;
}
_commitDebugCollisionVertexUpdate(array: StructArray, scale: number, padding: number, zOffset: number) {
array.emplaceBack(scale, -padding, -padding, zOffset);
array.emplaceBack(scale, padding, -padding, zOffset);
array.emplaceBack(scale, padding, padding, zOffset);
array.emplaceBack(scale, -padding, padding, zOffset);
}
_updateTextDebugCollisionBoxes(size: any, zoom: number, collisionBoxArray: CollisionBoxArray, startIndex: number, endIndex: number, instance: SymbolInstance) {
for (let b = startIndex; b < endIndex; b++) {
const box: CollisionBox = (collisionBoxArray.get(b): any);
const scale = this.getSymbolInstanceTextSize(size, instance, zoom, b);
const array = this.textCollisionBox.collisionVertexArrayExt;
this._commitDebugCollisionVertexUpdate(array, scale, box.padding, instance.zOffset);
}
}
_updateIconDebugCollisionBoxes(size: any, zoom: number, collisionBoxArray: CollisionBoxArray, startIndex: number, endIndex: number, instance: SymbolInstance) {
for (let b = startIndex; b < endIndex; b++) {
const box = (collisionBoxArray.get(b));
const scale = this.getSymbolInstanceIconSize(size, zoom, instance.placedIconSymbolIndex);
const array = this.iconCollisionBox.collisionVertexArrayExt;
this._commitDebugCollisionVertexUpdate(array, scale, box.padding, instance.zOffset);
}
}
updateCollisionDebugBuffers(zoom: number, collisionBoxArray: CollisionBoxArray) {
if (!this.hasDebugData()) {
return;
}
if (this.hasTextCollisionBoxData()) this.textCollisionBox.collisionVertexArrayExt.clear();
if (this.hasIconCollisionBoxData()) this.iconCollisionBox.collisionVertexArrayExt.clear();
const iconSize = symbolSize.evaluateSizeForZoom(this.iconSizeData, zoom);
const textSize = symbolSize.evaluateSizeForZoom(this.textSizeData, zoom);
for (let i = 0; i < this.symbolInstances.length; i++) {
const symbolInstance = this.symbolInstances.get(i);
this._updateTextDebugCollisionBoxes(textSize, zoom, collisionBoxArray, symbolInstance.textBoxStartIndex, symbolInstance.textBoxEndIndex, symbolInstance);
this._updateTextDebugCollisionBoxes(textSize, zoom, collisionBoxArray, symbolInstance.verticalTextBoxStartIndex, symbolInstance.verticalTextBoxEndIndex, symbolInstance);
this._updateIconDebugCollisionBoxes(iconSize, zoom, collisionBoxArray, symbolInstance.iconBoxStartIndex, symbolInstance.iconBoxEndIndex, symbolInstance);
this._updateIconDebugCollisionBoxes(iconSize, zoom, collisionBoxArray, symbolInstance.verticalIconBoxStartIndex, symbolInstance.verticalIconBoxEndIndex, symbolInstance);
}
if (this.hasTextCollisionBoxData() && this.textCollisionBox.collisionVertexBufferExt) {
this.textCollisionBox.collisionVertexBufferExt.updateData(this.textCollisionBox.collisionVertexArrayExt);
}
if (this.hasIconCollisionBoxData() && this.iconCollisionBox.collisionVertexBufferExt) {
this.iconCollisionBox.collisionVertexBufferExt.updateData(this.iconCollisionBox.collisionVertexArrayExt);
}
}
// 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 {
// Only one box allowed per instance
const collisionArrays = {};
if (textStartIndex < textEndIndex) {
const {x1, y1, x2, y2, padding, projectedAnchorX, projectedAnchorY, projectedAnchorZ, tileAnchorX, tileAnchorY, featureIndex} = collisionBoxArray.get(textStartIndex);
collisionArrays.textBox = {x1, y1, x2, y2, padding, projectedAnchorX, projectedAnchorY, projectedAnchorZ, tileAnchorX, tileAnchorY};
collisionArrays.textFeatureIndex = featureIndex;
}
if (verticalTextStartIndex < verticalTextEndIndex) {
const {x1, y1, x2, y2, padding, projectedAnchorX, projectedAnchorY, projectedAnchorZ, tileAnchorX, tileAnchorY, featureIndex} = collisionBoxArray.get(verticalTextStartIndex);
collisionArrays.verticalTextBox = {x1, y1, x2, y2, padding, projectedAnchorX, projectedAnchorY, projectedAnchorZ, tileAnchorX, tileAnchorY};
collisionArrays.verticalTextFeatureIndex = featureIndex;
}
if (iconStartIndex < iconEndIndex) {
const {x1, y1, x2, y2, padding, projectedAnchorX, projectedAnchorY, projectedAnchorZ, tileAnchorX, tileAnchorY, featureIndex} = collisionBoxArray.get(iconStartIndex);
collisionArrays.iconBox = {x1, y1, x2, y2, padding, projectedAnchorX, projectedAnchorY, projectedAnchorZ, tileAnchorX, tileAnchorY};
collisionArrays.iconFeatureIndex = featureIndex;
}
if (verticalIconStartIndex < verticalIconEndIndex) {
const {x1, y1, x2, y