mapbox-gl
Version:
A WebGL interactive maps library
693 lines (607 loc) • 32 kB
JavaScript
// @flow
import Point from '@mapbox/point-geometry';
import {mat2, mat4, vec3, vec4} from 'gl-matrix';
import * as symbolSize from './symbol_size.js';
import {addDynamicAttributes, updateGlobeVertexNormal} from '../data/bucket/symbol_bucket.js';
import type Projection from '../geo/projection/projection.js';
import type Painter from '../render/painter.js';
import type Transform from '../geo/transform.js';
import type SymbolBucket from '../data/bucket/symbol_bucket.js';
import type {
GlyphOffsetArray,
SymbolLineVertexArray,
SymbolDynamicLayoutArray,
SymbolGlobeExtArray,
PlacedSymbol
} from '../data/array_types.js';
import type {Mat4, Vec3, Vec4} from 'gl-matrix';
import {WritingMode} from '../symbol/shaping.js';
import {CanonicalTileID, OverscaledTileID} from '../source/tile_id.js';
import {calculateGlobeLabelMatrix} from '../geo/projection/globe_util.js';
export {updateLineLabels, hideGlyphs, getLabelPlaneMatrixForRendering, getLabelPlaneMatrixForPlacement, getGlCoordMatrix, project, projectClamped, getPerspectiveRatio, placeFirstAndLastGlyph, placeGlyphAlongLine, xyTransformMat4};
type PlacedGlyph = {|
angle: number,
path: Array<Vec3>,
point: Vec3,
tilePath: Array<Point>,
up: Vec3
|};
type ProjectionCache = {[_: number]: Vec3};
type PlacementStatus = {
needsFlipping?: boolean,
notEnoughRoom?: boolean,
useVertical?: boolean
};
const FlipState = {
unknown: 0,
flipRequired: 1,
flipNotRequired: 2
};
const maxTangent = Math.tan(85 * Math.PI / 180);
/*
* # Overview of coordinate spaces
*
* ## Tile coordinate spaces
* Each label has an anchor. Some labels have corresponding line geometries.
* The points for both anchors and lines are stored in tile units. Each tile has it's own
* coordinate space going from (0, 0) at the top left to (EXTENT, EXTENT) at the bottom right.
*
* ## GL coordinate space
* At the end of everything, the vertex shader needs to produce a position in GL coordinate space,
* which is (-1, 1) at the top left and (1, -1) in the bottom right.
*
* ## Map pixel coordinate spaces
* Each tile has a pixel coordinate space. It's just the tile units scaled so that one unit is
* whatever counts as 1 pixel at the current zoom.
* This space is used for pitch-alignment=map, rotation-alignment=map
*
* ## Rotated map pixel coordinate spaces
* Like the above, but rotated so axis of the space are aligned with the viewport instead of the tile.
* This space is used for pitch-alignment=map, rotation-alignment=viewport
*
* ## Viewport pixel coordinate space
* (0, 0) is at the top left of the canvas and (pixelWidth, pixelHeight) is at the bottom right corner
* of the canvas. This space is used for pitch-alignment=viewport
*
*
* # Vertex projection
* It goes roughly like this:
* 1. project the anchor and line from tile units into the correct label coordinate space
* - map pixel space pitch-alignment=map rotation-alignment=map
* - rotated map pixel space pitch-alignment=map rotation-alignment=viewport
* - viewport pixel space pitch-alignment=viewport rotation-alignment=*
* 2. if the label follows a line, find the point along the line that is the correct distance from the anchor.
* 3. add the glyph's corner offset to the point from step 3
* 4. convert from the label coordinate space to gl coordinates
*
* For horizontal labels we want to do step 1 in the shader for performance reasons (no cpu work).
* This is what `u_label_plane_matrix` is used for.
* For labels aligned with lines we have to steps 1 and 2 on the cpu since we need access to the line geometry.
* This is what `updateLineLabels(...)` does.
* Since the conversion is handled on the cpu we just set `u_label_plane_matrix` to an identity matrix.
*
* Steps 3 and 4 are done in the shaders for all labels.
*/
/*
* Returns a matrix for converting from tile units to the correct label coordinate space.
* This variation of the function returns a label space matrix specialized for rendering.
* It transforms coordinates as-is to whatever the target space is (either 2D or 3D).
* See also `getLabelPlaneMatrixForPlacement`
*/
function getLabelPlaneMatrixForRendering(posMatrix: Float32Array,
tileID: CanonicalTileID,
pitchWithMap: boolean,
rotateWithMap: boolean,
transform: Transform,
projection: Projection,
pixelsToTileUnits: Float32Array): Float32Array {
const m = mat4.create();
if (pitchWithMap) {
if (projection.name === 'globe') {
const lm = calculateGlobeLabelMatrix(transform, tileID);
mat4.multiply(m, m, lm);
} else {
const s = mat2.invert([], pixelsToTileUnits);
m[0] = s[0];
m[1] = s[1];
m[4] = s[2];
m[5] = s[3];
if (!rotateWithMap) {
mat4.rotateZ(m, m, transform.angle);
}
}
} else {
mat4.multiply(m, transform.labelPlaneMatrix, posMatrix);
}
return m;
}
/*
* Returns a matrix for converting from tile units to the correct label coordinate space.
* This variation of the function returns a matrix specialized for placement logic.
* Coordinates will be clamped to x&y 2D plane which is used with viewport and map aligned placement
* logic in most cases. Certain projections such as globe view will use 3D space for map aligned
* label placement.
*/
function getLabelPlaneMatrixForPlacement(posMatrix: Float32Array,
tileID: CanonicalTileID,
pitchWithMap: boolean,
rotateWithMap: boolean,
transform: Transform,
projection: Projection,
pixelsToTileUnits: Float32Array): Float32Array {
const m = getLabelPlaneMatrixForRendering(posMatrix, tileID, pitchWithMap, rotateWithMap, transform, projection, pixelsToTileUnits);
// Symbol placement logic is performed in 2D in most scenarios.
// For this reason project all coordinates to the xy-plane by discarding the z-component
if (projection.name !== 'globe' || !pitchWithMap) {
// Pre-multiply by scaling z to 0
m[2] = m[6] = m[10] = m[14] = 0;
}
return m;
}
/*
* Returns a matrix for converting from the correct label coordinate space to gl coords.
*/
function getGlCoordMatrix(posMatrix: Float32Array,
tileID: CanonicalTileID,
pitchWithMap: boolean,
rotateWithMap: boolean,
transform: Transform,
projection: Projection,
pixelsToTileUnits: Float32Array): Float32Array {
if (pitchWithMap) {
if (projection.name === 'globe') {
const m = getLabelPlaneMatrixForRendering(posMatrix, tileID, pitchWithMap, rotateWithMap, transform, projection, pixelsToTileUnits);
mat4.invert(m, m);
mat4.multiply(m, posMatrix, m);
return m;
} else {
const m = mat4.clone(posMatrix);
const s = mat4.identity([]);
s[0] = pixelsToTileUnits[0];
s[1] = pixelsToTileUnits[1];
s[4] = pixelsToTileUnits[2];
s[5] = pixelsToTileUnits[3];
mat4.multiply(m, m, s);
if (!rotateWithMap) {
mat4.rotateZ(m, m, -transform.angle);
}
return m;
}
} else {
return transform.glCoordMatrix;
}
}
function project(x: number, y: number, z: number, matrix: Mat4): Vec4 {
const pos = [x, y, z, 1];
if (z) {
vec4.transformMat4(pos, pos, matrix);
} else {
xyTransformMat4(pos, pos, matrix);
}
const w = pos[3];
pos[0] /= w;
pos[1] /= w;
pos[2] /= w;
return pos;
}
function projectClamped([x, y, z]: Vec3, matrix: Mat4): Vec4 {
const pos = [x, y, z, 1];
vec4.transformMat4(pos, pos, matrix);
// Clamp distance to a positive value so we can avoid screen coordinate
// being flipped possibly due to perspective projection
const w = pos[3] = Math.max(pos[3], 0.000001);
pos[0] /= w;
pos[1] /= w;
pos[2] /= w;
return pos;
}
function getPerspectiveRatio(cameraToCenterDistance: number, signedDistanceFromCamera: number): number {
return Math.min(0.5 + 0.5 * (cameraToCenterDistance / signedDistanceFromCamera), 1.5);
}
function isVisible(anchorPos: [number, number, number, number],
clippingBuffer: [number, number]) {
const x = anchorPos[0] / anchorPos[3];
const y = anchorPos[1] / anchorPos[3];
const inPaddedViewport = (
x >= -clippingBuffer[0] &&
x <= clippingBuffer[0] &&
y >= -clippingBuffer[1] &&
y <= clippingBuffer[1]);
return inPaddedViewport;
}
/*
* Update the `dynamicLayoutVertexBuffer` for the buffer with the correct glyph positions for the current map view.
* This is only run on labels that are aligned with lines. Horizontal labels are handled entirely in the shader.
*/
function updateLineLabels(bucket: SymbolBucket,
posMatrix: Float32Array,
painter: Painter,
isText: boolean,
labelPlaneMatrix: Float32Array,
glCoordMatrix: Float32Array,
pitchWithMap: boolean,
keepUpright: boolean,
getElevation: ?((p: Point) => Array<number>),
tileID: OverscaledTileID) {
const tr = painter.transform;
const sizeData = isText ? bucket.textSizeData : bucket.iconSizeData;
const partiallyEvaluatedSize = symbolSize.evaluateSizeForZoom(sizeData, painter.transform.zoom);
const isGlobe = tr.projection.name === 'globe';
const clippingBuffer = [256 / painter.width * 2 + 1, 256 / painter.height * 2 + 1];
const dynamicLayoutVertexArray = isText ?
bucket.text.dynamicLayoutVertexArray :
bucket.icon.dynamicLayoutVertexArray;
dynamicLayoutVertexArray.clear();
let globeExtVertexArray: ?SymbolGlobeExtArray = null;
if (isGlobe) {
globeExtVertexArray = isText ?
bucket.text.globeExtVertexArray :
bucket.icon.globeExtVertexArray;
}
const lineVertexArray = bucket.lineVertexArray;
const placedSymbols = isText ? bucket.text.placedSymbolArray : bucket.icon.placedSymbolArray;
const aspectRatio = painter.transform.width / painter.transform.height;
let useVertical: ?boolean = false;
let prevWritingMode;
for (let s = 0; s < placedSymbols.length; s++) {
const symbol = placedSymbols.get(s);
const {numGlyphs, writingMode} = symbol;
// Normally, the 'Horizontal|Vertical' writing mode is followed by a 'Vertical' counterpart, this
// is not true for 'Vertical' only line labels. For this case, we'll have to overwrite the 'useVertical'
// status before further checks.
if (writingMode === WritingMode.vertical && !useVertical && prevWritingMode !== WritingMode.horizontal) {
useVertical = true;
}
prevWritingMode = writingMode;
// Don't do calculations for vertical glyphs unless the previous symbol was horizontal
// and we determined that vertical glyphs were necessary.
// Also don't do calculations for symbols that are collided and fully faded out
if ((symbol.hidden || writingMode === WritingMode.vertical) && !useVertical) {
hideGlyphs(numGlyphs, dynamicLayoutVertexArray);
continue;
}
// Awkward... but we're counting on the paired "vertical" symbol coming immediately after its horizontal counterpart
useVertical = false;
// Project tile anchor to globe anchor
const tileAnchorPoint = new Point(symbol.tileAnchorX, symbol.tileAnchorY);
let {x, y, z} = tr.projection.projectTilePoint(tileAnchorPoint.x, tileAnchorPoint.y, tileID.canonical);
if (getElevation) {
const [dx, dy, dz] = getElevation(tileAnchorPoint);
x += dx;
y += dy;
z += dz;
}
const anchorPos = [x, y, z, 1.0];
vec4.transformMat4(anchorPos, anchorPos, posMatrix);
// Don't bother calculating the correct point for invisible labels.
if (!isVisible(anchorPos, clippingBuffer)) {
hideGlyphs(numGlyphs, dynamicLayoutVertexArray);
continue;
}
const cameraToAnchorDistance = anchorPos[3];
const perspectiveRatio = getPerspectiveRatio(painter.transform.cameraToCenterDistance, cameraToAnchorDistance);
const fontSize = symbolSize.evaluateSizeForFeature(sizeData, partiallyEvaluatedSize, symbol);
const pitchScaledFontSize = pitchWithMap ? fontSize / perspectiveRatio : fontSize * perspectiveRatio;
const labelPlaneAnchorPoint = project(x, y, z, labelPlaneMatrix);
// Skip labels behind the camera
if (labelPlaneAnchorPoint[3] <= 0.0) {
hideGlyphs(numGlyphs, dynamicLayoutVertexArray);
continue;
}
let projectionCache: ProjectionCache = {};
const getElevationForPlacement = pitchWithMap ? null : getElevation; // When pitchWithMap, we're projecting to scaled tile coordinate space: there is no need to get elevation as it doesn't affect projection.
const placeUnflipped = placeGlyphsAlongLine(symbol, pitchScaledFontSize, false /*unflipped*/, keepUpright, posMatrix, labelPlaneMatrix, glCoordMatrix,
bucket.glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, globeExtVertexArray, labelPlaneAnchorPoint, tileAnchorPoint, projectionCache, aspectRatio, getElevationForPlacement, tr.projection, tileID, pitchWithMap);
useVertical = placeUnflipped.useVertical;
if (getElevationForPlacement && placeUnflipped.needsFlipping) projectionCache = {}; // Truncated points should be recalculated.
if (placeUnflipped.notEnoughRoom || useVertical ||
(placeUnflipped.needsFlipping &&
placeGlyphsAlongLine(symbol, pitchScaledFontSize, true /*flipped*/, keepUpright, posMatrix, labelPlaneMatrix, glCoordMatrix,
bucket.glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, globeExtVertexArray, labelPlaneAnchorPoint, tileAnchorPoint, projectionCache, aspectRatio, getElevationForPlacement, tr.projection, tileID, pitchWithMap).notEnoughRoom)) {
hideGlyphs(numGlyphs, dynamicLayoutVertexArray);
}
}
if (isText) {
bucket.text.dynamicLayoutVertexBuffer.updateData(dynamicLayoutVertexArray);
if (globeExtVertexArray) {
bucket.text.globeExtVertexBuffer.updateData(globeExtVertexArray);
}
} else {
bucket.icon.dynamicLayoutVertexBuffer.updateData(dynamicLayoutVertexArray);
if (globeExtVertexArray) {
bucket.icon.globeExtVertexBuffer.updateData(globeExtVertexArray);
}
}
}
function placeFirstAndLastGlyph(
fontScale: number,
glyphOffsetArray: GlyphOffsetArray,
lineOffsetX: number,
lineOffsetY: number,
flip: boolean,
anchorPoint: Vec3,
tileAnchorPoint: Point,
symbol: PlacedSymbol,
lineVertexArray: SymbolLineVertexArray,
labelPlaneMatrix: Float32Array,
projectionCache: ProjectionCache,
getElevation: ?((p: Point) => Array<number>),
returnPathInTileCoords: ?boolean,
projection: Projection,
tileID: OverscaledTileID,
pitchWithMap: boolean): null | {|first: PlacedGlyph, last: PlacedGlyph|} {
const {lineStartIndex, glyphStartIndex, segment} = symbol;
const glyphEndIndex = glyphStartIndex + symbol.numGlyphs;
const lineEndIndex = lineStartIndex + symbol.lineLength;
const firstGlyphOffset = glyphOffsetArray.getoffsetX(glyphStartIndex);
const lastGlyphOffset = glyphOffsetArray.getoffsetX(glyphEndIndex - 1);
const firstPlacedGlyph = placeGlyphAlongLine(fontScale * firstGlyphOffset, lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, segment,
lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache, getElevation, returnPathInTileCoords, true, projection, tileID, pitchWithMap);
if (!firstPlacedGlyph)
return null;
const lastPlacedGlyph = placeGlyphAlongLine(fontScale * lastGlyphOffset, lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, segment,
lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache, getElevation, returnPathInTileCoords, true, projection, tileID, pitchWithMap);
if (!lastPlacedGlyph)
return null;
return {first: firstPlacedGlyph, last: lastPlacedGlyph};
}
// Check in the glCoordinate space, the rough estimation of angle between the text line and the Y axis.
// If the angle if less or equal to 5 degree, then keep the text glyphs unflipped even if it is required.
function isInFlipRetainRange(dx: number, dy: number) {
return dx === 0 || Math.abs(dy / dx) > maxTangent;
}
function requiresOrientationChange(writingMode: number, flipState: number, dx: number, dy: number) {
if (writingMode === WritingMode.horizontal && Math.abs(dy) > Math.abs(dx)) {
// On top of choosing whether to flip, choose whether to render this version of the glyphs or the alternate
// vertical glyphs. We can't just filter out vertical glyphs in the horizontal range because the horizontal
// and vertical versions can have slightly different projections which could lead to angles where both or
// neither showed.
return {useVertical: true};
}
// Check if flipping is required for "verticalOnly" case.
if (writingMode === WritingMode.vertical) {
return dy > 0 ? {needsFlipping: true} : null;
}
// symbol's flipState stores the flip decision from the previous frame, and that
// decision is reused when the symbol is in the retain range.
if (flipState !== FlipState.unknown && isInFlipRetainRange(dx, dy)) {
return (flipState === FlipState.flipRequired) ? {needsFlipping: true} : null;
}
// Check if flipping is required for "horizontal" case.
return dx < 0 ? {needsFlipping: true} : null;
}
function placeGlyphsAlongLine(symbol: PlacedSymbol, fontSize: number, flip: boolean, keepUpright: boolean, posMatrix: Float32Array, labelPlaneMatrix: Float32Array, glCoordMatrix: Float32Array, glyphOffsetArray: GlyphOffsetArray, lineVertexArray: SymbolLineVertexArray, dynamicLayoutVertexArray: SymbolDynamicLayoutArray, globeExtVertexArray: ?SymbolGlobeExtArray, anchorPoint: VecType, tileAnchorPoint: Point, projectionCache: ProjectionCache, aspectRatio: number, getElevation: ?((p: Point) => Array<number>), projection: Projection, tileID: OverscaledTileID, pitchWithMap: boolean): PlacementStatus {
const fontScale = fontSize / 24;
const lineOffsetX = symbol.lineOffsetX * fontScale;
const lineOffsetY = symbol.lineOffsetY * fontScale;
const {lineStartIndex, glyphStartIndex, numGlyphs, segment, writingMode, flipState} = symbol;
const lineEndIndex = lineStartIndex + symbol.lineLength;
const addGlyph = (glyph: PlacedGlyph) => {
if (globeExtVertexArray) {
const [ux, uy, uz] = glyph.up;
const offset = dynamicLayoutVertexArray.length;
updateGlobeVertexNormal(globeExtVertexArray, offset + 0, ux, uy, uz);
updateGlobeVertexNormal(globeExtVertexArray, offset + 1, ux, uy, uz);
updateGlobeVertexNormal(globeExtVertexArray, offset + 2, ux, uy, uz);
updateGlobeVertexNormal(globeExtVertexArray, offset + 3, ux, uy, uz);
}
const [x, y, z] = glyph.point;
addDynamicAttributes(dynamicLayoutVertexArray, x, y, z, glyph.angle);
};
if (numGlyphs > 1) {
// Place the first and the last glyph in the label first, so we can figure out
// the overall orientation of the label and determine whether it needs to be flipped in keepUpright mode
const firstAndLastGlyph = placeFirstAndLastGlyph(fontScale, glyphOffsetArray, lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol, lineVertexArray, labelPlaneMatrix, projectionCache, getElevation, false, projection, tileID, pitchWithMap);
if (!firstAndLastGlyph) {
return {notEnoughRoom: true};
}
if (keepUpright && !flip) {
let [x0, y0, z0] = firstAndLastGlyph.first.point;
let [x1, y1, z1] = firstAndLastGlyph.last.point;
[x0, y0] = project(x0, y0, z0, glCoordMatrix);
[x1, y1] = project(x1, y1, z1, glCoordMatrix);
const orientationChange = requiresOrientationChange(writingMode, flipState, (x1 - x0) * aspectRatio, y1 - y0);
symbol.flipState = orientationChange && orientationChange.needsFlipping ? FlipState.flipRequired : FlipState.flipNotRequired;
if (orientationChange) {
return orientationChange;
}
}
addGlyph(firstAndLastGlyph.first);
for (let glyphIndex = glyphStartIndex + 1; glyphIndex < glyphStartIndex + numGlyphs - 1; glyphIndex++) {
// Since first and last glyph fit on the line, the rest of the glyphs can be placed too, but check to make sure
const glyph = placeGlyphAlongLine(fontScale * glyphOffsetArray.getoffsetX(glyphIndex), lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, segment,
lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache, getElevation, false, false, projection, tileID, pitchWithMap);
if (!glyph) {
// undo previous glyphs of the symbol if it doesn't fit; it will be filled with hideGlyphs instead
dynamicLayoutVertexArray.length -= 4 * (glyphIndex - glyphStartIndex);
return {notEnoughRoom: true};
}
addGlyph(glyph);
}
addGlyph(firstAndLastGlyph.last);
} else {
// Only a single glyph to place
// So, determine whether to flip based on projected angle of the line segment it's on
if (keepUpright && !flip) {
const a = project(tileAnchorPoint.x, tileAnchorPoint.y, 0, posMatrix);
const tileVertexIndex = lineStartIndex + segment + 1;
const tileSegmentEnd = new Point(lineVertexArray.getx(tileVertexIndex), lineVertexArray.gety(tileVertexIndex));
const projectedVertex = project(tileSegmentEnd.x, tileSegmentEnd.y, 0, posMatrix);
// We know the anchor will be in the viewport, but the end of the line segment may be
// behind the plane of the camera, in which case we can use a point at any arbitrary (closer)
// point on the segment.
const b = (projectedVertex[3] > 0) ?
projectedVertex :
projectTruncatedLineSegment(tileAnchorPoint, tileSegmentEnd, a, 1, posMatrix, undefined, projection, tileID.canonical);
const orientationChange = requiresOrientationChange(writingMode, flipState, (b[0] - a[0]) * aspectRatio, b[1] - a[1]);
symbol.flipState = orientationChange && orientationChange.needsFlipping ? FlipState.flipRequired : FlipState.flipNotRequired;
if (orientationChange) {
return orientationChange;
}
}
const singleGlyph = placeGlyphAlongLine(fontScale * glyphOffsetArray.getoffsetX(glyphStartIndex), lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, segment,
lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache, getElevation, false, false, projection, tileID, pitchWithMap);
if (!singleGlyph) {
return {notEnoughRoom: true};
}
addGlyph(singleGlyph);
}
return {};
}
function elevatePointAndProject(p: Point, tileID: CanonicalTileID, posMatrix: Float32Array, projection: Projection, getElevation: ?((p: Point) => Array<number>)) {
const {x, y, z} = projection.projectTilePoint(p.x, p.y, tileID);
if (!getElevation) {
return project(x, y, z, posMatrix);
}
const [dx, dy, dz] = getElevation(p);
return project(x + dx, y + dy, z + dz, posMatrix);
}
function projectTruncatedLineSegment(previousTilePoint: Point, currentTilePoint: Point, previousProjectedPoint: Vec3, minimumLength: number, projectionMatrix: Float32Array, getElevation: ?((p: Point) => Array<number>), projection: Projection, tileID: CanonicalTileID): Vec3 {
// We are assuming "previousTilePoint" won't project to a point within one unit of the camera plane
// If it did, that would mean our label extended all the way out from within the viewport to a (very distant)
// point near the plane of the camera. We wouldn't be able to render the label anyway once it crossed the
// plane of the camera.
const unitVertex = previousTilePoint.sub(currentTilePoint)._unit()._add(previousTilePoint);
const projectedUnit = elevatePointAndProject(unitVertex, tileID, projectionMatrix, projection, getElevation);
vec3.sub(projectedUnit, previousProjectedPoint, projectedUnit);
vec3.normalize(projectedUnit, projectedUnit);
return vec3.scaleAndAdd(projectedUnit, previousProjectedPoint, projectedUnit, minimumLength);
}
function placeGlyphAlongLine(
offsetX: number,
lineOffsetX: number,
lineOffsetY: number,
flip: boolean,
anchorPoint: Vec3,
tileAnchorPoint: Point,
anchorSegment: number,
lineStartIndex: number,
lineEndIndex: number,
lineVertexArray: SymbolLineVertexArray,
labelPlaneMatrix: Float32Array,
projectionCache: ProjectionCache,
getElevation: ?((p: Point) => Array<number>),
returnPathInTileCoords: ?boolean,
endGlyph: ?boolean,
reprojection: Projection,
tileID: OverscaledTileID,
pitchWithMap: boolean): null | PlacedGlyph {
const combinedOffsetX = flip ?
offsetX - lineOffsetX :
offsetX + lineOffsetX;
let dir = combinedOffsetX > 0 ? 1 : -1;
let angle = 0;
if (flip) {
// The label needs to be flipped to keep text upright.
// Iterate in the reverse direction.
dir *= -1;
angle = Math.PI;
}
if (dir < 0) angle += Math.PI;
let currentIndex = lineStartIndex + anchorSegment + (dir > 0 ? 0 : 1) | 0;
let current = anchorPoint;
let prev = anchorPoint;
let distanceToPrev = 0;
let currentSegmentDistance = 0;
const absOffsetX = Math.abs(combinedOffsetX);
const pathVertices = [];
const tilePath = [];
let currentVertex = tileAnchorPoint;
let prevVertex = currentVertex;
const getTruncatedLineSegment = () => {
return projectTruncatedLineSegment(prevVertex, currentVertex, prev, absOffsetX - distanceToPrev + 1, labelPlaneMatrix, getElevation, reprojection, tileID.canonical);
};
while (distanceToPrev + currentSegmentDistance <= absOffsetX) {
currentIndex += dir;
// offset does not fit on the projected line
if (currentIndex < lineStartIndex || currentIndex >= lineEndIndex)
return null;
prev = current;
prevVertex = currentVertex;
pathVertices.push(prev);
if (returnPathInTileCoords) tilePath.push(prevVertex);
currentVertex = new Point(lineVertexArray.getx(currentIndex), lineVertexArray.gety(currentIndex));
current = projectionCache[currentIndex];
if (!current) {
const projection = elevatePointAndProject(currentVertex, tileID.canonical, labelPlaneMatrix, reprojection, getElevation);
if (projection[3] > 0) {
current = projectionCache[currentIndex] = projection;
} else {
// The vertex is behind the plane of the camera, so we can't project it
// Instead, we'll create a vertex along the line that's far enough to include the glyph
// Don't cache because the new vertex might not be far enough out for future glyphs on the same segment
current = getTruncatedLineSegment();
}
}
distanceToPrev += currentSegmentDistance;
currentSegmentDistance = vec3.distance(prev, current);
}
if (endGlyph && getElevation) {
// For terrain, always truncate end points in order to handle terrain curvature.
// If previously truncated, on signedDistanceFromCamera < 0, don't do it.
// Cache as end point. The cache is cleared if there is need for flipping in updateLineLabels.
if (projectionCache[currentIndex]) {
current = getTruncatedLineSegment();
currentSegmentDistance = vec3.distance(prev, current);
}
projectionCache[currentIndex] = current;
}
// The point is on the current segment. Interpolate to find it. Compute points on both label plane and tile space
const segmentInterpolationT = (absOffsetX - distanceToPrev) / currentSegmentDistance;
const tilePoint = currentVertex.sub(prevVertex)._mult(segmentInterpolationT)._add(prevVertex);
const prevToCurrent = vec3.sub([], current, prev);
const labelPlanePoint = vec3.scaleAndAdd([], prev, prevToCurrent, segmentInterpolationT);
let axisZ: Vec3 = [0, 0, 1];
let diffX = prevToCurrent[0];
let diffY = prevToCurrent[1];
if (pitchWithMap) {
axisZ = reprojection.upVector(tileID.canonical, tilePoint.x, tilePoint.y);
if (axisZ[0] !== 0 || axisZ[1] !== 0 || axisZ[2] !== 1) {
// Compute coordinate frame that is aligned to the tangent of the surface
const axisX = [axisZ[2], 0, -axisZ[0]];
const axisY = vec3.cross([], axisZ, axisX);
vec3.normalize(axisX, axisX);
vec3.normalize(axisY, axisY);
diffX = vec3.dot(prevToCurrent, axisX);
diffY = vec3.dot(prevToCurrent, axisY);
}
}
// offset the point from the line to text-offset and icon-offset
if (lineOffsetY) {
// Find a coordinate frame for the vertical offset
const offsetDir = vec3.cross([], axisZ, prevToCurrent);
vec3.normalize(offsetDir, offsetDir);
vec3.scaleAndAdd(labelPlanePoint, labelPlanePoint, offsetDir, lineOffsetY * dir);
}
const segmentAngle = angle + Math.atan2(diffY, diffX);
pathVertices.push(labelPlanePoint);
if (returnPathInTileCoords) {
tilePath.push(tilePoint);
}
return {
point: labelPlanePoint,
angle: segmentAngle,
path: pathVertices,
tilePath,
up: axisZ
};
}
// Hide them by moving them offscreen. We still need to add them to the buffer
// because the dynamic buffer is paired with a static buffer that doesn't get updated.
function hideGlyphs(num: number, dynamicLayoutVertexArray: SymbolDynamicLayoutArray) {
const offset = dynamicLayoutVertexArray.length;
const end = offset + 4 * num;
dynamicLayoutVertexArray.resize(end);
// Since all hidden glyphs have the same attributes, we can build up the array faster with a single call to
// Float32Array.fill for all vertices, instead of calling addDynamicAttributes for each vertex.
dynamicLayoutVertexArray.float32.fill(-Infinity, offset * 4, end * 4);
}
// For line label layout, we're not using z output and our w input is always 1
// This custom matrix transformation ignores those components to make projection faster
function xyTransformMat4(out: Vec4, a: Vec4, m: Mat4): Vec4 {
const x = a[0], y = a[1];
out[0] = m[0] * x + m[4] * y + m[12];
out[1] = m[1] * x + m[5] * y + m[13];
out[3] = m[3] * x + m[7] * y + m[15];
return out;
}