LabelCollection.js 39 KB

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