Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 | 1255x 1255x 1255x 1x 1x 1x 1x 60x 60x 36x 36x 36x 36x 36x 36x 36x 36x 60x 60x 60x 1x 521x 521x 521x 521x 521x 521x 521x 521x 521x 1431x 1431x 1088x 1088x 1088x 1088x 1x 1x 286x 286x 286x 286x 286x 286x 286x 27x 31x 286x 286x 286x 286x 60x 60x 286x 286x 286x 286x 286x 1310x 1310x 1310x 1310x 1310x 1310x 521x 521x 521x 521x 521x 521x 521x 444x 444x 444x 444x 444x 444x 20406x 1031131x 1031131x 1031131x 1031131x 1031131x 1031131x 1031131x 444x 444x 1310x 1310x 1255x 1255x 1255x 1255x 1310x 192x 192x 1310x 176x 176x 1134x 1134x 1134x 1088x 9x 1079x 1079x 1079x 1079x 1088x 1134x 1134x 1134x 1134x 1134x 1134x 1134x 1134x 1134x 1134x 1134x 1134x 1134x 1134x 1134x 1134x 1134x 1134x 1134x 20x 20x 1114x 1104x 1104x 10x 10x 10x 286x 286x 223x 63x 63x 1x 1x 1x 62x 62x 62x 62x 62x 62x 62x 62x 62x 62x 62x 62x 62x 62x 62x 62x 62x 323x 43x 280x 18x 262x 1x 1x 302x 302x 302x 302x 302x 302x 302x 302x 302x 302x 302x 302x 302x 302x 1354x 21x 21x 21x 21x 1333x 1333x 1333x 1333x 1333x 1333x 1333x 1067x 1333x 302x 302x 302x 302x 302x 302x 302x 302x 302x 302x 302x 302x 302x 67x 67x 67x 302x 302x 302x 302x 302x 1354x 21x 21x 21x 21x 21x 21x 21x 1333x 1333x 1333x 1333x 72x 72x 1261x 68x 1193x 1167x 1167x 26x 26x 1333x 1333x 287x 287x 1333x 1178x 1178x 1178x 1178x 1333x 1067x 1067x 302x 67x 3x 64x 6x 58x 67x 67x 18x 49x 3x 46x 40x 6x 67x 67x 67x 67x 67x 302x 12x 16x 16x 16x 9x 287x 287x 1224x 287x 59x 59x 287x 287x 9x 287x 291x 291x 291x 291x 291x 291x 291x 291x 291x 291x 291x 291x 291x 291x 291x 291x 291x 291x 1x 87x 47x 227x 227x 14x 213x 1x 328x 323x 323x 323x 1x 15x 13x 13x 13x 13x 13x 2x 1x 268x 268x 274x 268x 1x 18x 1x 122x 1x 121x 1x 931x 1x 930x 930x 930x 930x 930x 930x 930x 930x 303x 303x 1x 302x 302x 286x 286x 302x 302x 302x 302x 302x 930x 930x 930x 930x 930x 930x 930x 930x 1x 156x 1x 243x 243x 243x 243x | 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;
Iif (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 Eif (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;
Eif (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;
Eif (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);
Eif (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;
}
Eif (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;
|