LabelCollection.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995
  1. import BoundingRectangle from "../Core/BoundingRectangle.js";
  2. import Cartesian2 from "../Core/Cartesian2.js";
  3. import Color from "../Core/Color.js";
  4. import defaultValue from "../Core/defaultValue.js";
  5. import defined from "../Core/defined.js";
  6. import destroyObject from "../Core/destroyObject.js";
  7. import DeveloperError from "../Core/DeveloperError.js";
  8. import Matrix4 from "../Core/Matrix4.js";
  9. import writeTextToCanvas from "../Core/writeTextToCanvas.js";
  10. import bitmapSDF from "bitmap-sdf";
  11. import BillboardCollection from "./BillboardCollection.js";
  12. import BlendOption from "./BlendOption.js";
  13. import HeightReference from "./HeightReference.js";
  14. import HorizontalOrigin from "./HorizontalOrigin.js";
  15. import Label from "./Label.js";
  16. import LabelStyle from "./LabelStyle.js";
  17. import SDFSettings from "./SDFSettings.js";
  18. import TextureAtlas from "./TextureAtlas.js";
  19. import VerticalOrigin from "./VerticalOrigin.js";
  20. import GraphemeSplitter from "grapheme-splitter";
  21. // A glyph represents a single character in a particular label. It may or may
  22. // not have a billboard, depending on whether the texture info has an index into
  23. // the the label collection's texture atlas. Invisible characters have no texture, and
  24. // no billboard. However, it always has a valid dimensions object.
  25. function Glyph() {
  26. this.textureInfo = undefined;
  27. this.dimensions = undefined;
  28. this.billboard = undefined;
  29. }
  30. // GlyphTextureInfo represents a single character, drawn in a particular style,
  31. // shared and reference counted across all labels. It may or may not have an
  32. // index into the label collection's texture atlas, depending on whether the character
  33. // has both width and height, but it always has a valid dimensions object.
  34. function GlyphTextureInfo(labelCollection, index, dimensions) {
  35. this.labelCollection = labelCollection;
  36. this.index = index;
  37. this.dimensions = dimensions;
  38. }
  39. // Traditionally, leading is %20 of the font size.
  40. const defaultLineSpacingPercent = 1.2;
  41. const whitePixelCanvasId = "ID_WHITE_PIXEL";
  42. const whitePixelSize = new Cartesian2(4, 4);
  43. const whitePixelBoundingRegion = new BoundingRectangle(1, 1, 1, 1);
  44. function addWhitePixelCanvas(textureAtlas) {
  45. const canvas = document.createElement("canvas");
  46. canvas.width = whitePixelSize.x;
  47. canvas.height = whitePixelSize.y;
  48. const context2D = canvas.getContext("2d");
  49. context2D.fillStyle = "#fff";
  50. context2D.fillRect(0, 0, canvas.width, canvas.height);
  51. // Canvas operations take a frame to draw. Use the asynchronous add function which resolves a promise and allows the draw to complete,
  52. // but there's no need to wait on the promise before operation can continue
  53. textureAtlas.addImage(whitePixelCanvasId, canvas);
  54. }
  55. // reusable object for calling writeTextToCanvas
  56. const writeTextToCanvasParameters = {};
  57. function createGlyphCanvas(
  58. character,
  59. font,
  60. fillColor,
  61. outlineColor,
  62. outlineWidth,
  63. style,
  64. verticalOrigin
  65. ) {
  66. writeTextToCanvasParameters.font = font;
  67. writeTextToCanvasParameters.fillColor = fillColor;
  68. writeTextToCanvasParameters.strokeColor = outlineColor;
  69. writeTextToCanvasParameters.strokeWidth = outlineWidth;
  70. // Setting the padding to something bigger is necessary to get enough space for the outlining.
  71. writeTextToCanvasParameters.padding = SDFSettings.PADDING;
  72. if (verticalOrigin === VerticalOrigin.CENTER) {
  73. writeTextToCanvasParameters.textBaseline = "middle";
  74. } else if (verticalOrigin === VerticalOrigin.TOP) {
  75. writeTextToCanvasParameters.textBaseline = "top";
  76. } else {
  77. // VerticalOrigin.BOTTOM and VerticalOrigin.BASELINE
  78. writeTextToCanvasParameters.textBaseline = "bottom";
  79. }
  80. writeTextToCanvasParameters.fill =
  81. style === LabelStyle.FILL || style === LabelStyle.FILL_AND_OUTLINE;
  82. writeTextToCanvasParameters.stroke =
  83. style === LabelStyle.OUTLINE || style === LabelStyle.FILL_AND_OUTLINE;
  84. writeTextToCanvasParameters.backgroundColor = Color.BLACK;
  85. return writeTextToCanvas(character, writeTextToCanvasParameters);
  86. }
  87. function unbindGlyph(labelCollection, glyph) {
  88. glyph.textureInfo = undefined;
  89. glyph.dimensions = undefined;
  90. const billboard = glyph.billboard;
  91. if (defined(billboard)) {
  92. billboard.show = false;
  93. billboard.image = undefined;
  94. if (defined(billboard._removeCallbackFunc)) {
  95. billboard._removeCallbackFunc();
  96. billboard._removeCallbackFunc = undefined;
  97. }
  98. labelCollection._spareBillboards.push(billboard);
  99. glyph.billboard = undefined;
  100. }
  101. }
  102. function addGlyphToTextureAtlas(textureAtlas, id, canvas, glyphTextureInfo) {
  103. glyphTextureInfo.index = textureAtlas.addImageSync(id, canvas);
  104. }
  105. const splitter = new GraphemeSplitter();
  106. function rebindAllGlyphs(labelCollection, label) {
  107. const text = label._renderedText;
  108. const graphemes = splitter.splitGraphemes(text);
  109. const textLength = graphemes.length;
  110. const glyphs = label._glyphs;
  111. const glyphsLength = glyphs.length;
  112. let glyph;
  113. let glyphIndex;
  114. let textIndex;
  115. // Compute a font size scale relative to the sdf font generated size.
  116. label._relativeSize = label._fontSize / SDFSettings.FONT_SIZE;
  117. // if we have more glyphs than needed, unbind the extras.
  118. if (textLength < glyphsLength) {
  119. for (glyphIndex = textLength; glyphIndex < glyphsLength; ++glyphIndex) {
  120. unbindGlyph(labelCollection, glyphs[glyphIndex]);
  121. }
  122. }
  123. // presize glyphs to match the new text length
  124. glyphs.length = textLength;
  125. const showBackground =
  126. label._showBackground && text.split("\n").join("").length > 0;
  127. let backgroundBillboard = label._backgroundBillboard;
  128. const backgroundBillboardCollection =
  129. labelCollection._backgroundBillboardCollection;
  130. if (!showBackground) {
  131. if (defined(backgroundBillboard)) {
  132. backgroundBillboardCollection.remove(backgroundBillboard);
  133. label._backgroundBillboard = backgroundBillboard = undefined;
  134. }
  135. } else {
  136. if (!defined(backgroundBillboard)) {
  137. backgroundBillboard = backgroundBillboardCollection.add({
  138. collection: labelCollection,
  139. image: whitePixelCanvasId,
  140. imageSubRegion: whitePixelBoundingRegion,
  141. });
  142. label._backgroundBillboard = backgroundBillboard;
  143. }
  144. backgroundBillboard.color = label._backgroundColor;
  145. backgroundBillboard.show = label._show;
  146. backgroundBillboard.position = label._position;
  147. backgroundBillboard.eyeOffset = label._eyeOffset;
  148. backgroundBillboard.pixelOffset = label._pixelOffset;
  149. backgroundBillboard.horizontalOrigin = HorizontalOrigin.LEFT;
  150. backgroundBillboard.verticalOrigin = label._verticalOrigin;
  151. backgroundBillboard.heightReference = label._heightReference;
  152. backgroundBillboard.scale = label.totalScale;
  153. backgroundBillboard.pickPrimitive = label;
  154. backgroundBillboard.id = label._id;
  155. backgroundBillboard.translucencyByDistance = label._translucencyByDistance;
  156. backgroundBillboard.pixelOffsetScaleByDistance =
  157. label._pixelOffsetScaleByDistance;
  158. backgroundBillboard.scaleByDistance = label._scaleByDistance;
  159. backgroundBillboard.distanceDisplayCondition =
  160. label._distanceDisplayCondition;
  161. backgroundBillboard.disableDepthTestDistance =
  162. label._disableDepthTestDistance;
  163. }
  164. const glyphTextureCache = labelCollection._glyphTextureCache;
  165. // walk the text looking for new characters (creating new glyphs for each)
  166. // or changed characters (rebinding existing glyphs)
  167. for (textIndex = 0; textIndex < textLength; ++textIndex) {
  168. const character = graphemes[textIndex];
  169. const verticalOrigin = label._verticalOrigin;
  170. const id = JSON.stringify([
  171. character,
  172. label._fontFamily,
  173. label._fontStyle,
  174. label._fontWeight,
  175. +verticalOrigin,
  176. ]);
  177. let glyphTextureInfo = glyphTextureCache[id];
  178. if (!defined(glyphTextureInfo)) {
  179. const glyphFont = `${label._fontStyle} ${label._fontWeight} ${SDFSettings.FONT_SIZE}px ${label._fontFamily}`;
  180. const canvas = createGlyphCanvas(
  181. character,
  182. glyphFont,
  183. Color.WHITE,
  184. Color.WHITE,
  185. 0.0,
  186. LabelStyle.FILL,
  187. verticalOrigin
  188. );
  189. glyphTextureInfo = new GlyphTextureInfo(
  190. labelCollection,
  191. -1,
  192. canvas.dimensions
  193. );
  194. glyphTextureCache[id] = glyphTextureInfo;
  195. if (canvas.width > 0 && canvas.height > 0) {
  196. const sdfValues = bitmapSDF(canvas, {
  197. cutoff: SDFSettings.CUTOFF,
  198. radius: SDFSettings.RADIUS,
  199. });
  200. // Context is originally created in writeTextToCanvas()
  201. const ctx = canvas.getContext("2d");
  202. const canvasWidth = canvas.width;
  203. const canvasHeight = canvas.height;
  204. const imgData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
  205. for (let i = 0; i < canvasWidth; i++) {
  206. for (let j = 0; j < canvasHeight; j++) {
  207. const baseIndex = j * canvasWidth + i;
  208. const alpha = sdfValues[baseIndex] * 255;
  209. const imageIndex = baseIndex * 4;
  210. imgData.data[imageIndex + 0] = alpha;
  211. imgData.data[imageIndex + 1] = alpha;
  212. imgData.data[imageIndex + 2] = alpha;
  213. imgData.data[imageIndex + 3] = alpha;
  214. }
  215. }
  216. ctx.putImageData(imgData, 0, 0);
  217. if (character !== " ") {
  218. addGlyphToTextureAtlas(
  219. labelCollection._textureAtlas,
  220. id,
  221. canvas,
  222. glyphTextureInfo
  223. );
  224. }
  225. }
  226. }
  227. glyph = glyphs[textIndex];
  228. if (defined(glyph)) {
  229. // clean up leftover information from the previous glyph
  230. if (glyphTextureInfo.index === -1) {
  231. // no texture, and therefore no billboard, for this glyph.
  232. // so, completely unbind glyph.
  233. unbindGlyph(labelCollection, glyph);
  234. } else if (defined(glyph.textureInfo)) {
  235. // we have a texture and billboard. If we had one before, release
  236. // our reference to that texture info, but reuse the billboard.
  237. glyph.textureInfo = undefined;
  238. }
  239. } else {
  240. // create a glyph object
  241. glyph = new Glyph();
  242. glyphs[textIndex] = glyph;
  243. }
  244. glyph.textureInfo = glyphTextureInfo;
  245. glyph.dimensions = glyphTextureInfo.dimensions;
  246. // if we have a texture, configure the existing billboard, or obtain one
  247. if (glyphTextureInfo.index !== -1) {
  248. let billboard = glyph.billboard;
  249. const spareBillboards = labelCollection._spareBillboards;
  250. if (!defined(billboard)) {
  251. if (spareBillboards.length > 0) {
  252. billboard = spareBillboards.pop();
  253. } else {
  254. billboard = labelCollection._billboardCollection.add({
  255. collection: labelCollection,
  256. });
  257. billboard._labelDimensions = new Cartesian2();
  258. billboard._labelTranslate = new Cartesian2();
  259. }
  260. glyph.billboard = billboard;
  261. }
  262. billboard.show = label._show;
  263. billboard.position = label._position;
  264. billboard.eyeOffset = label._eyeOffset;
  265. billboard.pixelOffset = label._pixelOffset;
  266. billboard.horizontalOrigin = HorizontalOrigin.LEFT;
  267. billboard.verticalOrigin = label._verticalOrigin;
  268. billboard.heightReference = label._heightReference;
  269. billboard.scale = label.totalScale;
  270. billboard.pickPrimitive = label;
  271. billboard.id = label._id;
  272. billboard.image = id;
  273. billboard.translucencyByDistance = label._translucencyByDistance;
  274. billboard.pixelOffsetScaleByDistance = label._pixelOffsetScaleByDistance;
  275. billboard.scaleByDistance = label._scaleByDistance;
  276. billboard.distanceDisplayCondition = label._distanceDisplayCondition;
  277. billboard.disableDepthTestDistance = label._disableDepthTestDistance;
  278. billboard._batchIndex = label._batchIndex;
  279. billboard.outlineColor = label.outlineColor;
  280. if (label.style === LabelStyle.FILL_AND_OUTLINE) {
  281. billboard.color = label._fillColor;
  282. billboard.outlineWidth = label.outlineWidth;
  283. } else if (label.style === LabelStyle.FILL) {
  284. billboard.color = label._fillColor;
  285. billboard.outlineWidth = 0.0;
  286. } else if (label.style === LabelStyle.OUTLINE) {
  287. billboard.color = Color.TRANSPARENT;
  288. billboard.outlineWidth = label.outlineWidth;
  289. }
  290. }
  291. }
  292. // changing glyphs will cause the position of the
  293. // glyphs to change, since different characters have different widths
  294. label._repositionAllGlyphs = true;
  295. }
  296. function calculateWidthOffset(lineWidth, horizontalOrigin, backgroundPadding) {
  297. if (horizontalOrigin === HorizontalOrigin.CENTER) {
  298. return -lineWidth / 2;
  299. } else if (horizontalOrigin === HorizontalOrigin.RIGHT) {
  300. return -(lineWidth + backgroundPadding.x);
  301. }
  302. return backgroundPadding.x;
  303. }
  304. // reusable Cartesian2 instances
  305. const glyphPixelOffset = new Cartesian2();
  306. const scratchBackgroundPadding = new Cartesian2();
  307. function repositionAllGlyphs(label) {
  308. const glyphs = label._glyphs;
  309. const text = label._renderedText;
  310. let glyph;
  311. let dimensions;
  312. let lastLineWidth = 0;
  313. let maxLineWidth = 0;
  314. const lineWidths = [];
  315. let maxGlyphDescent = Number.NEGATIVE_INFINITY;
  316. let maxGlyphY = 0;
  317. let numberOfLines = 1;
  318. let glyphIndex;
  319. const glyphLength = glyphs.length;
  320. const backgroundBillboard = label._backgroundBillboard;
  321. const backgroundPadding = Cartesian2.clone(
  322. defined(backgroundBillboard) ? label._backgroundPadding : Cartesian2.ZERO,
  323. scratchBackgroundPadding
  324. );
  325. // We need to scale the background padding, which is specified in pixels by the inverse of the relative size so it is scaled properly.
  326. backgroundPadding.x /= label._relativeSize;
  327. backgroundPadding.y /= label._relativeSize;
  328. for (glyphIndex = 0; glyphIndex < glyphLength; ++glyphIndex) {
  329. if (text.charAt(glyphIndex) === "\n") {
  330. lineWidths.push(lastLineWidth);
  331. ++numberOfLines;
  332. lastLineWidth = 0;
  333. } else {
  334. glyph = glyphs[glyphIndex];
  335. dimensions = glyph.dimensions;
  336. maxGlyphY = Math.max(maxGlyphY, dimensions.height - dimensions.descent);
  337. maxGlyphDescent = Math.max(maxGlyphDescent, dimensions.descent);
  338. //Computing the line width must also account for the kerning that occurs between letters.
  339. lastLineWidth += dimensions.width - dimensions.minx;
  340. if (glyphIndex < glyphLength - 1) {
  341. lastLineWidth += glyphs[glyphIndex + 1].dimensions.minx;
  342. }
  343. maxLineWidth = Math.max(maxLineWidth, lastLineWidth);
  344. }
  345. }
  346. lineWidths.push(lastLineWidth);
  347. const maxLineHeight = maxGlyphY + maxGlyphDescent;
  348. const scale = label.totalScale;
  349. const horizontalOrigin = label._horizontalOrigin;
  350. const verticalOrigin = label._verticalOrigin;
  351. let lineIndex = 0;
  352. let lineWidth = lineWidths[lineIndex];
  353. let widthOffset = calculateWidthOffset(
  354. lineWidth,
  355. horizontalOrigin,
  356. backgroundPadding
  357. );
  358. const lineSpacing =
  359. (defined(label._lineHeight)
  360. ? label._lineHeight
  361. : defaultLineSpacingPercent * label._fontSize) / label._relativeSize;
  362. const otherLinesHeight = lineSpacing * (numberOfLines - 1);
  363. let totalLineWidth = maxLineWidth;
  364. let totalLineHeight = maxLineHeight + otherLinesHeight;
  365. if (defined(backgroundBillboard)) {
  366. totalLineWidth += backgroundPadding.x * 2;
  367. totalLineHeight += backgroundPadding.y * 2;
  368. backgroundBillboard._labelHorizontalOrigin = horizontalOrigin;
  369. }
  370. glyphPixelOffset.x = widthOffset * scale;
  371. glyphPixelOffset.y = 0;
  372. let firstCharOfLine = true;
  373. let lineOffsetY = 0;
  374. for (glyphIndex = 0; glyphIndex < glyphLength; ++glyphIndex) {
  375. if (text.charAt(glyphIndex) === "\n") {
  376. ++lineIndex;
  377. lineOffsetY += lineSpacing;
  378. lineWidth = lineWidths[lineIndex];
  379. widthOffset = calculateWidthOffset(
  380. lineWidth,
  381. horizontalOrigin,
  382. backgroundPadding
  383. );
  384. glyphPixelOffset.x = widthOffset * scale;
  385. firstCharOfLine = true;
  386. } else {
  387. glyph = glyphs[glyphIndex];
  388. dimensions = glyph.dimensions;
  389. if (verticalOrigin === VerticalOrigin.TOP) {
  390. glyphPixelOffset.y =
  391. dimensions.height - maxGlyphY - backgroundPadding.y;
  392. glyphPixelOffset.y += SDFSettings.PADDING;
  393. } else if (verticalOrigin === VerticalOrigin.CENTER) {
  394. glyphPixelOffset.y =
  395. (otherLinesHeight + dimensions.height - maxGlyphY) / 2;
  396. } else if (verticalOrigin === VerticalOrigin.BASELINE) {
  397. glyphPixelOffset.y = otherLinesHeight;
  398. glyphPixelOffset.y -= SDFSettings.PADDING;
  399. } else {
  400. // VerticalOrigin.BOTTOM
  401. glyphPixelOffset.y =
  402. otherLinesHeight + maxGlyphDescent + backgroundPadding.y;
  403. glyphPixelOffset.y -= SDFSettings.PADDING;
  404. }
  405. glyphPixelOffset.y =
  406. (glyphPixelOffset.y - dimensions.descent - lineOffsetY) * scale;
  407. // Handle any offsets for the first character of the line since the bounds might not be right on the bottom left pixel.
  408. if (firstCharOfLine) {
  409. glyphPixelOffset.x -= SDFSettings.PADDING * scale;
  410. firstCharOfLine = false;
  411. }
  412. if (defined(glyph.billboard)) {
  413. glyph.billboard._setTranslate(glyphPixelOffset);
  414. glyph.billboard._labelDimensions.x = totalLineWidth;
  415. glyph.billboard._labelDimensions.y = totalLineHeight;
  416. glyph.billboard._labelHorizontalOrigin = horizontalOrigin;
  417. }
  418. //Compute the next x offset taking into account the kerning performed
  419. //on both the current letter as well as the next letter to be drawn
  420. //as well as any applied scale.
  421. if (glyphIndex < glyphLength - 1) {
  422. const nextGlyph = glyphs[glyphIndex + 1];
  423. glyphPixelOffset.x +=
  424. (dimensions.width - dimensions.minx + nextGlyph.dimensions.minx) *
  425. scale;
  426. }
  427. }
  428. }
  429. if (defined(backgroundBillboard) && text.split("\n").join("").length > 0) {
  430. if (horizontalOrigin === HorizontalOrigin.CENTER) {
  431. widthOffset = -maxLineWidth / 2 - backgroundPadding.x;
  432. } else if (horizontalOrigin === HorizontalOrigin.RIGHT) {
  433. widthOffset = -(maxLineWidth + backgroundPadding.x * 2);
  434. } else {
  435. widthOffset = 0;
  436. }
  437. glyphPixelOffset.x = widthOffset * scale;
  438. if (verticalOrigin === VerticalOrigin.TOP) {
  439. glyphPixelOffset.y = maxLineHeight - maxGlyphY - maxGlyphDescent;
  440. } else if (verticalOrigin === VerticalOrigin.CENTER) {
  441. glyphPixelOffset.y = (maxLineHeight - maxGlyphY) / 2 - maxGlyphDescent;
  442. } else if (verticalOrigin === VerticalOrigin.BASELINE) {
  443. glyphPixelOffset.y = -backgroundPadding.y - maxGlyphDescent;
  444. } else {
  445. // VerticalOrigin.BOTTOM
  446. glyphPixelOffset.y = 0;
  447. }
  448. glyphPixelOffset.y = glyphPixelOffset.y * scale;
  449. backgroundBillboard.width = totalLineWidth;
  450. backgroundBillboard.height = totalLineHeight;
  451. backgroundBillboard._setTranslate(glyphPixelOffset);
  452. backgroundBillboard._labelTranslate = Cartesian2.clone(
  453. glyphPixelOffset,
  454. backgroundBillboard._labelTranslate
  455. );
  456. }
  457. if (label.heightReference === HeightReference.CLAMP_TO_GROUND) {
  458. for (glyphIndex = 0; glyphIndex < glyphLength; ++glyphIndex) {
  459. glyph = glyphs[glyphIndex];
  460. const billboard = glyph.billboard;
  461. if (defined(billboard)) {
  462. billboard._labelTranslate = Cartesian2.clone(
  463. glyphPixelOffset,
  464. billboard._labelTranslate
  465. );
  466. }
  467. }
  468. }
  469. }
  470. function destroyLabel(labelCollection, label) {
  471. const glyphs = label._glyphs;
  472. for (let i = 0, len = glyphs.length; i < len; ++i) {
  473. unbindGlyph(labelCollection, glyphs[i]);
  474. }
  475. if (defined(label._backgroundBillboard)) {
  476. labelCollection._backgroundBillboardCollection.remove(
  477. label._backgroundBillboard
  478. );
  479. label._backgroundBillboard = undefined;
  480. }
  481. label._labelCollection = undefined;
  482. if (defined(label._removeCallbackFunc)) {
  483. label._removeCallbackFunc();
  484. }
  485. destroyObject(label);
  486. }
  487. /**
  488. * A renderable collection of labels. Labels are viewport-aligned text positioned in the 3D scene.
  489. * Each label can have a different font, color, scale, etc.
  490. * <br /><br />
  491. * <div align='center'>
  492. * <img src='Images/Label.png' width='400' height='300' /><br />
  493. * Example labels
  494. * </div>
  495. * <br /><br />
  496. * Labels are added and removed from the collection using {@link LabelCollection#add}
  497. * and {@link LabelCollection#remove}.
  498. *
  499. * @alias LabelCollection
  500. * @constructor
  501. *
  502. * @param {object} [options] Object with the following properties:
  503. * @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] The 4x4 transformation matrix that transforms each label from model to world coordinates.
  504. * @param {boolean} [options.debugShowBoundingVolume=false] For debugging only. Determines if this primitive's commands' bounding spheres are shown.
  505. * @param {Scene} [options.scene] Must be passed in for labels that use the height reference property or will be depth tested against the globe.
  506. * @param {BlendOption} [options.blendOption=BlendOption.OPAQUE_AND_TRANSLUCENT] The label blending option. The default
  507. * is used for rendering both opaque and translucent labels. However, if either all of the labels are completely opaque or all are completely translucent,
  508. * setting the technique to BlendOption.OPAQUE or BlendOption.TRANSLUCENT can improve performance by up to 2x.
  509. * @param {boolean} [options.show=true] Determines if the labels in the collection will be shown.
  510. *
  511. * @performance For best performance, prefer a few collections, each with many labels, to
  512. * many collections with only a few labels each. Avoid having collections where some
  513. * labels change every frame and others do not; instead, create one or more collections
  514. * for static labels, and one or more collections for dynamic labels.
  515. *
  516. * @see LabelCollection#add
  517. * @see LabelCollection#remove
  518. * @see Label
  519. * @see BillboardCollection
  520. *
  521. * @demo {@link https://sandcastle.cesium.com/index.html?src=Labels.html|Cesium Sandcastle Labels Demo}
  522. *
  523. * @example
  524. * // Create a label collection with two labels
  525. * const labels = scene.primitives.add(new Cesium.LabelCollection());
  526. * labels.add({
  527. * position : new Cesium.Cartesian3(1.0, 2.0, 3.0),
  528. * text : 'A label'
  529. * });
  530. * labels.add({
  531. * position : new Cesium.Cartesian3(4.0, 5.0, 6.0),
  532. * text : 'Another label'
  533. * });
  534. */
  535. function LabelCollection(options) {
  536. options = defaultValue(options, defaultValue.EMPTY_OBJECT);
  537. this._scene = options.scene;
  538. this._batchTable = options.batchTable;
  539. this._textureAtlas = undefined;
  540. this._backgroundTextureAtlas = undefined;
  541. this._backgroundBillboardCollection = new BillboardCollection({
  542. scene: this._scene,
  543. });
  544. this._backgroundBillboardCollection.destroyTextureAtlas = false;
  545. this._billboardCollection = new BillboardCollection({
  546. scene: this._scene,
  547. batchTable: this._batchTable,
  548. });
  549. this._billboardCollection.destroyTextureAtlas = false;
  550. this._billboardCollection._sdf = true;
  551. this._spareBillboards = [];
  552. this._glyphTextureCache = {};
  553. this._labels = [];
  554. this._labelsToUpdate = [];
  555. this._totalGlyphCount = 0;
  556. this._highlightColor = Color.clone(Color.WHITE); // Only used by Vector3DTilePoints
  557. /**
  558. * Determines if labels in this collection will be shown.
  559. *
  560. * @type {boolean}
  561. * @default true
  562. */
  563. this.show = defaultValue(options.show, true);
  564. /**
  565. * The 4x4 transformation matrix that transforms each label in this collection from model to world coordinates.
  566. * When this is the identity matrix, the labels are drawn in world coordinates, i.e., Earth's WGS84 coordinates.
  567. * Local reference frames can be used by providing a different transformation matrix, like that returned
  568. * by {@link Transforms.eastNorthUpToFixedFrame}.
  569. *
  570. * @type Matrix4
  571. * @default {@link Matrix4.IDENTITY}
  572. *
  573. * @example
  574. * const center = Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883);
  575. * labels.modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(center);
  576. * labels.add({
  577. * position : new Cesium.Cartesian3(0.0, 0.0, 0.0),
  578. * text : 'Center'
  579. * });
  580. * labels.add({
  581. * position : new Cesium.Cartesian3(1000000.0, 0.0, 0.0),
  582. * text : 'East'
  583. * });
  584. * labels.add({
  585. * position : new Cesium.Cartesian3(0.0, 1000000.0, 0.0),
  586. * text : 'North'
  587. * });
  588. * labels.add({
  589. * position : new Cesium.Cartesian3(0.0, 0.0, 1000000.0),
  590. * text : 'Up'
  591. * });
  592. */
  593. this.modelMatrix = Matrix4.clone(
  594. defaultValue(options.modelMatrix, Matrix4.IDENTITY)
  595. );
  596. /**
  597. * This property is for debugging only; it is not for production use nor is it optimized.
  598. * <p>
  599. * Draws the bounding sphere for each draw command in the primitive.
  600. * </p>
  601. *
  602. * @type {boolean}
  603. *
  604. * @default false
  605. */
  606. this.debugShowBoundingVolume = defaultValue(
  607. options.debugShowBoundingVolume,
  608. false
  609. );
  610. /**
  611. * The label blending option. The default is used for rendering both opaque and translucent labels.
  612. * However, if either all of the labels are completely opaque or all are completely translucent,
  613. * setting the technique to BlendOption.OPAQUE or BlendOption.TRANSLUCENT can improve
  614. * performance by up to 2x.
  615. * @type {BlendOption}
  616. * @default BlendOption.OPAQUE_AND_TRANSLUCENT
  617. */
  618. this.blendOption = defaultValue(
  619. options.blendOption,
  620. BlendOption.OPAQUE_AND_TRANSLUCENT
  621. );
  622. }
  623. Object.defineProperties(LabelCollection.prototype, {
  624. /**
  625. * Returns the number of labels in this collection. This is commonly used with
  626. * {@link LabelCollection#get} to iterate over all the labels
  627. * in the collection.
  628. * @memberof LabelCollection.prototype
  629. * @type {number}
  630. */
  631. length: {
  632. get: function () {
  633. return this._labels.length;
  634. },
  635. },
  636. });
  637. /**
  638. * Creates and adds a label with the specified initial properties to the collection.
  639. * The added label is returned so it can be modified or removed from the collection later.
  640. *
  641. * @param {object} [options] A template describing the label's properties as shown in Example 1.
  642. * @returns {Label} The label that was added to the collection.
  643. *
  644. * @performance Calling <code>add</code> is expected constant time. However, the collection's vertex buffer
  645. * is rewritten; this operations is <code>O(n)</code> and also incurs
  646. * CPU to GPU overhead. For best performance, add as many billboards as possible before
  647. * calling <code>update</code>.
  648. *
  649. * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
  650. *
  651. *
  652. * @example
  653. * // Example 1: Add a label, specifying all the default values.
  654. * const l = labels.add({
  655. * show : true,
  656. * position : Cesium.Cartesian3.ZERO,
  657. * text : '',
  658. * font : '30px sans-serif',
  659. * fillColor : Cesium.Color.WHITE,
  660. * outlineColor : Cesium.Color.BLACK,
  661. * outlineWidth : 1.0,
  662. * showBackground : false,
  663. * backgroundColor : new Cesium.Color(0.165, 0.165, 0.165, 0.8),
  664. * backgroundPadding : new Cesium.Cartesian2(7, 5),
  665. * style : Cesium.LabelStyle.FILL,
  666. * pixelOffset : Cesium.Cartesian2.ZERO,
  667. * eyeOffset : Cesium.Cartesian3.ZERO,
  668. * horizontalOrigin : Cesium.HorizontalOrigin.LEFT,
  669. * verticalOrigin : Cesium.VerticalOrigin.BASELINE,
  670. * scale : 1.0,
  671. * translucencyByDistance : undefined,
  672. * pixelOffsetScaleByDistance : undefined,
  673. * heightReference : HeightReference.NONE,
  674. * distanceDisplayCondition : undefined
  675. * });
  676. *
  677. * @example
  678. * // Example 2: Specify only the label's cartographic position,
  679. * // text, and font.
  680. * const l = labels.add({
  681. * position : Cesium.Cartesian3.fromRadians(longitude, latitude, height),
  682. * text : 'Hello World',
  683. * font : '24px Helvetica',
  684. * });
  685. *
  686. * @see LabelCollection#remove
  687. * @see LabelCollection#removeAll
  688. */
  689. LabelCollection.prototype.add = function (options) {
  690. const label = new Label(options, this);
  691. this._labels.push(label);
  692. this._labelsToUpdate.push(label);
  693. return label;
  694. };
  695. /**
  696. * Removes a label from the collection. Once removed, a label is no longer usable.
  697. *
  698. * @param {Label} label The label to remove.
  699. * @returns {boolean} <code>true</code> if the label was removed; <code>false</code> if the label was not found in the collection.
  700. *
  701. * @performance Calling <code>remove</code> is expected constant time. However, the collection's vertex buffer
  702. * is rewritten - an <code>O(n)</code> operation that also incurs CPU to GPU overhead. For
  703. * best performance, remove as many labels as possible before calling <code>update</code>.
  704. * If you intend to temporarily hide a label, it is usually more efficient to call
  705. * {@link Label#show} instead of removing and re-adding the label.
  706. *
  707. * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
  708. *
  709. *
  710. * @example
  711. * const l = labels.add(...);
  712. * labels.remove(l); // Returns true
  713. *
  714. * @see LabelCollection#add
  715. * @see LabelCollection#removeAll
  716. * @see Label#show
  717. */
  718. LabelCollection.prototype.remove = function (label) {
  719. if (defined(label) && label._labelCollection === this) {
  720. const index = this._labels.indexOf(label);
  721. if (index !== -1) {
  722. this._labels.splice(index, 1);
  723. destroyLabel(this, label);
  724. return true;
  725. }
  726. }
  727. return false;
  728. };
  729. /**
  730. * Removes all labels from the collection.
  731. *
  732. * @performance <code>O(n)</code>. It is more efficient to remove all the labels
  733. * from a collection and then add new ones than to create a new collection entirely.
  734. *
  735. * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
  736. *
  737. *
  738. * @example
  739. * labels.add(...);
  740. * labels.add(...);
  741. * labels.removeAll();
  742. *
  743. * @see LabelCollection#add
  744. * @see LabelCollection#remove
  745. */
  746. LabelCollection.prototype.removeAll = function () {
  747. const labels = this._labels;
  748. for (let i = 0, len = labels.length; i < len; ++i) {
  749. destroyLabel(this, labels[i]);
  750. }
  751. labels.length = 0;
  752. };
  753. /**
  754. * Check whether this collection contains a given label.
  755. *
  756. * @param {Label} label The label to check for.
  757. * @returns {boolean} true if this collection contains the label, false otherwise.
  758. *
  759. * @see LabelCollection#get
  760. *
  761. */
  762. LabelCollection.prototype.contains = function (label) {
  763. return defined(label) && label._labelCollection === this;
  764. };
  765. /**
  766. * Returns the label in the collection at the specified index. Indices are zero-based
  767. * and increase as labels are added. Removing a label shifts all labels after
  768. * it to the left, changing their indices. This function is commonly used with
  769. * {@link LabelCollection#length} to iterate over all the labels
  770. * in the collection.
  771. *
  772. * @param {number} index The zero-based index of the billboard.
  773. *
  774. * @returns {Label} The label at the specified index.
  775. *
  776. * @performance Expected constant time. If labels were removed from the collection and
  777. * {@link Scene#render} was not called, an implicit <code>O(n)</code>
  778. * operation is performed.
  779. *
  780. * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
  781. *
  782. *
  783. * @example
  784. * // Toggle the show property of every label in the collection
  785. * const len = labels.length;
  786. * for (let i = 0; i < len; ++i) {
  787. * const l = billboards.get(i);
  788. * l.show = !l.show;
  789. * }
  790. *
  791. * @see LabelCollection#length
  792. */
  793. LabelCollection.prototype.get = function (index) {
  794. //>>includeStart('debug', pragmas.debug);
  795. if (!defined(index)) {
  796. throw new DeveloperError("index is required.");
  797. }
  798. //>>includeEnd('debug');
  799. return this._labels[index];
  800. };
  801. /**
  802. * @private
  803. *
  804. */
  805. LabelCollection.prototype.update = function (frameState) {
  806. if (!this.show) {
  807. return;
  808. }
  809. const billboardCollection = this._billboardCollection;
  810. const backgroundBillboardCollection = this._backgroundBillboardCollection;
  811. billboardCollection.modelMatrix = this.modelMatrix;
  812. billboardCollection.debugShowBoundingVolume = this.debugShowBoundingVolume;
  813. backgroundBillboardCollection.modelMatrix = this.modelMatrix;
  814. backgroundBillboardCollection.debugShowBoundingVolume = this.debugShowBoundingVolume;
  815. const context = frameState.context;
  816. if (!defined(this._textureAtlas)) {
  817. this._textureAtlas = new TextureAtlas({
  818. context: context,
  819. });
  820. billboardCollection.textureAtlas = this._textureAtlas;
  821. }
  822. if (!defined(this._backgroundTextureAtlas)) {
  823. this._backgroundTextureAtlas = new TextureAtlas({
  824. context: context,
  825. initialSize: whitePixelSize,
  826. });
  827. backgroundBillboardCollection.textureAtlas = this._backgroundTextureAtlas;
  828. addWhitePixelCanvas(this._backgroundTextureAtlas);
  829. }
  830. const len = this._labelsToUpdate.length;
  831. for (let i = 0; i < len; ++i) {
  832. const label = this._labelsToUpdate[i];
  833. if (label.isDestroyed()) {
  834. continue;
  835. }
  836. const preUpdateGlyphCount = label._glyphs.length;
  837. if (label._rebindAllGlyphs) {
  838. rebindAllGlyphs(this, label);
  839. label._rebindAllGlyphs = false;
  840. }
  841. if (label._repositionAllGlyphs) {
  842. repositionAllGlyphs(label);
  843. label._repositionAllGlyphs = false;
  844. }
  845. const glyphCountDifference = label._glyphs.length - preUpdateGlyphCount;
  846. this._totalGlyphCount += glyphCountDifference;
  847. }
  848. const blendOption =
  849. backgroundBillboardCollection.length > 0
  850. ? BlendOption.TRANSLUCENT
  851. : this.blendOption;
  852. billboardCollection.blendOption = blendOption;
  853. backgroundBillboardCollection.blendOption = blendOption;
  854. billboardCollection._highlightColor = this._highlightColor;
  855. backgroundBillboardCollection._highlightColor = this._highlightColor;
  856. this._labelsToUpdate.length = 0;
  857. backgroundBillboardCollection.update(frameState);
  858. billboardCollection.update(frameState);
  859. };
  860. /**
  861. * Returns true if this object was destroyed; otherwise, false.
  862. * <br /><br />
  863. * If this object was destroyed, it should not be used; calling any function other than
  864. * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
  865. *
  866. * @returns {boolean} True if this object was destroyed; otherwise, false.
  867. *
  868. * @see LabelCollection#destroy
  869. */
  870. LabelCollection.prototype.isDestroyed = function () {
  871. return false;
  872. };
  873. /**
  874. * Destroys the WebGL resources held by this object. Destroying an object allows for deterministic
  875. * release of WebGL resources, instead of relying on the garbage collector to destroy this object.
  876. * <br /><br />
  877. * Once an object is destroyed, it should not be used; calling any function other than
  878. * <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
  879. * assign the return value (<code>undefined</code>) to the object as done in the example.
  880. *
  881. * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
  882. *
  883. *
  884. * @example
  885. * labels = labels && labels.destroy();
  886. *
  887. * @see LabelCollection#isDestroyed
  888. */
  889. LabelCollection.prototype.destroy = function () {
  890. this.removeAll();
  891. this._billboardCollection = this._billboardCollection.destroy();
  892. this._textureAtlas = this._textureAtlas && this._textureAtlas.destroy();
  893. this._backgroundBillboardCollection = this._backgroundBillboardCollection.destroy();
  894. this._backgroundTextureAtlas =
  895. this._backgroundTextureAtlas && this._backgroundTextureAtlas.destroy();
  896. return destroyObject(this);
  897. };
  898. export default LabelCollection;