1013 lines
34 KiB
JavaScript
1013 lines
34 KiB
JavaScript
import BoundingRectangle from "../Core/BoundingRectangle.js";
|
|
import Cartesian2 from "../Core/Cartesian2.js";
|
|
import Color from "../Core/Color.js";
|
|
import Frozen from "../Core/Frozen.js";
|
|
import defined from "../Core/defined.js";
|
|
import destroyObject from "../Core/destroyObject.js";
|
|
import DeveloperError from "../Core/DeveloperError.js";
|
|
import Matrix4 from "../Core/Matrix4.js";
|
|
import writeTextToCanvas from "../Core/writeTextToCanvas.js";
|
|
import bitmapSDF from "bitmap-sdf";
|
|
import BillboardCollection from "./BillboardCollection.js";
|
|
import BillboardTexture from "./BillboardTexture.js";
|
|
import BlendOption from "./BlendOption.js";
|
|
import { isHeightReferenceClamp } from "./HeightReference.js";
|
|
import HorizontalOrigin from "./HorizontalOrigin.js";
|
|
import Label from "./Label.js";
|
|
import LabelStyle from "./LabelStyle.js";
|
|
import SDFSettings from "./SDFSettings.js";
|
|
import TextureAtlas from "../Renderer/TextureAtlas.js";
|
|
import VerticalOrigin from "./VerticalOrigin.js";
|
|
import GraphemeSplitter from "grapheme-splitter";
|
|
|
|
/**
|
|
* A glyph represents a single character in label.
|
|
* @private
|
|
*/
|
|
function Glyph() {
|
|
/**
|
|
* Object containing dimensions of the character as rendered to a canvas.
|
|
* @see {writeTextToCanvas}
|
|
* @type {object}
|
|
* @private
|
|
*/
|
|
this.dimensions = undefined;
|
|
|
|
/**
|
|
* Reference to loaded image data for a single character, drawn in a particular style, shared and referenced across all labels.
|
|
* @type {BillboardTexture}
|
|
* @private
|
|
*/
|
|
this.billboardTexture = undefined;
|
|
|
|
/**
|
|
* The individual billboard used to render the glyph. This may be <code>undefined</code> if the associated character is whitespace.
|
|
* @type {Billboard|undefined}
|
|
* @private
|
|
*/
|
|
this.billboard = undefined;
|
|
}
|
|
|
|
// Traditionally, leading is %20 of the font size.
|
|
const defaultLineSpacingPercent = 1.2;
|
|
const whitePixelCanvasId = "ID_WHITE_PIXEL";
|
|
const whitePixelSize = new Cartesian2(4, 4);
|
|
const whitePixelBoundingRegion = new BoundingRectangle(1, 1, 1, 1);
|
|
|
|
/**
|
|
* Create the background image and start loading it into a texture
|
|
* @private
|
|
* @param {BillboardCollection} billboardCollection
|
|
* @param {LabelCollection} labelCollection
|
|
* @returns {Billboard}
|
|
*/
|
|
function getWhitePixelBillboard(billboardCollection, labelCollection) {
|
|
const billboardTexture = labelCollection._backgroundBillboardTexture;
|
|
if (!billboardTexture.hasImage) {
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = whitePixelSize.x;
|
|
canvas.height = whitePixelSize.y;
|
|
|
|
const context2D = canvas.getContext("2d");
|
|
context2D.fillStyle = "#fff";
|
|
context2D.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
billboardTexture.loadImage(whitePixelCanvasId, canvas);
|
|
billboardTexture.addImageSubRegion(
|
|
whitePixelCanvasId,
|
|
whitePixelBoundingRegion,
|
|
);
|
|
}
|
|
|
|
const billboard = billboardCollection.add({
|
|
collection: labelCollection,
|
|
});
|
|
|
|
billboard.setImageTexture(billboardTexture);
|
|
|
|
return billboard;
|
|
}
|
|
|
|
// reusable object for calling writeTextToCanvas
|
|
const writeTextToCanvasParameters = {};
|
|
function createGlyphCanvas(
|
|
character,
|
|
font,
|
|
fillColor,
|
|
outlineColor,
|
|
outlineWidth,
|
|
style,
|
|
) {
|
|
writeTextToCanvasParameters.font = font;
|
|
writeTextToCanvasParameters.fillColor = fillColor;
|
|
writeTextToCanvasParameters.strokeColor = outlineColor;
|
|
writeTextToCanvasParameters.strokeWidth = outlineWidth;
|
|
// Setting the padding to something bigger is necessary to get enough space for the outlining.
|
|
writeTextToCanvasParameters.padding = SDFSettings.PADDING;
|
|
|
|
writeTextToCanvasParameters.fill =
|
|
style === LabelStyle.FILL || style === LabelStyle.FILL_AND_OUTLINE;
|
|
writeTextToCanvasParameters.stroke =
|
|
style === LabelStyle.OUTLINE || style === LabelStyle.FILL_AND_OUTLINE;
|
|
writeTextToCanvasParameters.backgroundColor = Color.BLACK;
|
|
|
|
return writeTextToCanvas(character, writeTextToCanvasParameters);
|
|
}
|
|
|
|
function unbindGlyphBillboard(labelCollection, glyph) {
|
|
const billboard = glyph.billboard;
|
|
if (defined(billboard)) {
|
|
billboard.show = false;
|
|
if (defined(billboard._removeCallbackFunc)) {
|
|
billboard._removeCallbackFunc();
|
|
billboard._removeCallbackFunc = undefined;
|
|
}
|
|
labelCollection._spareBillboards.push(billboard);
|
|
glyph.billboard = undefined;
|
|
}
|
|
}
|
|
|
|
const splitter = new GraphemeSplitter();
|
|
const whitespaceRegex = /\s/;
|
|
|
|
function rebindAllGlyphs(labelCollection, label) {
|
|
const text = label._renderedText;
|
|
const graphemes = splitter.splitGraphemes(text);
|
|
const textLength = graphemes.length;
|
|
const glyphs = label._glyphs;
|
|
const glyphsLength = glyphs.length;
|
|
|
|
// Compute a font size scale relative to the sdf font generated size.
|
|
label._relativeSize = label._fontSize / SDFSettings.FONT_SIZE;
|
|
|
|
// if we have more glyphs than needed, unbind the extras.
|
|
if (textLength < glyphsLength) {
|
|
for (let glyphIndex = textLength; glyphIndex < glyphsLength; ++glyphIndex) {
|
|
unbindGlyphBillboard(labelCollection, glyphs[glyphIndex]);
|
|
}
|
|
}
|
|
|
|
// presize glyphs to match the new text length
|
|
glyphs.length = textLength;
|
|
|
|
const showBackground =
|
|
label.show && label._showBackground && text.split("\n").join("").length > 0;
|
|
let backgroundBillboard = label._backgroundBillboard;
|
|
const backgroundBillboardCollection =
|
|
labelCollection._backgroundBillboardCollection;
|
|
if (!showBackground) {
|
|
if (defined(backgroundBillboard)) {
|
|
backgroundBillboardCollection.remove(backgroundBillboard);
|
|
label._backgroundBillboard = backgroundBillboard = undefined;
|
|
}
|
|
} else {
|
|
if (!defined(backgroundBillboard)) {
|
|
backgroundBillboard = getWhitePixelBillboard(
|
|
backgroundBillboardCollection,
|
|
labelCollection,
|
|
);
|
|
label._backgroundBillboard = backgroundBillboard;
|
|
}
|
|
|
|
backgroundBillboard.color = label._backgroundColor;
|
|
backgroundBillboard.show = label._show;
|
|
backgroundBillboard.position = label._position;
|
|
backgroundBillboard.eyeOffset = label._eyeOffset;
|
|
backgroundBillboard.pixelOffset = label._pixelOffset;
|
|
backgroundBillboard.horizontalOrigin = HorizontalOrigin.LEFT;
|
|
backgroundBillboard.verticalOrigin = label._verticalOrigin;
|
|
backgroundBillboard.heightReference = label._heightReference;
|
|
backgroundBillboard.scale = label.totalScale;
|
|
backgroundBillboard.pickPrimitive = label;
|
|
backgroundBillboard.id = label._id;
|
|
backgroundBillboard.translucencyByDistance = label._translucencyByDistance;
|
|
backgroundBillboard.pixelOffsetScaleByDistance =
|
|
label._pixelOffsetScaleByDistance;
|
|
backgroundBillboard.scaleByDistance = label._scaleByDistance;
|
|
backgroundBillboard.distanceDisplayCondition =
|
|
label._distanceDisplayCondition;
|
|
backgroundBillboard.disableDepthTestDistance =
|
|
label._disableDepthTestDistance;
|
|
backgroundBillboard.clusterShow = label.clusterShow;
|
|
}
|
|
|
|
const glyphBillboardCollection = labelCollection._glyphBillboardCollection;
|
|
const glyphTextureCache = glyphBillboardCollection.billboardTextureCache;
|
|
const textDimensionsCache = labelCollection._textDimensionsCache;
|
|
|
|
// walk the text looking for new characters (creating new glyphs for each)
|
|
// or changed characters (rebinding existing glyphs)
|
|
for (let textIndex = 0; textIndex < textLength; ++textIndex) {
|
|
const character = graphemes[textIndex];
|
|
const verticalOrigin = label._verticalOrigin;
|
|
|
|
const id = JSON.stringify([
|
|
character,
|
|
label._fontFamily,
|
|
label._fontStyle,
|
|
label._fontWeight,
|
|
+verticalOrigin,
|
|
]);
|
|
|
|
let dimensions = textDimensionsCache[id];
|
|
let glyphBillboardTexture = glyphTextureCache.get(id);
|
|
if (!defined(glyphBillboardTexture) || !defined(dimensions)) {
|
|
glyphBillboardTexture = new BillboardTexture(glyphBillboardCollection);
|
|
glyphTextureCache.set(id, glyphBillboardTexture);
|
|
|
|
const glyphFont = `${label._fontStyle} ${label._fontWeight} ${SDFSettings.FONT_SIZE}px ${label._fontFamily}`;
|
|
|
|
const canvas = createGlyphCanvas(
|
|
character,
|
|
glyphFont,
|
|
Color.WHITE,
|
|
Color.WHITE,
|
|
0.0,
|
|
LabelStyle.FILL,
|
|
);
|
|
|
|
dimensions = canvas.dimensions;
|
|
textDimensionsCache[id] = dimensions;
|
|
|
|
if (
|
|
canvas.width > 0 &&
|
|
canvas.height > 0 &&
|
|
!whitespaceRegex.test(character)
|
|
) {
|
|
const sdfValues = bitmapSDF(canvas, {
|
|
cutoff: SDFSettings.CUTOFF,
|
|
radius: SDFSettings.RADIUS,
|
|
});
|
|
|
|
// Context is originally created in writeTextToCanvas()
|
|
const ctx = canvas.getContext("2d");
|
|
const canvasWidth = canvas.width;
|
|
const canvasHeight = canvas.height;
|
|
const imgData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
|
|
for (let i = 0; i < canvasWidth; i++) {
|
|
for (let j = 0; j < canvasHeight; j++) {
|
|
const baseIndex = j * canvasWidth + i;
|
|
const alpha = sdfValues[baseIndex] * 255;
|
|
const imageIndex = baseIndex * 4;
|
|
imgData.data[imageIndex + 0] = alpha;
|
|
imgData.data[imageIndex + 1] = alpha;
|
|
imgData.data[imageIndex + 2] = alpha;
|
|
imgData.data[imageIndex + 3] = alpha;
|
|
}
|
|
}
|
|
ctx.putImageData(imgData, 0, 0);
|
|
glyphBillboardTexture.loadImage(id, canvas);
|
|
}
|
|
}
|
|
|
|
let glyph = glyphs[textIndex];
|
|
if (!defined(glyph)) {
|
|
glyph = new Glyph();
|
|
glyph.dimensions = dimensions;
|
|
glyph.billboardTexture = glyphBillboardTexture;
|
|
glyphs[textIndex] = glyph;
|
|
}
|
|
|
|
if (glyph.billboardTexture.id !== id) {
|
|
// This glyph has been mapped to a new texture. If we had one before, release
|
|
// our reference to that texture and dimensions, but reuse the billboard.
|
|
glyph.billboardTexture = glyphBillboardTexture;
|
|
glyph.dimensions = dimensions;
|
|
}
|
|
|
|
if (!glyphBillboardTexture.hasImage) {
|
|
// No texture, and therefore no billboard, for this glyph.
|
|
// so, completely unbind glyph to free up the billboard for others
|
|
unbindGlyphBillboard(labelCollection, glyph);
|
|
continue;
|
|
}
|
|
|
|
// If we have a texture, configure the existing billboard, or obtain one
|
|
let billboard = glyph.billboard;
|
|
const spareBillboards = labelCollection._spareBillboards;
|
|
if (!defined(billboard)) {
|
|
if (spareBillboards.length > 0) {
|
|
billboard = spareBillboards.pop();
|
|
} else {
|
|
billboard = glyphBillboardCollection.add({
|
|
collection: labelCollection,
|
|
});
|
|
billboard._labelDimensions = new Cartesian2();
|
|
billboard._labelTranslate = new Cartesian2();
|
|
}
|
|
glyph.billboard = billboard;
|
|
}
|
|
|
|
billboard.setImageTexture(glyphBillboardTexture);
|
|
|
|
billboard.show = label._show;
|
|
billboard.position = label._position;
|
|
billboard.eyeOffset = label._eyeOffset;
|
|
billboard.pixelOffset = label._pixelOffset;
|
|
billboard.horizontalOrigin = HorizontalOrigin.LEFT;
|
|
billboard.verticalOrigin = label._verticalOrigin;
|
|
billboard.heightReference = label._heightReference;
|
|
billboard.scale = label.totalScale;
|
|
billboard.pickPrimitive = label;
|
|
billboard.id = label._id;
|
|
billboard.translucencyByDistance = label._translucencyByDistance;
|
|
billboard.pixelOffsetScaleByDistance = label._pixelOffsetScaleByDistance;
|
|
billboard.scaleByDistance = label._scaleByDistance;
|
|
billboard.distanceDisplayCondition = label._distanceDisplayCondition;
|
|
billboard.disableDepthTestDistance = label._disableDepthTestDistance;
|
|
billboard._batchIndex = label._batchIndex;
|
|
billboard.outlineColor = label.outlineColor;
|
|
if (label.style === LabelStyle.FILL_AND_OUTLINE) {
|
|
billboard.color = label._fillColor;
|
|
billboard.outlineWidth = label.outlineWidth;
|
|
} else if (label.style === LabelStyle.FILL) {
|
|
billboard.color = label._fillColor;
|
|
billboard.outlineWidth = 0.0;
|
|
} else if (label.style === LabelStyle.OUTLINE) {
|
|
billboard.color = Color.TRANSPARENT;
|
|
billboard.outlineWidth = label.outlineWidth;
|
|
}
|
|
}
|
|
|
|
// changing glyphs will cause the position of the
|
|
// glyphs to change, since different characters have different widths
|
|
label._repositionAllGlyphs = true;
|
|
}
|
|
|
|
function calculateWidthOffset(lineWidth, horizontalOrigin, backgroundPadding) {
|
|
if (horizontalOrigin === HorizontalOrigin.CENTER) {
|
|
return -lineWidth / 2;
|
|
} else if (horizontalOrigin === HorizontalOrigin.RIGHT) {
|
|
return -(lineWidth + backgroundPadding.x);
|
|
}
|
|
return backgroundPadding.x;
|
|
}
|
|
|
|
// reusable Cartesian2 instances
|
|
const glyphPixelOffset = new Cartesian2();
|
|
const scratchBackgroundPadding = new Cartesian2();
|
|
|
|
function repositionAllGlyphs(label) {
|
|
const glyphs = label._glyphs;
|
|
const text = label._renderedText;
|
|
let lastLineWidth = 0;
|
|
let maxLineWidth = 0;
|
|
const lineWidths = [];
|
|
let maxGlyphDescent = Number.NEGATIVE_INFINITY;
|
|
let maxGlyphY = 0;
|
|
let numberOfLines = 1;
|
|
const glyphLength = glyphs.length;
|
|
|
|
const backgroundBillboard = label._backgroundBillboard;
|
|
const backgroundPadding = Cartesian2.clone(
|
|
defined(backgroundBillboard) ? label._backgroundPadding : Cartesian2.ZERO,
|
|
scratchBackgroundPadding,
|
|
);
|
|
|
|
// We need to scale the background padding, which is specified in pixels by the inverse of the relative size so it is scaled properly.
|
|
backgroundPadding.x /= label._relativeSize;
|
|
backgroundPadding.y /= label._relativeSize;
|
|
|
|
for (let glyphIndex = 0; glyphIndex < glyphLength; ++glyphIndex) {
|
|
if (text.charAt(glyphIndex) === "\n") {
|
|
lineWidths.push(lastLineWidth);
|
|
++numberOfLines;
|
|
lastLineWidth = 0;
|
|
continue;
|
|
}
|
|
|
|
const glyph = glyphs[glyphIndex];
|
|
const dimensions = glyph.dimensions;
|
|
if (defined(dimensions)) {
|
|
maxGlyphY = Math.max(maxGlyphY, dimensions.height - dimensions.descent);
|
|
maxGlyphDescent = Math.max(maxGlyphDescent, dimensions.descent);
|
|
|
|
// Computing the line width must also account for the kerning that occurs between letters.
|
|
lastLineWidth += dimensions.width - dimensions.minx;
|
|
if (glyphIndex < glyphLength - 1) {
|
|
lastLineWidth += glyphs[glyphIndex + 1].dimensions.minx;
|
|
}
|
|
maxLineWidth = Math.max(maxLineWidth, lastLineWidth);
|
|
}
|
|
}
|
|
lineWidths.push(lastLineWidth);
|
|
const maxLineHeight = maxGlyphY + maxGlyphDescent;
|
|
|
|
const scale = label.totalScale;
|
|
const horizontalOrigin = label._horizontalOrigin;
|
|
const verticalOrigin = label._verticalOrigin;
|
|
let lineIndex = 0;
|
|
let lineWidth = lineWidths[lineIndex];
|
|
let widthOffset = calculateWidthOffset(
|
|
lineWidth,
|
|
horizontalOrigin,
|
|
backgroundPadding,
|
|
);
|
|
const lineSpacing =
|
|
(defined(label._lineHeight)
|
|
? label._lineHeight
|
|
: defaultLineSpacingPercent * label._fontSize) / label._relativeSize;
|
|
const otherLinesHeight = lineSpacing * (numberOfLines - 1);
|
|
let totalLineWidth = maxLineWidth;
|
|
let totalLineHeight = maxLineHeight + otherLinesHeight;
|
|
|
|
if (defined(backgroundBillboard)) {
|
|
totalLineWidth += backgroundPadding.x * 2;
|
|
totalLineHeight += backgroundPadding.y * 2;
|
|
backgroundBillboard._labelHorizontalOrigin = horizontalOrigin;
|
|
}
|
|
|
|
glyphPixelOffset.x = widthOffset * scale;
|
|
glyphPixelOffset.y = 0;
|
|
|
|
let firstCharOfLine = true;
|
|
|
|
let lineOffsetY = 0;
|
|
for (let glyphIndex = 0; glyphIndex < glyphLength; ++glyphIndex) {
|
|
if (text.charAt(glyphIndex) === "\n") {
|
|
++lineIndex;
|
|
lineOffsetY += lineSpacing;
|
|
lineWidth = lineWidths[lineIndex];
|
|
widthOffset = calculateWidthOffset(
|
|
lineWidth,
|
|
horizontalOrigin,
|
|
backgroundPadding,
|
|
);
|
|
glyphPixelOffset.x = widthOffset * scale;
|
|
firstCharOfLine = true;
|
|
continue;
|
|
}
|
|
|
|
const glyph = glyphs[glyphIndex];
|
|
const dimensions = glyph.dimensions;
|
|
if (defined(dimensions)) {
|
|
if (verticalOrigin === VerticalOrigin.TOP) {
|
|
glyphPixelOffset.y =
|
|
dimensions.height - maxGlyphY - backgroundPadding.y;
|
|
glyphPixelOffset.y += SDFSettings.PADDING;
|
|
} else if (verticalOrigin === VerticalOrigin.CENTER) {
|
|
glyphPixelOffset.y =
|
|
(otherLinesHeight + dimensions.height - maxGlyphY) / 2;
|
|
} else if (verticalOrigin === VerticalOrigin.BASELINE) {
|
|
glyphPixelOffset.y = otherLinesHeight;
|
|
glyphPixelOffset.y -= SDFSettings.PADDING;
|
|
} else {
|
|
// VerticalOrigin.BOTTOM
|
|
glyphPixelOffset.y =
|
|
otherLinesHeight + maxGlyphDescent + backgroundPadding.y;
|
|
glyphPixelOffset.y -= SDFSettings.PADDING;
|
|
}
|
|
glyphPixelOffset.y =
|
|
(glyphPixelOffset.y - dimensions.descent - lineOffsetY) * scale;
|
|
|
|
// Handle any offsets for the first character of the line since the bounds might not be right on the bottom left pixel.
|
|
if (firstCharOfLine) {
|
|
glyphPixelOffset.x -= SDFSettings.PADDING * scale;
|
|
firstCharOfLine = false;
|
|
}
|
|
|
|
if (defined(glyph.billboard)) {
|
|
glyph.billboard._setTranslate(glyphPixelOffset);
|
|
glyph.billboard._labelDimensions.x = totalLineWidth;
|
|
glyph.billboard._labelDimensions.y = totalLineHeight;
|
|
glyph.billboard._labelHorizontalOrigin = horizontalOrigin;
|
|
}
|
|
|
|
//Compute the next x offset taking into account the kerning performed
|
|
//on both the current letter as well as the next letter to be drawn
|
|
//as well as any applied scale.
|
|
if (glyphIndex < glyphLength - 1) {
|
|
const nextGlyph = glyphs[glyphIndex + 1];
|
|
glyphPixelOffset.x +=
|
|
(dimensions.width - dimensions.minx + nextGlyph.dimensions.minx) *
|
|
scale;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (defined(backgroundBillboard) && text.split("\n").join("").length > 0) {
|
|
if (horizontalOrigin === HorizontalOrigin.CENTER) {
|
|
widthOffset = -maxLineWidth / 2 - backgroundPadding.x;
|
|
} else if (horizontalOrigin === HorizontalOrigin.RIGHT) {
|
|
widthOffset = -(maxLineWidth + backgroundPadding.x * 2);
|
|
} else {
|
|
widthOffset = 0;
|
|
}
|
|
glyphPixelOffset.x = widthOffset * scale;
|
|
|
|
if (verticalOrigin === VerticalOrigin.TOP) {
|
|
glyphPixelOffset.y = maxLineHeight - maxGlyphY - maxGlyphDescent;
|
|
} else if (verticalOrigin === VerticalOrigin.CENTER) {
|
|
glyphPixelOffset.y = (maxLineHeight - maxGlyphY) / 2 - maxGlyphDescent;
|
|
} else if (verticalOrigin === VerticalOrigin.BASELINE) {
|
|
glyphPixelOffset.y = -backgroundPadding.y - maxGlyphDescent;
|
|
} else {
|
|
// VerticalOrigin.BOTTOM
|
|
glyphPixelOffset.y = 0;
|
|
}
|
|
glyphPixelOffset.y = glyphPixelOffset.y * scale;
|
|
|
|
backgroundBillboard.width = totalLineWidth;
|
|
backgroundBillboard.height = totalLineHeight;
|
|
backgroundBillboard._setTranslate(glyphPixelOffset);
|
|
backgroundBillboard._labelTranslate = Cartesian2.clone(
|
|
glyphPixelOffset,
|
|
backgroundBillboard._labelTranslate,
|
|
);
|
|
}
|
|
|
|
if (isHeightReferenceClamp(label.heightReference)) {
|
|
for (let glyphIndex = 0; glyphIndex < glyphLength; ++glyphIndex) {
|
|
const glyph = glyphs[glyphIndex];
|
|
const billboard = glyph.billboard;
|
|
if (defined(billboard)) {
|
|
billboard._labelTranslate = Cartesian2.clone(
|
|
glyphPixelOffset,
|
|
billboard._labelTranslate,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function destroyLabel(labelCollection, label) {
|
|
const glyphs = label._glyphs;
|
|
for (let i = 0, len = glyphs.length; i < len; ++i) {
|
|
unbindGlyphBillboard(labelCollection, glyphs[i]);
|
|
}
|
|
if (defined(label._backgroundBillboard)) {
|
|
labelCollection._backgroundBillboardCollection.remove(
|
|
label._backgroundBillboard,
|
|
);
|
|
label._backgroundBillboard = undefined;
|
|
}
|
|
label._labelCollection = undefined;
|
|
|
|
if (defined(label._removeCallbackFunc)) {
|
|
label._removeCallbackFunc();
|
|
}
|
|
|
|
destroyObject(label);
|
|
}
|
|
|
|
/**
|
|
* A renderable collection of labels. Labels are viewport-aligned text positioned in the 3D scene.
|
|
* Each label can have a different font, color, scale, etc.
|
|
* <br /><br />
|
|
* <div align='center'>
|
|
* <img src='Images/Label.png' width='400' height='300' /><br />
|
|
* Example labels
|
|
* </div>
|
|
* <br /><br />
|
|
* Labels are added and removed from the collection using {@link LabelCollection#add}
|
|
* and {@link LabelCollection#remove}.
|
|
*
|
|
* @alias LabelCollection
|
|
* @constructor
|
|
*
|
|
* @param {object} [options] Object with the following properties:
|
|
* @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] The 4x4 transformation matrix that transforms each label from model to world coordinates.
|
|
* @param {boolean} [options.debugShowBoundingVolume=false] For debugging only. Determines if this primitive's commands' bounding spheres are shown.
|
|
* @param {Scene} [options.scene] Must be passed in for labels that use the height reference property or will be depth tested against the globe.
|
|
* @param {BlendOption} [options.blendOption=BlendOption.OPAQUE_AND_TRANSLUCENT] The label blending option. The default
|
|
* is used for rendering both opaque and translucent labels. However, if either all of the labels are completely opaque or all are completely translucent,
|
|
* setting the technique to BlendOption.OPAQUE or BlendOption.TRANSLUCENT can improve performance by up to 2x.
|
|
* @param {boolean} [options.show=true] Determines if the labels in the collection will be shown.
|
|
*
|
|
* @performance For best performance, prefer a few collections, each with many labels, to
|
|
* many collections with only a few labels each. Avoid having collections where some
|
|
* labels change every frame and others do not; instead, create one or more collections
|
|
* for static labels, and one or more collections for dynamic labels.
|
|
*
|
|
* @see LabelCollection#add
|
|
* @see LabelCollection#remove
|
|
* @see Label
|
|
* @see BillboardCollection
|
|
*
|
|
* @demo {@link https://sandcastle.cesium.com/index.html?src=Labels.html|Cesium Sandcastle Labels Demo}
|
|
*
|
|
* @example
|
|
* // Create a label collection with two labels
|
|
* const labels = scene.primitives.add(new Cesium.LabelCollection());
|
|
* labels.add({
|
|
* position : new Cesium.Cartesian3(1.0, 2.0, 3.0),
|
|
* text : 'A label'
|
|
* });
|
|
* labels.add({
|
|
* position : new Cesium.Cartesian3(4.0, 5.0, 6.0),
|
|
* text : 'Another label'
|
|
* });
|
|
*/
|
|
function LabelCollection(options) {
|
|
options = options ?? Frozen.EMPTY_OBJECT;
|
|
|
|
this._scene = options.scene;
|
|
this._batchTable = options.batchTable;
|
|
|
|
const backgroundBillboardCollection = new BillboardCollection({
|
|
scene: this._scene,
|
|
textureAtlas: new TextureAtlas({
|
|
initialSize: whitePixelSize,
|
|
}),
|
|
});
|
|
this._backgroundBillboardCollection = backgroundBillboardCollection;
|
|
this._backgroundBillboardTexture = new BillboardTexture(
|
|
backgroundBillboardCollection,
|
|
);
|
|
|
|
this._glyphBillboardCollection = new BillboardCollection({
|
|
scene: this._scene,
|
|
batchTable: this._batchTable,
|
|
});
|
|
this._glyphBillboardCollection._sdf = true;
|
|
|
|
this._spareBillboards = [];
|
|
this._textDimensionsCache = {};
|
|
this._labels = [];
|
|
this._labelsToUpdate = [];
|
|
this._totalGlyphCount = 0;
|
|
|
|
this._highlightColor = Color.clone(Color.WHITE); // Only used by Vector3DTilePoints
|
|
|
|
/**
|
|
* Determines if labels in this collection will be shown.
|
|
*
|
|
* @type {boolean}
|
|
* @default true
|
|
*/
|
|
this.show = options.show ?? true;
|
|
|
|
/**
|
|
* The 4x4 transformation matrix that transforms each label in this collection from model to world coordinates.
|
|
* When this is the identity matrix, the labels are drawn in world coordinates, i.e., Earth's WGS84 coordinates.
|
|
* Local reference frames can be used by providing a different transformation matrix, like that returned
|
|
* by {@link Transforms.eastNorthUpToFixedFrame}.
|
|
*
|
|
* @type Matrix4
|
|
* @default {@link Matrix4.IDENTITY}
|
|
*
|
|
* @example
|
|
* const center = Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883);
|
|
* labels.modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(center);
|
|
* labels.add({
|
|
* position : new Cesium.Cartesian3(0.0, 0.0, 0.0),
|
|
* text : 'Center'
|
|
* });
|
|
* labels.add({
|
|
* position : new Cesium.Cartesian3(1000000.0, 0.0, 0.0),
|
|
* text : 'East'
|
|
* });
|
|
* labels.add({
|
|
* position : new Cesium.Cartesian3(0.0, 1000000.0, 0.0),
|
|
* text : 'North'
|
|
* });
|
|
* labels.add({
|
|
* position : new Cesium.Cartesian3(0.0, 0.0, 1000000.0),
|
|
* text : 'Up'
|
|
* });
|
|
*/
|
|
this.modelMatrix = Matrix4.clone(options.modelMatrix ?? Matrix4.IDENTITY);
|
|
|
|
/**
|
|
* This property is for debugging only; it is not for production use nor is it optimized.
|
|
* <p>
|
|
* Draws the bounding sphere for each draw command in the primitive.
|
|
* </p>
|
|
*
|
|
* @type {boolean}
|
|
*
|
|
* @default false
|
|
*/
|
|
this.debugShowBoundingVolume = options.debugShowBoundingVolume ?? false;
|
|
|
|
/**
|
|
* The label blending option. The default is used for rendering both opaque and translucent labels.
|
|
* However, if either all of the labels are completely opaque or all are completely translucent,
|
|
* setting the technique to BlendOption.OPAQUE or BlendOption.TRANSLUCENT can improve
|
|
* performance by up to 2x.
|
|
* @type {BlendOption}
|
|
* @default BlendOption.OPAQUE_AND_TRANSLUCENT
|
|
*/
|
|
this.blendOption = options.blendOption ?? BlendOption.OPAQUE_AND_TRANSLUCENT;
|
|
}
|
|
|
|
Object.defineProperties(LabelCollection.prototype, {
|
|
/**
|
|
* Returns the number of labels in this collection. This is commonly used with
|
|
* {@link LabelCollection#get} to iterate over all the labels
|
|
* in the collection.
|
|
* @memberof LabelCollection.prototype
|
|
* @type {number}
|
|
* @readonly
|
|
*/
|
|
length: {
|
|
get: function () {
|
|
return this._labels.length;
|
|
},
|
|
},
|
|
|
|
/**
|
|
* Returns the size in bytes of the WebGL texture resources.
|
|
* @private
|
|
* @memberof LabelCollection.prototype
|
|
* @type {number}
|
|
* @readonly
|
|
*/
|
|
sizeInBytes: {
|
|
get: function () {
|
|
return (
|
|
this._glyphBillboardCollection.sizeInBytes +
|
|
this._backgroundBillboardCollection.sizeInBytes
|
|
);
|
|
},
|
|
},
|
|
|
|
/**
|
|
* True when all labels currently in the collection are ready for rendering.
|
|
* @private
|
|
* @memberof LabelCollection.prototype
|
|
* @type {boolean}
|
|
* @readonly
|
|
*/
|
|
ready: {
|
|
get: function () {
|
|
const backgroundBillboard = this._backgroundBillboardCollection.get(0);
|
|
if (defined(backgroundBillboard) && !backgroundBillboard.ready) {
|
|
return false;
|
|
}
|
|
|
|
return this._glyphBillboardCollection.ready;
|
|
},
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Creates and adds a label with the specified initial properties to the collection.
|
|
* The added label is returned so it can be modified or removed from the collection later.
|
|
*
|
|
* @param {Label.ConstructorOptions} [options] A template describing the label's properties as shown in Example 1.
|
|
* @returns {Label} The label that was added to the collection.
|
|
*
|
|
* @performance Calling <code>add</code> is expected constant time. However, the collection's vertex buffer
|
|
* is rewritten; this operations is <code>O(n)</code> and also incurs
|
|
* CPU to GPU overhead. For best performance, add as many billboards as possible before
|
|
* calling <code>update</code>.
|
|
*
|
|
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
|
|
*
|
|
* @example
|
|
* // Example 1: Add a label, specifying all the default values.
|
|
* const l = labels.add({
|
|
* show : true,
|
|
* position : Cesium.Cartesian3.ZERO,
|
|
* text : '',
|
|
* font : '30px sans-serif',
|
|
* fillColor : Cesium.Color.WHITE,
|
|
* outlineColor : Cesium.Color.BLACK,
|
|
* outlineWidth : 1.0,
|
|
* showBackground : false,
|
|
* backgroundColor : new Cesium.Color(0.165, 0.165, 0.165, 0.8),
|
|
* backgroundPadding : new Cesium.Cartesian2(7, 5),
|
|
* style : Cesium.LabelStyle.FILL,
|
|
* pixelOffset : Cesium.Cartesian2.ZERO,
|
|
* eyeOffset : Cesium.Cartesian3.ZERO,
|
|
* horizontalOrigin : Cesium.HorizontalOrigin.LEFT,
|
|
* verticalOrigin : Cesium.VerticalOrigin.BASELINE,
|
|
* scale : 1.0,
|
|
* translucencyByDistance : undefined,
|
|
* pixelOffsetScaleByDistance : undefined,
|
|
* heightReference : HeightReference.NONE,
|
|
* distanceDisplayCondition : undefined
|
|
* });
|
|
*
|
|
* @example
|
|
* // Example 2: Specify only the label's cartographic position,
|
|
* // text, and font.
|
|
* const l = labels.add({
|
|
* position : Cesium.Cartesian3.fromRadians(longitude, latitude, height),
|
|
* text : 'Hello World',
|
|
* font : '24px Helvetica',
|
|
* });
|
|
*
|
|
*
|
|
* @see LabelCollection#remove
|
|
* @see LabelCollection#removeAll
|
|
*/
|
|
LabelCollection.prototype.add = function (options) {
|
|
const label = new Label(options, this);
|
|
|
|
this._labels.push(label);
|
|
this._labelsToUpdate.push(label);
|
|
|
|
return label;
|
|
};
|
|
|
|
/**
|
|
* Removes a label from the collection. Once removed, a label is no longer usable.
|
|
*
|
|
* @param {Label} label The label to remove.
|
|
* @returns {boolean} <code>true</code> if the label was removed; <code>false</code> if the label was not found in the collection.
|
|
*
|
|
* @performance Calling <code>remove</code> is expected constant time. However, the collection's vertex buffer
|
|
* is rewritten - an <code>O(n)</code> operation that also incurs CPU to GPU overhead. For
|
|
* best performance, remove as many labels as possible before calling <code>update</code>.
|
|
* If you intend to temporarily hide a label, it is usually more efficient to call
|
|
* {@link Label#show} instead of removing and re-adding the label.
|
|
*
|
|
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
|
|
*
|
|
*
|
|
* @example
|
|
* const l = labels.add(...);
|
|
* labels.remove(l); // Returns true
|
|
*
|
|
* @see LabelCollection#add
|
|
* @see LabelCollection#removeAll
|
|
* @see Label#show
|
|
*/
|
|
LabelCollection.prototype.remove = function (label) {
|
|
if (defined(label) && label._labelCollection === this) {
|
|
const index = this._labels.indexOf(label);
|
|
if (index !== -1) {
|
|
this._labels.splice(index, 1);
|
|
destroyLabel(this, label);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Removes all labels from the collection.
|
|
*
|
|
* @performance <code>O(n)</code>. It is more efficient to remove all the labels
|
|
* from a collection and then add new ones than to create a new collection entirely.
|
|
*
|
|
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
|
|
*
|
|
*
|
|
* @example
|
|
* labels.add(...);
|
|
* labels.add(...);
|
|
* labels.removeAll();
|
|
*
|
|
* @see LabelCollection#add
|
|
* @see LabelCollection#remove
|
|
*/
|
|
LabelCollection.prototype.removeAll = function () {
|
|
const labels = this._labels;
|
|
|
|
for (let i = 0, len = labels.length; i < len; ++i) {
|
|
destroyLabel(this, labels[i]);
|
|
}
|
|
|
|
labels.length = 0;
|
|
};
|
|
|
|
/**
|
|
* Check whether this collection contains a given label.
|
|
*
|
|
* @param {Label} label The label to check for.
|
|
* @returns {boolean} true if this collection contains the label, false otherwise.
|
|
*
|
|
* @see LabelCollection#get
|
|
*
|
|
*/
|
|
LabelCollection.prototype.contains = function (label) {
|
|
return defined(label) && label._labelCollection === this;
|
|
};
|
|
|
|
/**
|
|
* Returns the label in the collection at the specified index. Indices are zero-based
|
|
* and increase as labels are added. Removing a label shifts all labels after
|
|
* it to the left, changing their indices. This function is commonly used with
|
|
* {@link LabelCollection#length} to iterate over all the labels
|
|
* in the collection.
|
|
*
|
|
* @param {number} index The zero-based index of the billboard.
|
|
*
|
|
* @returns {Label} The label at the specified index.
|
|
*
|
|
* @performance Expected constant time. If labels were removed from the collection and
|
|
* {@link Scene#render} was not called, an implicit <code>O(n)</code>
|
|
* operation is performed.
|
|
*
|
|
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
|
|
*
|
|
*
|
|
* @example
|
|
* // Toggle the show property of every label in the collection
|
|
* const len = labels.length;
|
|
* for (let i = 0; i < len; ++i) {
|
|
* const l = billboards.get(i);
|
|
* l.show = !l.show;
|
|
* }
|
|
*
|
|
* @see LabelCollection#length
|
|
*/
|
|
LabelCollection.prototype.get = function (index) {
|
|
//>>includeStart('debug', pragmas.debug);
|
|
if (!defined(index)) {
|
|
throw new DeveloperError("index is required.");
|
|
}
|
|
//>>includeEnd('debug');
|
|
|
|
return this._labels[index];
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
LabelCollection.prototype.update = function (frameState) {
|
|
if (!this.show) {
|
|
return;
|
|
}
|
|
|
|
const glyphBillboardCollection = this._glyphBillboardCollection;
|
|
const backgroundBillboardCollection = this._backgroundBillboardCollection;
|
|
|
|
glyphBillboardCollection.modelMatrix = this.modelMatrix;
|
|
glyphBillboardCollection.debugShowBoundingVolume =
|
|
this.debugShowBoundingVolume;
|
|
backgroundBillboardCollection.modelMatrix = this.modelMatrix;
|
|
backgroundBillboardCollection.debugShowBoundingVolume =
|
|
this.debugShowBoundingVolume;
|
|
|
|
const len = this._labelsToUpdate.length;
|
|
for (let i = 0; i < len; ++i) {
|
|
const label = this._labelsToUpdate[i];
|
|
if (label.isDestroyed()) {
|
|
continue;
|
|
}
|
|
|
|
const preUpdateGlyphCount = label._glyphs.length;
|
|
|
|
if (label._rebindAllGlyphs) {
|
|
rebindAllGlyphs(this, label);
|
|
label._rebindAllGlyphs = false;
|
|
}
|
|
|
|
if (label._repositionAllGlyphs) {
|
|
repositionAllGlyphs(label);
|
|
label._repositionAllGlyphs = false;
|
|
}
|
|
|
|
const glyphCountDifference = label._glyphs.length - preUpdateGlyphCount;
|
|
this._totalGlyphCount += glyphCountDifference;
|
|
}
|
|
|
|
const blendOption =
|
|
backgroundBillboardCollection.length > 0
|
|
? BlendOption.TRANSLUCENT
|
|
: this.blendOption;
|
|
glyphBillboardCollection.blendOption = blendOption;
|
|
backgroundBillboardCollection.blendOption = blendOption;
|
|
|
|
glyphBillboardCollection._highlightColor = this._highlightColor;
|
|
backgroundBillboardCollection._highlightColor = this._highlightColor;
|
|
|
|
this._labelsToUpdate.length = 0;
|
|
backgroundBillboardCollection.update(frameState);
|
|
glyphBillboardCollection.update(frameState);
|
|
};
|
|
|
|
/**
|
|
* Returns true if this object was destroyed; otherwise, false.
|
|
* <br /><br />
|
|
* If this object was destroyed, it should not be used; calling any function other than
|
|
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
|
|
*
|
|
* @returns {boolean} True if this object was destroyed; otherwise, false.
|
|
*
|
|
* @see LabelCollection#destroy
|
|
*/
|
|
LabelCollection.prototype.isDestroyed = function () {
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Destroys the WebGL resources held by this object. Destroying an object allows for deterministic
|
|
* release of WebGL resources, instead of relying on the garbage collector to destroy this object.
|
|
* <br /><br />
|
|
* Once an object is destroyed, it should not be used; calling any function other than
|
|
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
|
|
* assign the return value (<code>undefined</code>) to the object as done in the example.
|
|
*
|
|
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
|
|
*
|
|
*
|
|
* @example
|
|
* labels = labels && labels.destroy();
|
|
*
|
|
* @see LabelCollection#isDestroyed
|
|
*/
|
|
LabelCollection.prototype.destroy = function () {
|
|
this.removeAll();
|
|
this._glyphBillboardCollection = this._glyphBillboardCollection.destroy();
|
|
this._backgroundBillboardCollection =
|
|
this._backgroundBillboardCollection.destroy();
|
|
|
|
return destroyObject(this);
|
|
};
|
|
export default LabelCollection;
|