terriajs-cesium
Version:
CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
1,091 lines (987 loc) • 39.2 kB
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";
import { Check } from "@cesium/engine";
/**
* 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;
billboard._clampedPosition = undefined;
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;
let backgroundBillboard = label._backgroundBillboard;
const backgroundBillboardCollection =
labelCollection._backgroundBillboardCollection;
// Create backgroundBillboard if needed
if (label._showBackground && !defined(backgroundBillboard)) {
backgroundBillboard = getWhitePixelBillboard(
backgroundBillboardCollection,
labelCollection,
);
label._backgroundBillboard = backgroundBillboard;
}
updateBackgroundBillboard(
backgroundBillboardCollection,
label,
backgroundBillboard,
);
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();
billboard._positionFromParent = true;
}
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 updateBackgroundBillboard(
backgroundBillboardCollection,
label,
backgroundBillboard,
) {
if (!defined(backgroundBillboard)) {
return;
}
const showBackground =
label.show &&
label._showBackground &&
label._renderedText.split("\n").join("").length > 0;
// Label is shown and background is hidden - remove the background billboard
if (label.show && !showBackground) {
backgroundBillboardCollection.remove(backgroundBillboard);
label._backgroundBillboard = backgroundBillboard = undefined;
return;
}
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;
}
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.
* @param {number} [options.coarseDepthTestDistance] The distance from the camera, beyond which, labels are depth-tested against an approximation of the globe ellipsoid rather than against the full globe depth buffer. If unspecified, the default value is determined relative to the value of {@link Ellipsoid.default}.
* @param {number} [options.threePointDepthTestDistance] The distance from the camera, within which, lables with a {@link Label#heightReference} value of {@link HeightReference.CLAMP_TO_GROUND} or {@link HeightReference.CLAMP_TO_TERRAIN} are depth tested against three key points. This ensures that if any key point of the label is visible, the whole label will be visible. If unspecified, the default value is determined relative to the value of {@link Ellipsoid.default}.
* @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?id=labels|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,
}),
coarseDepthTestDistance: options.coarseDepthTestDistance,
threePointDepthTestDistance: options.threePointDepthTestDistance,
});
this._backgroundBillboardCollection = backgroundBillboardCollection;
this._backgroundBillboardTexture = new BillboardTexture(
backgroundBillboardCollection,
);
this._glyphBillboardCollection = new BillboardCollection({
scene: this._scene,
batchTable: this._batchTable,
coarseDepthTestDistance: options.coarseDepthTestDistance,
threePointDepthTestDistance: options.threePointDepthTestDistance,
});
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;
},
},
/**
* The distance from the camera, beyond which, labels are depth-tested against an approximation of
* the globe ellipsoid rather than against the full globe depth buffer. When set to <code>0</code>, the
* approximate depth test is always applied. When set to <code>Number.POSITIVE_INFINITY</code>, the
* approximate depth test is never applied.
* <br/><br/>
* This setting only applies when a label's {@link Label#disableDepthTestDistance} value would
* otherwise allow depth testing—i.e., distance from the camera to the label is less than the
* label's {@link Label#disableDepthTestDistance} value.
* @memberof LabelCollection.prototype
* @type {number}
*/
coarseDepthTestDistance: {
get: function () {
return this._backgroundBillboardCollection.coarseDepthTestDistance;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number("coarseDepthTestDistance", value);
//>>includeEnd('debug');
this._backgroundBillboardCollection.coarseDepthTestDistance = value;
this._glyphBillboardCollection.coarseDepthTestDistance = value;
},
},
/**
* The distance from the camera, within which, labels with a {@link Label#heightReference} value
* of {@link HeightReference.CLAMP_TO_GROUND} or {@link HeightReference.CLAMP_TO_TERRAIN} are depth tested
* against three key points. This ensures that if any key point of the label is visible, the whole
* label will be visible. When set to <code>0</code>, this feature is disabled and portions of a
* label behind terrain be clipped.
* <br/><br/>
* This setting only applies when a labels's {@link Label#disableDepthTestDistance} value would
* otherwise allow depth testing—i.e., distance from the camera to the label is less than the
* labels's {@link Label#disableDepthTestDistance} value.
* @see {@link https://cesium.com/blog/2018/07/30/billboards-on-terrain-improvements/|Billboards and Labels on Terrain Improvements}
* @memberof LabelCollection.prototype
* @type {number}
*/
threePointDepthTestDistance: {
get: function () {
return this._backgroundBillboardCollection.threePointDepthTestDistance;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number("threePointDepthTestDistance", value);
//>>includeEnd('debug');
this._backgroundBillboardCollection.threePointDepthTestDistance = value;
this._glyphBillboardCollection.threePointDepthTestDistance = value;
},
},
});
/**
* 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;