Spaces:
Sleeping
Sleeping
| // @flow | |
| import Point from '@mapbox/point-geometry'; | |
| import {mat4, vec4} from 'gl-matrix'; | |
| import * as symbolSize from './symbol_size'; | |
| import {addDynamicAttributes} from '../data/bucket/symbol_bucket'; | |
| import type Painter from '../render/painter'; | |
| import type Transform from '../geo/transform'; | |
| import type SymbolBucket from '../data/bucket/symbol_bucket'; | |
| import type { | |
| GlyphOffsetArray, | |
| SymbolLineVertexArray, | |
| SymbolDynamicLayoutArray | |
| } from '../data/array_types'; | |
| import {WritingMode} from '../symbol/shaping'; | |
| export {updateLineLabels, hideGlyphs, getLabelPlaneMatrix, getGlCoordMatrix, project, getPerspectiveRatio, placeFirstAndLastGlyph, placeGlyphAlongLine, xyTransformMat4}; | |
| /* | |
| * # 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. | |
| */ | |
| function getLabelPlaneMatrix(posMatrix: mat4, | |
| pitchWithMap: boolean, | |
| rotateWithMap: boolean, | |
| transform: Transform, | |
| pixelsToTileUnits: number) { | |
| const m = mat4.create(); | |
| if (pitchWithMap) { | |
| mat4.scale(m, m, [1 / pixelsToTileUnits, 1 / pixelsToTileUnits, 1]); | |
| if (!rotateWithMap) { | |
| mat4.rotateZ(m, m, transform.angle); | |
| } | |
| } else { | |
| mat4.multiply(m, transform.labelPlaneMatrix, posMatrix); | |
| } | |
| return m; | |
| } | |
| /* | |
| * Returns a matrix for converting from the correct label coordinate space to gl coords. | |
| */ | |
| function getGlCoordMatrix(posMatrix: mat4, | |
| pitchWithMap: boolean, | |
| rotateWithMap: boolean, | |
| transform: Transform, | |
| pixelsToTileUnits: number) { | |
| if (pitchWithMap) { | |
| const m = mat4.clone(posMatrix); | |
| mat4.scale(m, m, [pixelsToTileUnits, pixelsToTileUnits, 1]); | |
| if (!rotateWithMap) { | |
| mat4.rotateZ(m, m, -transform.angle); | |
| } | |
| return m; | |
| } else { | |
| return transform.glCoordMatrix; | |
| } | |
| } | |
| function project(point: Point, matrix: mat4) { | |
| const pos = [point.x, point.y, 0, 1]; | |
| xyTransformMat4(pos, pos, matrix); | |
| const w = pos[3]; | |
| return { | |
| point: new Point(pos[0] / w, pos[1] / w), | |
| signedDistanceFromCamera: w | |
| }; | |
| } | |
| function getPerspectiveRatio(cameraToCenterDistance: number, signedDistanceFromCamera: number): number { | |
| return 0.5 + 0.5 * (cameraToCenterDistance / signedDistanceFromCamera); | |
| } | |
| 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: mat4, | |
| painter: Painter, | |
| isText: boolean, | |
| labelPlaneMatrix: mat4, | |
| glCoordMatrix: mat4, | |
| pitchWithMap: boolean, | |
| keepUpright: boolean) { | |
| const sizeData = isText ? bucket.textSizeData : bucket.iconSizeData; | |
| const partiallyEvaluatedSize = symbolSize.evaluateSizeForZoom(sizeData, painter.transform.zoom); | |
| const clippingBuffer = [256 / painter.width * 2 + 1, 256 / painter.height * 2 + 1]; | |
| const dynamicLayoutVertexArray = isText ? | |
| bucket.text.dynamicLayoutVertexArray : | |
| bucket.icon.dynamicLayoutVertexArray; | |
| dynamicLayoutVertexArray.clear(); | |
| const lineVertexArray = bucket.lineVertexArray; | |
| const placedSymbols = isText ? bucket.text.placedSymbolArray : bucket.icon.placedSymbolArray; | |
| const aspectRatio = painter.transform.width / painter.transform.height; | |
| let useVertical = false; | |
| for (let s = 0; s < placedSymbols.length; s++) { | |
| const symbol: any = placedSymbols.get(s); | |
| // 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 || symbol.writingMode === WritingMode.vertical && !useVertical) { | |
| hideGlyphs(symbol.numGlyphs, dynamicLayoutVertexArray); | |
| continue; | |
| } | |
| // Awkward... but we're counting on the paired "vertical" symbol coming immediately after its horizontal counterpart | |
| useVertical = false; | |
| const anchorPos = [symbol.anchorX, symbol.anchorY, 0, 1]; | |
| vec4.transformMat4(anchorPos, anchorPos, posMatrix); | |
| // Don't bother calculating the correct point for invisible labels. | |
| if (!isVisible(anchorPos, clippingBuffer)) { | |
| hideGlyphs(symbol.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 tileAnchorPoint = new Point(symbol.anchorX, symbol.anchorY); | |
| const anchorPoint = project(tileAnchorPoint, labelPlaneMatrix).point; | |
| const projectionCache = {}; | |
| const placeUnflipped: any = placeGlyphsAlongLine(symbol, pitchScaledFontSize, false /*unflipped*/, keepUpright, posMatrix, labelPlaneMatrix, glCoordMatrix, | |
| bucket.glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, anchorPoint, tileAnchorPoint, projectionCache, aspectRatio); | |
| useVertical = placeUnflipped.useVertical; | |
| if (placeUnflipped.notEnoughRoom || useVertical || | |
| (placeUnflipped.needsFlipping && | |
| placeGlyphsAlongLine(symbol, pitchScaledFontSize, true /*flipped*/, keepUpright, posMatrix, labelPlaneMatrix, glCoordMatrix, | |
| bucket.glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, anchorPoint, tileAnchorPoint, projectionCache, aspectRatio).notEnoughRoom)) { | |
| hideGlyphs(symbol.numGlyphs, dynamicLayoutVertexArray); | |
| } | |
| } | |
| if (isText) { | |
| bucket.text.dynamicLayoutVertexBuffer.updateData(dynamicLayoutVertexArray); | |
| } else { | |
| bucket.icon.dynamicLayoutVertexBuffer.updateData(dynamicLayoutVertexArray); | |
| } | |
| } | |
| function placeFirstAndLastGlyph(fontScale: number, glyphOffsetArray: GlyphOffsetArray, lineOffsetX: number, lineOffsetY: number, flip: boolean, anchorPoint: Point, tileAnchorPoint: Point, symbol: any, lineVertexArray: SymbolLineVertexArray, labelPlaneMatrix: mat4, projectionCache: any) { | |
| const glyphEndIndex = symbol.glyphStartIndex + symbol.numGlyphs; | |
| const lineStartIndex = symbol.lineStartIndex; | |
| const lineEndIndex = symbol.lineStartIndex + symbol.lineLength; | |
| const firstGlyphOffset = glyphOffsetArray.getoffsetX(symbol.glyphStartIndex); | |
| const lastGlyphOffset = glyphOffsetArray.getoffsetX(glyphEndIndex - 1); | |
| const firstPlacedGlyph = placeGlyphAlongLine(fontScale * firstGlyphOffset, lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol.segment, | |
| lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache); | |
| if (!firstPlacedGlyph) | |
| return null; | |
| const lastPlacedGlyph = placeGlyphAlongLine(fontScale * lastGlyphOffset, lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol.segment, | |
| lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache); | |
| if (!lastPlacedGlyph) | |
| return null; | |
| return {first: firstPlacedGlyph, last: lastPlacedGlyph}; | |
| } | |
| function requiresOrientationChange(writingMode, firstPoint, lastPoint, aspectRatio) { | |
| if (writingMode === WritingMode.horizontal) { | |
| // 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. | |
| const rise = Math.abs(lastPoint.y - firstPoint.y); | |
| const run = Math.abs(lastPoint.x - firstPoint.x) * aspectRatio; | |
| if (rise > run) { | |
| return {useVertical: true}; | |
| } | |
| } | |
| if (writingMode === WritingMode.vertical ? firstPoint.y < lastPoint.y : firstPoint.x > lastPoint.x) { | |
| // Includes "horizontalOnly" case for labels without vertical glyphs | |
| return {needsFlipping: true}; | |
| } | |
| return null; | |
| } | |
| function placeGlyphsAlongLine(symbol, fontSize, flip, keepUpright, posMatrix, labelPlaneMatrix, glCoordMatrix, glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, anchorPoint, tileAnchorPoint, projectionCache, aspectRatio) { | |
| const fontScale = fontSize / 24; | |
| const lineOffsetX = symbol.lineOffsetX * fontScale; | |
| const lineOffsetY = symbol.lineOffsetY * fontScale; | |
| let placedGlyphs; | |
| if (symbol.numGlyphs > 1) { | |
| const glyphEndIndex = symbol.glyphStartIndex + symbol.numGlyphs; | |
| const lineStartIndex = symbol.lineStartIndex; | |
| const lineEndIndex = symbol.lineStartIndex + symbol.lineLength; | |
| // 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); | |
| if (!firstAndLastGlyph) { | |
| return {notEnoughRoom: true}; | |
| } | |
| const firstPoint = project(firstAndLastGlyph.first.point, glCoordMatrix).point; | |
| const lastPoint = project(firstAndLastGlyph.last.point, glCoordMatrix).point; | |
| if (keepUpright && !flip) { | |
| const orientationChange = requiresOrientationChange(symbol.writingMode, firstPoint, lastPoint, aspectRatio); | |
| if (orientationChange) { | |
| return orientationChange; | |
| } | |
| } | |
| placedGlyphs = [firstAndLastGlyph.first]; | |
| for (let glyphIndex = symbol.glyphStartIndex + 1; glyphIndex < glyphEndIndex - 1; glyphIndex++) { | |
| // Since first and last glyph fit on the line, we're sure that the rest of the glyphs can be placed | |
| // $FlowFixMe | |
| placedGlyphs.push(placeGlyphAlongLine(fontScale * glyphOffsetArray.getoffsetX(glyphIndex), lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol.segment, | |
| lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache)); | |
| } | |
| placedGlyphs.push(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, posMatrix).point; | |
| const tileVertexIndex = (symbol.lineStartIndex + symbol.segment + 1); | |
| // $FlowFixMe | |
| const tileSegmentEnd = new Point(lineVertexArray.getx(tileVertexIndex), lineVertexArray.gety(tileVertexIndex)); | |
| const projectedVertex = project(tileSegmentEnd, 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.signedDistanceFromCamera > 0) ? | |
| projectedVertex.point : | |
| projectTruncatedLineSegment(tileAnchorPoint, tileSegmentEnd, a, 1, posMatrix); | |
| const orientationChange = requiresOrientationChange(symbol.writingMode, a, b, aspectRatio); | |
| if (orientationChange) { | |
| return orientationChange; | |
| } | |
| } | |
| // $FlowFixMe | |
| const singleGlyph = placeGlyphAlongLine(fontScale * glyphOffsetArray.getoffsetX(symbol.glyphStartIndex), lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol.segment, | |
| symbol.lineStartIndex, symbol.lineStartIndex + symbol.lineLength, lineVertexArray, labelPlaneMatrix, projectionCache); | |
| if (!singleGlyph) | |
| return {notEnoughRoom: true}; | |
| placedGlyphs = [singleGlyph]; | |
| } | |
| for (const glyph: any of placedGlyphs) { | |
| addDynamicAttributes(dynamicLayoutVertexArray, glyph.point, glyph.angle); | |
| } | |
| return {}; | |
| } | |
| function projectTruncatedLineSegment(previousTilePoint: Point, currentTilePoint: Point, previousProjectedPoint: Point, minimumLength: number, projectionMatrix: mat4) { | |
| // 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 projectedUnitVertex = project(previousTilePoint.add(previousTilePoint.sub(currentTilePoint)._unit()), projectionMatrix).point; | |
| const projectedUnitSegment = previousProjectedPoint.sub(projectedUnitVertex); | |
| return previousProjectedPoint.add(projectedUnitSegment._mult(minimumLength / projectedUnitSegment.mag())); | |
| } | |
| function placeGlyphAlongLine(offsetX: number, | |
| lineOffsetX: number, | |
| lineOffsetY: number, | |
| flip: boolean, | |
| anchorPoint: Point, | |
| tileAnchorPoint: Point, | |
| anchorSegment: number, | |
| lineStartIndex: number, | |
| lineEndIndex: number, | |
| lineVertexArray: SymbolLineVertexArray, | |
| labelPlaneMatrix: mat4, | |
| projectionCache: {[_: number]: Point}) { | |
| 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 = dir > 0 ? | |
| lineStartIndex + anchorSegment : | |
| lineStartIndex + anchorSegment + 1; | |
| let current = anchorPoint; | |
| let prev = anchorPoint; | |
| let distanceToPrev = 0; | |
| let currentSegmentDistance = 0; | |
| const absOffsetX = Math.abs(combinedOffsetX); | |
| const pathVertices = []; | |
| while (distanceToPrev + currentSegmentDistance <= absOffsetX) { | |
| currentIndex += dir; | |
| // offset does not fit on the projected line | |
| if (currentIndex < lineStartIndex || currentIndex >= lineEndIndex) | |
| return null; | |
| prev = current; | |
| pathVertices.push(current); | |
| current = projectionCache[currentIndex]; | |
| if (current === undefined) { | |
| const currentVertex = new Point(lineVertexArray.getx(currentIndex), lineVertexArray.gety(currentIndex)); | |
| const projection = project(currentVertex, labelPlaneMatrix); | |
| if (projection.signedDistanceFromCamera > 0) { | |
| current = projectionCache[currentIndex] = projection.point; | |
| } 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 | |
| const previousLineVertexIndex = currentIndex - dir; | |
| const previousTilePoint = distanceToPrev === 0 ? | |
| tileAnchorPoint : | |
| new Point(lineVertexArray.getx(previousLineVertexIndex), lineVertexArray.gety(previousLineVertexIndex)); | |
| // Don't cache because the new vertex might not be far enough out for future glyphs on the same segment | |
| current = projectTruncatedLineSegment(previousTilePoint, currentVertex, prev, absOffsetX - distanceToPrev + 1, labelPlaneMatrix); | |
| } | |
| } | |
| distanceToPrev += currentSegmentDistance; | |
| currentSegmentDistance = prev.dist(current); | |
| } | |
| // The point is on the current segment. Interpolate to find it. | |
| const segmentInterpolationT = (absOffsetX - distanceToPrev) / currentSegmentDistance; | |
| const prevToCurrent = current.sub(prev); | |
| const p = prevToCurrent.mult(segmentInterpolationT)._add(prev); | |
| // offset the point from the line to text-offset and icon-offset | |
| p._add(prevToCurrent._unit()._perp()._mult(lineOffsetY * dir)); | |
| const segmentAngle = angle + Math.atan2(current.y - prev.y, current.x - prev.x); | |
| pathVertices.push(p); | |
| return { | |
| point: p, | |
| angle: segmentAngle, | |
| path: pathVertices | |
| }; | |
| } | |
| const hiddenGlyphAttributes = new Float32Array([-Infinity, -Infinity, 0, -Infinity, -Infinity, 0, -Infinity, -Infinity, 0, -Infinity, -Infinity, 0]); | |
| // 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) { | |
| for (let i = 0; i < num; i++) { | |
| const offset = dynamicLayoutVertexArray.length; | |
| dynamicLayoutVertexArray.resize(offset + 4); | |
| // Since all hidden glyphs have the same attributes, we can build up the array faster with a single call to Float32Array.set | |
| // for each set of four vertices, instead of calling addDynamicAttributes for each vertex. | |
| dynamicLayoutVertexArray.float32.set(hiddenGlyphAttributes, offset * 3); | |
| } | |
| } | |
| // 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) { | |
| 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; | |
| } | |