智慧水务管理系统 - 精河县供水工程综合管理平台

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