mapbox-gl
Version:
A WebGL interactive maps library
918 lines (796 loc) • 40.2 kB
JavaScript
'use strict';
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 VectorTileFeature = require('vector-tile').VectorTileFeature;
const shapeText = Shaping.shapeText;
const shapeIcon = Shaping.shapeIcon;
const WritingMode = Shaping.WritingMode;
const getGlyphQuads = Quads.getGlyphQuads;
const getIconQuads = Quads.getIconQuads;
const elementArrayType = createElementArrayType();
const layoutAttributes = [
{name: 'a_pos_offset', components: 4, type: 'Int16'},
{name: 'a_data', components: 4, type: 'Uint16'}
];
const symbolInterfaces = {
glyph: {
layoutAttributes: layoutAttributes,
elementArrayType: elementArrayType,
paintAttributes: [
{name: 'a_fill_color', property: 'text-color', type: 'Uint8'},
{name: 'a_halo_color', property: 'text-halo-color', type: 'Uint8'},
{name: 'a_halo_width', property: 'text-halo-width', type: 'Uint16', multiplier: 10},
{name: 'a_halo_blur', property: 'text-halo-blur', type: 'Uint16', multiplier: 10},
{name: 'a_opacity', property: 'text-opacity', type: 'Uint8', multiplier: 255}
]
},
icon: {
layoutAttributes: layoutAttributes,
elementArrayType: elementArrayType,
paintAttributes: [
{name: 'a_fill_color', property: 'icon-color', type: 'Uint8'},
{name: 'a_halo_color', property: 'icon-halo-color', type: 'Uint8'},
{name: 'a_halo_width', property: 'icon-halo-width', type: 'Uint16', multiplier: 10},
{name: 'a_halo_blur', property: 'icon-halo-blur', type: 'Uint16', multiplier: 10},
{name: 'a_opacity', property: 'icon-opacity', type: 'Uint8', multiplier: 255}
]
},
collisionBox: { // used to render collision boxes for debugging purposes
layoutAttributes: [
{name: 'a_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, x, y, ox, oy, tx, ty, sizeVertex, minzoom, maxzoom, labelminzoom, labelangle) {
array.emplaceBack(
// a_pos_offset
x,
y,
Math.round(ox * 64),
Math.round(oy * 64),
// a_data
tx / 4, // x coordinate of symbol on glyph atlas texture
ty / 4, // y coordinate of symbol on glyph atlas texture
packUint8ToFloat(
(labelminzoom || 0) * 10, // labelminzoom
labelangle % 256 // labelangle
),
packUint8ToFloat(
(minzoom || 0) * 10, // minzoom
Math.min(maxzoom || 25, 25) * 10 // maxzoom
),
// a_size
sizeVertex ? sizeVertex[0] : undefined,
sizeVertex ? sizeVertex[1] : undefined,
sizeVertex ? sizeVertex[2] : undefined
);
}
function addCollisionBoxVertex(layoutVertexArray, point, extrude, maxZoom, placementZoom) {
return layoutVertexArray.emplaceBack(
// pos
point.x,
point.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 {
constructor(options) {
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 = {
glyph: util.extend({}, symbolInterfaces.glyph, {
layoutAttributes: [].concat(
symbolInterfaces.glyph.layoutAttributes,
getSizeAttributeDeclarations(layer, 'text-size')
)
}),
icon: util.extend({}, symbolInterfaces.icon, {
layoutAttributes: [].concat(
symbolInterfaces.icon.layoutAttributes,
getSizeAttributeDeclarations(layer, 'icon-size')
)
}),
collisionBox: util.extend({}, symbolInterfaces.collisionBox, {
layoutAttributes: [].concat(
symbolInterfaces.collisionBox.layoutAttributes
)
})
};
// 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;
} else {
this.textSizeData = getSizeData(this.zoom, layer, 'text-size');
this.iconSizeData = getSizeData(this.zoom, layer, 'icon-size');
}
}
populate(features, options) {
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 (let i = 0; i < features.length; i++) {
const feature = features[i];
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: i,
sourceLayerIndex: feature.sourceLayerIndex,
geometry: loadGeometry(feature),
properties: feature.properties,
type: VectorTileFeature.types[feature.type]
});
if (icon) {
icons[icon] = true;
}
if (text) {
for (let i = 0; i < text.length; i++) {
stack[text.charCodeAt(i)] = 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) {
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,
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, icons) {
this.symbolInstances = [];
const tileSize = 512 * this.overscaling;
this.tilePixelRatio = EXTENT / tileSize;
this.compareText = {};
this.iconsNeedLinear = false;
const layout = this.layers[0].layout;
let horizontalAlign = 0.5,
verticalAlign = 0.5;
switch (layout['text-anchor']) {
case 'right':
case 'top-right':
case 'bottom-right':
horizontalAlign = 1;
break;
case 'left':
case 'top-left':
case 'bottom-left':
horizontalAlign = 0;
break;
}
switch (layout['text-anchor']) {
case 'bottom':
case 'bottom-right':
case 'bottom-left':
verticalAlign = 1;
break;
case 'top':
case 'top-right':
case 'top-left':
verticalAlign = 0;
break;
}
const justify = layout['text-justify'] === 'right' ? 1 :
layout['text-justify'] === 'left' ? 0 :
0.5;
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);
shapedTextOrientations = {
[WritingMode.horizontal]: shapeText(feature.text, stacks[fontstack], maxWidth, lineHeight, horizontalAlign, verticalAlign, justify, spacing, textOffset, oneEm, WritingMode.horizontal),
[WritingMode.vertical]: allowsVerticalWritingMode && textAlongLine && shapeText(feature.text, stacks[fontstack], maxWidth, lineHeight, horizontalAlign, verticalAlign, justify, spacing, textOffset, oneEm, WritingMode.vertical)
};
} else {
shapedTextOrientations = {};
}
let shapedIcon;
if (feature.icon) {
const image = icons[feature.icon];
const iconOffset = this.layers[0].getLayoutValue('icon-offset', {zoom: this.zoom}, feature.properties);
shapedIcon = shapeIcon(image, iconOffset);
if (image) {
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.pixelRatio !== 1) {
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, shapedTextOrientations, shapedIcon) {
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);
// 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,
iconBoxScale, iconPadding, iconAlongLine,
{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, repeatDistance, anchor) {
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, showCollisionBoxes) {
// Calculate which labels can be shown and when they can be shown and
// create the bufers used for rendering.
this.createArrays();
const layer = this.layers[0];
const layout = layer.layout;
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) {
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'],
textAlongLine,
collisionTile.angle,
symbolInstance.featureProperties,
symbolInstance.writingModes);
}
}
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'],
iconAlongLine,
collisionTile.angle,
symbolInstance.featureProperties
);
}
}
}
if (showCollisionBoxes) this.addToDebugBuffers(collisionTile);
}
addSymbols(arrays, quads, scale, sizeVertex, keepUpright, alongLine, placementAngle, featureProperties, writingModes) {
const elementArray = arrays.elementArray;
const layoutVertexArray = arrays.layoutVertexArray;
const zoom = this.zoom;
const placementZoom = Math.max(Math.log(scale) / Math.LN2 + zoom, 0);
for (const symbol of quads) {
// drop incorrectly oriented glyphs
const a = (symbol.anchorAngle + placementAngle + Math.PI) % (Math.PI * 2);
if (writingModes & WritingMode.vertical) {
if (alongLine && symbol.writingMode === WritingMode.vertical) {
if (keepUpright && alongLine && a <= (Math.PI * 5 / 4) || a > (Math.PI * 7 / 4)) continue;
} else if (keepUpright && alongLine && a <= (Math.PI * 3 / 4) || a > (Math.PI * 5 / 4)) continue;
} else if (keepUpright && alongLine && (a <= Math.PI / 2 || a > Math.PI * 3 / 2)) continue;
const tl = symbol.tl,
tr = symbol.tr,
bl = symbol.bl,
br = symbol.br,
tex = symbol.tex,
anchorPoint = symbol.anchorPoint;
let minZoom = Math.max(zoom + Math.log(symbol.minScale) / Math.LN2, placementZoom);
const maxZoom = Math.min(zoom + Math.log(symbol.maxScale) / Math.LN2, 25);
if (maxZoom <= minZoom) continue;
// Lower min zoom so that while fading out the label it can be shown outside of collision-free zoom levels
if (minZoom === placementZoom) minZoom = 0;
// Encode angle of glyph
const glyphAngle = Math.round((symbol.glyphAngle / (Math.PI * 2)) * 256);
const segment = arrays.prepareSegment(4);
const index = segment.vertexLength;
addVertex(layoutVertexArray, anchorPoint.x, anchorPoint.y, tl.x, tl.y, tex.x, tex.y, sizeVertex, minZoom, maxZoom, placementZoom, glyphAngle);
addVertex(layoutVertexArray, anchorPoint.x, anchorPoint.y, tr.x, tr.y, tex.x + tex.w, tex.y, sizeVertex, minZoom, maxZoom, placementZoom, glyphAngle);
addVertex(layoutVertexArray, anchorPoint.x, anchorPoint.y, bl.x, bl.y, tex.x, tex.y + tex.h, sizeVertex, minZoom, maxZoom, placementZoom, glyphAngle);
addVertex(layoutVertexArray, anchorPoint.x, anchorPoint.y, br.x, br.y, tex.x + tex.w, tex.y + tex.h, sizeVertex, minZoom, maxZoom, placementZoom, glyphAngle);
elementArray.emplaceBack(index, index + 1, index + 2);
elementArray.emplaceBack(index + 1, index + 2, index + 3);
segment.vertexLength += 4;
segment.primitiveLength += 2;
}
arrays.populatePaintArrays(featureProperties);
}
addToDebugBuffers(collisionTile) {
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);
const anchorPoint = 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, anchorPoint, tl, maxZoom, placementZoom);
addCollisionBoxVertex(layoutVertexArray, anchorPoint, tr, maxZoom, placementZoom);
addCollisionBoxVertex(layoutVertexArray, anchorPoint, br, maxZoom, placementZoom);
addCollisionBoxVertex(layoutVertexArray, anchorPoint, 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, line, shapedTextOrientations, shapedIcon, layer, addToBuffers, collisionBoxArray, featureIndex, sourceLayerIndex, bucketIndex,
textBoxScale, textPadding, textAlongLine,
iconBoxScale, iconPadding, iconAlongLine, globalProperties, featureProperties) {
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],
textBoxScale, line, 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, iconBoxScale, line, 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,
anchor,
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 = {
isFeatureConstant: layer.isLayoutValueFeatureConstant(sizeProperty),
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 getSizeAttributeDeclarations(layer, sizeProperty) {
// The contents of the a_size vertex attribute depend on the type of
// property value for {text,icon}-size.
if (
layer.isLayoutValueZoomConstant(sizeProperty) &&
!layer.isLayoutValueFeatureConstant(sizeProperty)
) {
// source function: one size value per vertex
return [{
name: 'a_size', components: 1, type: 'Uint16'
}];
} else if (
!layer.isLayoutValueZoomConstant(sizeProperty) &&
!layer.isLayoutValueFeatureConstant(sizeProperty)
) {
// composite function:
// [ text-size(lowerZoomStop, feature),
// text-size(upperZoomStop, feature),
// layoutSize == text-size(layoutZoomLevel, feature) ]
return [{
name: 'a_size', components: 3, type: 'Uint16'
}];
}
// constant or camera function
return [];
}
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),
10 * layer.getLayoutValue(sizeProperty, {zoom: 1 + tileZoom}, 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;
module.exports = SymbolBucket;