mapbox-gl
Version:
A WebGL interactive maps library
435 lines (386 loc) • 18.1 kB
JavaScript
'use strict';
const Point = require('point-geometry');
module.exports = {
getIconQuads: getIconQuads,
getGlyphQuads: getGlyphQuads,
SymbolQuad: SymbolQuad
};
const minScale = 0.5; // underscale by 1 zoom level
/**
* A textured quad for rendering a single icon or glyph.
*
* The zoom range the glyph can be shown is defined by minScale and maxScale.
*
* @param {Point} anchorPoint the point the symbol is anchored around
* @param {Point} tl The offset of the top left corner from the anchor.
* @param {Point} tr The offset of the top right corner from the anchor.
* @param {Point} bl The offset of the bottom left corner from the anchor.
* @param {Point} br The offset of the bottom right corner from the anchor.
* @param {Object} tex The texture coordinates.
* @param {number} anchorAngle The angle of the label at it's center, not the angle of this quad.
* @param {number} glyphAngle The angle of the glyph to be positioned in the quad.
* @param {number} minScale The minimum scale, relative to the tile's intended scale, that the glyph can be shown at.
* @param {number} maxScale The maximum scale, relative to the tile's intended scale, that the glyph can be shown at.
*
* @class SymbolQuad
* @private
*/
function SymbolQuad(anchorPoint, tl, tr, bl, br, tex, anchorAngle, glyphAngle, minScale, maxScale, writingMode) {
this.anchorPoint = anchorPoint;
this.tl = tl;
this.tr = tr;
this.bl = bl;
this.br = br;
this.tex = tex;
this.anchorAngle = anchorAngle;
this.glyphAngle = glyphAngle;
this.minScale = minScale;
this.maxScale = maxScale;
this.writingMode = writingMode;
}
/**
* Create the quads used for rendering an icon.
*
* @param {Anchor} anchor
* @param {PositionedIcon} shapedIcon
* @param {number} boxScale A magic number for converting glyph metric units to geometry units.
* @param {Array<Array<Point>>} line
* @param {StyleLayer} layer
* @param {boolean} alongLine Whether the icon should be placed along the line.
* @param {Shaping} shapedText Shaping for corresponding text
* @param {Object} globalProperties
* @param {Object} featureProperties
* @returns {Array<SymbolQuad>}
* @private
*/
function getIconQuads(anchor, shapedIcon, boxScale, line, layer, alongLine, shapedText, globalProperties, featureProperties) {
const rect = shapedIcon.image.rect;
const layout = layer.layout;
const border = 1;
const left = shapedIcon.left - border;
const right = left + rect.w / shapedIcon.image.pixelRatio;
const top = shapedIcon.top - border;
const bottom = top + rect.h / shapedIcon.image.pixelRatio;
let tl, tr, br, bl;
// text-fit mode
if (layout['icon-text-fit'] !== 'none' && shapedText) {
const iconWidth = (right - left),
iconHeight = (bottom - top),
size = layout['text-size'] / 24,
textLeft = shapedText.left * size,
textRight = shapedText.right * size,
textTop = shapedText.top * size,
textBottom = shapedText.bottom * size,
textWidth = textRight - textLeft,
textHeight = textBottom - textTop,
padT = layout['icon-text-fit-padding'][0],
padR = layout['icon-text-fit-padding'][1],
padB = layout['icon-text-fit-padding'][2],
padL = layout['icon-text-fit-padding'][3],
offsetY = layout['icon-text-fit'] === 'width' ? (textHeight - iconHeight) * 0.5 : 0,
offsetX = layout['icon-text-fit'] === 'height' ? (textWidth - iconWidth) * 0.5 : 0,
width = layout['icon-text-fit'] === 'width' || layout['icon-text-fit'] === 'both' ? textWidth : iconWidth,
height = layout['icon-text-fit'] === 'height' || layout['icon-text-fit'] === 'both' ? textHeight : iconHeight;
tl = new Point(textLeft + offsetX - padL, textTop + offsetY - padT);
tr = new Point(textLeft + offsetX + padR + width, textTop + offsetY - padT);
br = new Point(textLeft + offsetX + padR + width, textTop + offsetY + padB + height);
bl = new Point(textLeft + offsetX - padL, textTop + offsetY + padB + height);
// Normal icon size mode
} else {
tl = new Point(left, top);
tr = new Point(right, top);
br = new Point(right, bottom);
bl = new Point(left, bottom);
}
let angle = layer.getLayoutValue('icon-rotate', globalProperties, featureProperties) * Math.PI / 180;
if (alongLine) {
const prev = line[anchor.segment];
if (anchor.y === prev.y && anchor.x === prev.x && anchor.segment + 1 < line.length) {
const next = line[anchor.segment + 1];
angle += Math.atan2(anchor.y - next.y, anchor.x - next.x) + Math.PI;
} else {
angle += Math.atan2(anchor.y - prev.y, anchor.x - prev.x);
}
}
if (angle) {
const sin = Math.sin(angle),
cos = Math.cos(angle),
matrix = [cos, -sin, sin, cos];
tl = tl.matMult(matrix);
tr = tr.matMult(matrix);
bl = bl.matMult(matrix);
br = br.matMult(matrix);
}
return [new SymbolQuad(new Point(anchor.x, anchor.y), tl, tr, bl, br, shapedIcon.image.rect, 0, 0, minScale, Infinity)];
}
/**
* Create the quads used for rendering a text label.
*
* @param {Anchor} anchor
* @param {Shaping} shaping
* @param {number} boxScale A magic number for converting from glyph metric units to geometry units.
* @param {Array<Array<Point>>} line
* @param {StyleLayer} layer
* @param {boolean} alongLine Whether the label should be placed along the line.
* @param {Object} globalProperties
* @param {Object} featureProperties
* @returns {Array<SymbolQuad>}
* @private
*/
function getGlyphQuads(anchor, shaping, boxScale, line, layer, alongLine, globalProperties, featureProperties) {
const textRotate = layer.getLayoutValue('text-rotate', globalProperties, featureProperties) * Math.PI / 180;
const keepUpright = layer.layout['text-keep-upright'];
const positionedGlyphs = shaping.positionedGlyphs;
const quads = [];
for (let k = 0; k < positionedGlyphs.length; k++) {
const positionedGlyph = positionedGlyphs[k];
const glyph = positionedGlyph.glyph;
if (!glyph) continue;
const rect = glyph.rect;
if (!rect) continue;
const centerX = (positionedGlyph.x + glyph.advance / 2) * boxScale;
let glyphInstances;
let labelMinScale = minScale;
if (alongLine) {
glyphInstances = [];
labelMinScale = getLineGlyphs(glyphInstances, anchor, centerX, line, anchor.segment, false);
if (keepUpright) {
labelMinScale = Math.min(labelMinScale, getLineGlyphs(glyphInstances, anchor, centerX, line, anchor.segment, true));
}
} else {
glyphInstances = [{
anchorPoint: new Point(anchor.x, anchor.y),
upsideDown: false,
angle: 0,
maxScale: Infinity,
minScale: minScale
}];
}
const x1 = positionedGlyph.x + glyph.left;
const y1 = positionedGlyph.y - glyph.top;
const x2 = x1 + rect.w;
const y2 = y1 + rect.h;
const center = new Point(positionedGlyph.x, glyph.advance / 2);
const otl = new Point(x1, y1);
const otr = new Point(x2, y1);
const obl = new Point(x1, y2);
const obr = new Point(x2, y2);
if (positionedGlyph.angle !== 0) {
otl._sub(center)._rotate(positionedGlyph.angle)._add(center);
otr._sub(center)._rotate(positionedGlyph.angle)._add(center);
obl._sub(center)._rotate(positionedGlyph.angle)._add(center);
obr._sub(center)._rotate(positionedGlyph.angle)._add(center);
}
for (let i = 0; i < glyphInstances.length; i++) {
const instance = glyphInstances[i];
let tl = otl,
tr = otr,
bl = obl,
br = obr;
if (textRotate) {
const sin = Math.sin(textRotate),
cos = Math.cos(textRotate),
matrix = [cos, -sin, sin, cos];
tl = tl.matMult(matrix);
tr = tr.matMult(matrix);
bl = bl.matMult(matrix);
br = br.matMult(matrix);
}
// Prevent label from extending past the end of the line
const glyphMinScale = Math.max(instance.minScale, labelMinScale);
// All the glyphs for a label are tagged with either the "right side up" or "upside down" anchor angle,
// which is used at placement time to determine which set to show
const anchorAngle = (anchor.angle + (instance.upsideDown ? Math.PI : 0.0) + 2 * Math.PI) % (2 * Math.PI);
const glyphAngle = (instance.angle + (instance.upsideDown ? Math.PI : 0.0) + 2 * Math.PI) % (2 * Math.PI);
quads.push(new SymbolQuad(instance.anchorPoint, tl, tr, bl, br, rect, anchorAngle, glyphAngle, glyphMinScale, instance.maxScale, shaping.writingMode));
}
}
return quads;
}
/**
* We can only render glyph quads that slide along a straight line. To draw
* curved lines we need an instance of a glyph for each segment it appears on.
* This creates all the instances of a glyph that are necessary to render a label.
*
* Given (1) a glyph positioned relative to an anchor point and (2) a line to follow,
* calculates which segment of the line the glyph will fall on for each possible
* scale range, and for each range produces a "virtual" anchor point and an angle that will
* place the glyph on the right segment and rotated to the correct angle.
*
* Because one glyph quad is made ahead of time for each possible orientation, the
* symbol_sdf shader can quickly handle changing layout as we zoom in and out
*
* If the "keepUpright" property is set, we call getLineGlyphs twice (once upright and
* once "upside down"). This will generate two sets of glyphs following the line in opposite
* directions. Later, SymbolLayout::place will look at the glyphs and based on the placement
* angle determine if their original anchor was "upright" or not -- based on that, it throws
* away one set of glyphs or the other (this work has to be done in the CPU, but it's just a
* filter so it's fast)
*
* We need a
* @param {Array<Object>} glyphs An empty array that glyphInstances are added to.
* @param {Anchor} anchor
* @param {number} glyphHorizontalOffsetFromAnchor The glyph's offset from the center of the label.
* @param {Array<Point>} line
* @param {number} anchorSegment The index of the segment of the line on which the anchor exists.
* @param {boolean} upsideDown
*
* @returns {number} minScale
* @private
*/
function getLineGlyphs(glyphs, anchor, glyphHorizontalOffsetFromAnchor, line, anchorSegment, upsideDown) {
// This is true if the glyph is "logically forward" of the anchor point, based on the ordering of line segments
// The actual angle of the line is irrelevant
// If "upsideDown" is set, everything is flipped
const glyphIsLogicallyForward = (glyphHorizontalOffsetFromAnchor >= 0) ^ upsideDown;
const glyphDistanceFromAnchor = Math.abs(glyphHorizontalOffsetFromAnchor);
const initialSegmentAnchor = new Point(anchor.x, anchor.y);
const initialSegmentEnd = getSegmentEnd(glyphIsLogicallyForward, line, anchorSegment);
let virtualSegment = {
anchor: initialSegmentAnchor,
end: initialSegmentEnd,
index: anchorSegment,
minScale: getMinScaleForSegment(glyphDistanceFromAnchor, initialSegmentAnchor, initialSegmentEnd),
maxScale: Infinity
};
while (true) {
insertSegmentGlyph(glyphs,
virtualSegment,
glyphIsLogicallyForward,
upsideDown);
if (virtualSegment.minScale <= anchor.scale) {
// No need to calculate below the scale where the label starts showing
return anchor.scale;
}
const nextVirtualSegment = getNextVirtualSegment(virtualSegment,
line,
glyphDistanceFromAnchor,
glyphIsLogicallyForward);
if (!nextVirtualSegment) {
// There are no more segments, so we can't fit this glyph on the line at a lower scale
// This implies we can't show the label at all at lower scale, so we update the anchor's min scale
return virtualSegment.minScale;
} else {
virtualSegment = nextVirtualSegment;
}
}
}
/**
* @param {Array<Object>} glyphs
* @param {Object} virtualSegment
* @param {boolean} glyphIsLogicallyForward
* @param {boolean} upsideDown
* @private
*/
function insertSegmentGlyph(glyphs, virtualSegment, glyphIsLogicallyForward, upsideDown) {
const segmentAngle = Math.atan2(virtualSegment.end.y - virtualSegment.anchor.y, virtualSegment.end.x - virtualSegment.anchor.x);
// If !glyphIsLogicallyForward, we're iterating through the segments in reverse logical order as well, so we need to flip the segment angle
const glyphAngle = glyphIsLogicallyForward ? segmentAngle : segmentAngle + Math.PI;
// Insert a glyph rotated at this angle for display in the range from [scale, previous(larger) scale].
glyphs.push({
anchorPoint: virtualSegment.anchor,
upsideDown: upsideDown,
minScale: virtualSegment.minScale,
maxScale: virtualSegment.maxScale,
angle: (glyphAngle + 2.0 * Math.PI) % (2.0 * Math.PI)});
}
/**
* Given the distance along the line from the label anchor to the beginning of the current segment,
* project a "virtual anchor" point at the same distance along the line extending out from this segment.
*
* B <-- beginning of current segment
* * . . . . . . . *--------* E <-- end of current segment
* VA |
* / VA = "virtual segment anchor"
* /
* ---*-----`
* A = label anchor
*
* Distance _along line_ from A to B == straight-line distance from VA to B.
*
* @param {Point} segmentBegin
* @param {Point} segmentEnd
* @param {number} distanceFromAnchorToSegmentBegin
*
* @returns {Point} virtualSegmentAnchor
* @private
*/
function getVirtualSegmentAnchor(segmentBegin, segmentEnd, distanceFromAnchorToSegmentBegin) {
const segmentDirectionUnitVector = segmentEnd.sub(segmentBegin)._unit();
return segmentBegin.sub(segmentDirectionUnitVector._mult(distanceFromAnchorToSegmentBegin));
}
/**
* Given the segment joining `segmentAnchor` and `segmentEnd` and a desired offset
* `glyphDistanceFromAnchor` at which a glyph is to be placed, calculate the minimum
* "scale" at which the glyph will fall on the segment (i.e., not past the end)
*
* "Scale" here refers to the ratio between the *rendered* zoom level and the text-layout
* zoom level, which is 1 + (source tile's zoom level). `glyphDistanceFromAnchor`, although
* passed in units consistent with the text-layout zoom level, is based on text size. So
* when the tile is being rendered at z < text-layout zoom, the glyph's actual distance from
* the anchor is larger relative to the segment's length than at layout time:
*
*
* GLYPH
* z == layout-zoom, scale == 1: segmentAnchor *--------------^-------------* segmentEnd
* z == layout-zoom - 1, scale == 0.5: segmentAnchor *--------------^* segmentEnd
*
* <-------------->
* Anchor-to-glyph distance stays visually fixed,
* so it changes relative to the segment.
* @param {number} glyphDistanceFromAnchor
* @param {Point} segmentAnchor
* @param {Point} segmentEnd
* @returns {number} minScale
* @private
*/
function getMinScaleForSegment(glyphDistanceFromAnchor, segmentAnchor, segmentEnd) {
const distanceFromAnchorToEnd = segmentAnchor.dist(segmentEnd);
return glyphDistanceFromAnchor / distanceFromAnchorToEnd;
}
/**
* @param {boolean} glyphIsLogicallyForward
* @param {Array<Point>} line
* @param {number} segmentIndex
*
* @returns {Point} segmentEnd
* @private
*/
function getSegmentEnd(glyphIsLogicallyForward, line, segmentIndex) {
return glyphIsLogicallyForward ? line[segmentIndex + 1] : line[segmentIndex];
}
/**
* @param {Object} previousVirtualSegment
* @param {Array<Point>} line
* @param {number} glyphDistanceFromAnchor
* @param {boolean} glyphIsLogicallyForward
* @returns {Object} virtualSegment
* @private
*/
function getNextVirtualSegment(previousVirtualSegment, line, glyphDistanceFromAnchor, glyphIsLogicallyForward) {
const nextSegmentBegin = previousVirtualSegment.end;
let end = nextSegmentBegin;
let index = previousVirtualSegment.index;
// skip duplicate nodes
while (end.equals(nextSegmentBegin)) {
// look ahead by 2 points in the line because the segment index refers to the beginning
// of the segment, and we need an endpoint too
if (glyphIsLogicallyForward && (index + 2 < line.length)) {
index += 1;
} else if (!glyphIsLogicallyForward && index !== 0) {
index -= 1;
} else {
return null;
}
end = getSegmentEnd(glyphIsLogicallyForward, line, index);
}
const anchor = getVirtualSegmentAnchor(nextSegmentBegin, end,
previousVirtualSegment.anchor.dist(previousVirtualSegment.end));
return {
anchor: anchor,
end: end,
index: index,
minScale: getMinScaleForSegment(glyphDistanceFromAnchor, anchor, end),
maxScale: previousVirtualSegment.minScale
};
}