EntityCluster.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997
  1. import BoundingRectangle from "../Core/BoundingRectangle.js";
  2. import Cartesian2 from "../Core/Cartesian2.js";
  3. import Cartesian3 from "../Core/Cartesian3.js";
  4. import defaultValue from "../Core/defaultValue.js";
  5. import defined from "../Core/defined.js";
  6. import EllipsoidalOccluder from "../Core/EllipsoidalOccluder.js";
  7. import Event from "../Core/Event.js";
  8. import Matrix4 from "../Core/Matrix4.js";
  9. import Billboard from "../Scene/Billboard.js";
  10. import BillboardCollection from "../Scene/BillboardCollection.js";
  11. import Label from "../Scene/Label.js";
  12. import LabelCollection from "../Scene/LabelCollection.js";
  13. import PointPrimitive from "../Scene/PointPrimitive.js";
  14. import PointPrimitiveCollection from "../Scene/PointPrimitiveCollection.js";
  15. import SceneMode from "../Scene/SceneMode.js";
  16. import KDBush from "../ThirdParty/kdbush.js";
  17. /**
  18. * Defines how screen space objects (billboards, points, labels) are clustered.
  19. *
  20. * @param {Object} [options] An object with the following properties:
  21. * @param {Boolean} [options.enabled=false] Whether or not to enable clustering.
  22. * @param {Number} [options.pixelRange=80] The pixel range to extend the screen space bounding box.
  23. * @param {Number} [options.minimumClusterSize=2] The minimum number of screen space objects that can be clustered.
  24. * @param {Boolean} [options.clusterBillboards=true] Whether or not to cluster the billboards of an entity.
  25. * @param {Boolean} [options.clusterLabels=true] Whether or not to cluster the labels of an entity.
  26. * @param {Boolean} [options.clusterPoints=true] Whether or not to cluster the points of an entity.
  27. * @param {Boolean} [options.show=true] Determines if the entities in the cluster will be shown.
  28. *
  29. * @alias EntityCluster
  30. * @constructor
  31. *
  32. * @demo {@link https://sandcastle.cesium.com/index.html?src=Clustering.html|Cesium Sandcastle Clustering Demo}
  33. */
  34. function EntityCluster(options) {
  35. options = defaultValue(options, defaultValue.EMPTY_OBJECT);
  36. this._enabled = defaultValue(options.enabled, false);
  37. this._pixelRange = defaultValue(options.pixelRange, 80);
  38. this._minimumClusterSize = defaultValue(options.minimumClusterSize, 2);
  39. this._clusterBillboards = defaultValue(options.clusterBillboards, true);
  40. this._clusterLabels = defaultValue(options.clusterLabels, true);
  41. this._clusterPoints = defaultValue(options.clusterPoints, true);
  42. this._labelCollection = undefined;
  43. this._billboardCollection = undefined;
  44. this._pointCollection = undefined;
  45. this._clusterBillboardCollection = undefined;
  46. this._clusterLabelCollection = undefined;
  47. this._clusterPointCollection = undefined;
  48. this._collectionIndicesByEntity = {};
  49. this._unusedLabelIndices = [];
  50. this._unusedBillboardIndices = [];
  51. this._unusedPointIndices = [];
  52. this._previousClusters = [];
  53. this._previousHeight = undefined;
  54. this._enabledDirty = false;
  55. this._clusterDirty = false;
  56. this._cluster = undefined;
  57. this._removeEventListener = undefined;
  58. this._clusterEvent = new Event();
  59. /**
  60. * Determines if entities in this collection will be shown.
  61. *
  62. * @type {Boolean}
  63. * @default true
  64. */
  65. this.show = defaultValue(options.show, true);
  66. }
  67. function getX(point) {
  68. return point.coord.x;
  69. }
  70. function getY(point) {
  71. return point.coord.y;
  72. }
  73. function expandBoundingBox(bbox, pixelRange) {
  74. bbox.x -= pixelRange;
  75. bbox.y -= pixelRange;
  76. bbox.width += pixelRange * 2.0;
  77. bbox.height += pixelRange * 2.0;
  78. }
  79. const labelBoundingBoxScratch = new BoundingRectangle();
  80. function getBoundingBox(item, coord, pixelRange, entityCluster, result) {
  81. if (defined(item._labelCollection) && entityCluster._clusterLabels) {
  82. result = Label.getScreenSpaceBoundingBox(item, coord, result);
  83. } else if (
  84. defined(item._billboardCollection) &&
  85. entityCluster._clusterBillboards
  86. ) {
  87. result = Billboard.getScreenSpaceBoundingBox(item, coord, result);
  88. } else if (
  89. defined(item._pointPrimitiveCollection) &&
  90. entityCluster._clusterPoints
  91. ) {
  92. result = PointPrimitive.getScreenSpaceBoundingBox(item, coord, result);
  93. }
  94. expandBoundingBox(result, pixelRange);
  95. if (
  96. entityCluster._clusterLabels &&
  97. !defined(item._labelCollection) &&
  98. defined(item.id) &&
  99. hasLabelIndex(entityCluster, item.id.id) &&
  100. defined(item.id._label)
  101. ) {
  102. const labelIndex =
  103. entityCluster._collectionIndicesByEntity[item.id.id].labelIndex;
  104. const label = entityCluster._labelCollection.get(labelIndex);
  105. const labelBBox = Label.getScreenSpaceBoundingBox(
  106. label,
  107. coord,
  108. labelBoundingBoxScratch
  109. );
  110. expandBoundingBox(labelBBox, pixelRange);
  111. result = BoundingRectangle.union(result, labelBBox, result);
  112. }
  113. return result;
  114. }
  115. function addNonClusteredItem(item, entityCluster) {
  116. item.clusterShow = true;
  117. if (
  118. !defined(item._labelCollection) &&
  119. defined(item.id) &&
  120. hasLabelIndex(entityCluster, item.id.id) &&
  121. defined(item.id._label)
  122. ) {
  123. const labelIndex =
  124. entityCluster._collectionIndicesByEntity[item.id.id].labelIndex;
  125. const label = entityCluster._labelCollection.get(labelIndex);
  126. label.clusterShow = true;
  127. }
  128. }
  129. function addCluster(position, numPoints, ids, entityCluster) {
  130. const cluster = {
  131. billboard: entityCluster._clusterBillboardCollection.add(),
  132. label: entityCluster._clusterLabelCollection.add(),
  133. point: entityCluster._clusterPointCollection.add(),
  134. };
  135. cluster.billboard.show = false;
  136. cluster.point.show = false;
  137. cluster.label.show = true;
  138. cluster.label.text = numPoints.toLocaleString();
  139. cluster.label.id = ids;
  140. cluster.billboard.position = cluster.label.position = cluster.point.position = position;
  141. entityCluster._clusterEvent.raiseEvent(ids, cluster);
  142. }
  143. function hasLabelIndex(entityCluster, entityId) {
  144. return (
  145. defined(entityCluster) &&
  146. defined(entityCluster._collectionIndicesByEntity[entityId]) &&
  147. defined(entityCluster._collectionIndicesByEntity[entityId].labelIndex)
  148. );
  149. }
  150. function getScreenSpacePositions(
  151. collection,
  152. points,
  153. scene,
  154. occluder,
  155. entityCluster
  156. ) {
  157. if (!defined(collection)) {
  158. return;
  159. }
  160. const length = collection.length;
  161. for (let i = 0; i < length; ++i) {
  162. const item = collection.get(i);
  163. item.clusterShow = false;
  164. if (
  165. !item.show ||
  166. (entityCluster._scene.mode === SceneMode.SCENE3D &&
  167. !occluder.isPointVisible(item.position))
  168. ) {
  169. continue;
  170. }
  171. const canClusterLabels =
  172. entityCluster._clusterLabels && defined(item._labelCollection);
  173. const canClusterBillboards =
  174. entityCluster._clusterBillboards && defined(item.id._billboard);
  175. const canClusterPoints =
  176. entityCluster._clusterPoints && defined(item.id._point);
  177. if (canClusterLabels && (canClusterPoints || canClusterBillboards)) {
  178. continue;
  179. }
  180. const coord = item.computeScreenSpacePosition(scene);
  181. if (!defined(coord)) {
  182. continue;
  183. }
  184. points.push({
  185. index: i,
  186. collection: collection,
  187. clustered: false,
  188. coord: coord,
  189. });
  190. }
  191. }
  192. const pointBoundinRectangleScratch = new BoundingRectangle();
  193. const totalBoundingRectangleScratch = new BoundingRectangle();
  194. const neighborBoundingRectangleScratch = new BoundingRectangle();
  195. function createDeclutterCallback(entityCluster) {
  196. return function (amount) {
  197. if ((defined(amount) && amount < 0.05) || !entityCluster.enabled) {
  198. return;
  199. }
  200. const scene = entityCluster._scene;
  201. const labelCollection = entityCluster._labelCollection;
  202. const billboardCollection = entityCluster._billboardCollection;
  203. const pointCollection = entityCluster._pointCollection;
  204. if (
  205. (!defined(labelCollection) &&
  206. !defined(billboardCollection) &&
  207. !defined(pointCollection)) ||
  208. (!entityCluster._clusterBillboards &&
  209. !entityCluster._clusterLabels &&
  210. !entityCluster._clusterPoints)
  211. ) {
  212. return;
  213. }
  214. let clusteredLabelCollection = entityCluster._clusterLabelCollection;
  215. let clusteredBillboardCollection =
  216. entityCluster._clusterBillboardCollection;
  217. let clusteredPointCollection = entityCluster._clusterPointCollection;
  218. if (defined(clusteredLabelCollection)) {
  219. clusteredLabelCollection.removeAll();
  220. } else {
  221. clusteredLabelCollection = entityCluster._clusterLabelCollection = new LabelCollection(
  222. {
  223. scene: scene,
  224. }
  225. );
  226. }
  227. if (defined(clusteredBillboardCollection)) {
  228. clusteredBillboardCollection.removeAll();
  229. } else {
  230. clusteredBillboardCollection = entityCluster._clusterBillboardCollection = new BillboardCollection(
  231. {
  232. scene: scene,
  233. }
  234. );
  235. }
  236. if (defined(clusteredPointCollection)) {
  237. clusteredPointCollection.removeAll();
  238. } else {
  239. clusteredPointCollection = entityCluster._clusterPointCollection = new PointPrimitiveCollection();
  240. }
  241. const pixelRange = entityCluster._pixelRange;
  242. const minimumClusterSize = entityCluster._minimumClusterSize;
  243. const clusters = entityCluster._previousClusters;
  244. const newClusters = [];
  245. const previousHeight = entityCluster._previousHeight;
  246. const currentHeight = scene.camera.positionCartographic.height;
  247. const ellipsoid = scene.mapProjection.ellipsoid;
  248. const cameraPosition = scene.camera.positionWC;
  249. const occluder = new EllipsoidalOccluder(ellipsoid, cameraPosition);
  250. const points = [];
  251. if (entityCluster._clusterLabels) {
  252. getScreenSpacePositions(
  253. labelCollection,
  254. points,
  255. scene,
  256. occluder,
  257. entityCluster
  258. );
  259. }
  260. if (entityCluster._clusterBillboards) {
  261. getScreenSpacePositions(
  262. billboardCollection,
  263. points,
  264. scene,
  265. occluder,
  266. entityCluster
  267. );
  268. }
  269. if (entityCluster._clusterPoints) {
  270. getScreenSpacePositions(
  271. pointCollection,
  272. points,
  273. scene,
  274. occluder,
  275. entityCluster
  276. );
  277. }
  278. let i;
  279. let j;
  280. let length;
  281. let bbox;
  282. let neighbors;
  283. let neighborLength;
  284. let neighborIndex;
  285. let neighborPoint;
  286. let ids;
  287. let numPoints;
  288. let collection;
  289. let collectionIndex;
  290. const index = new KDBush(points, getX, getY, 64, Int32Array);
  291. if (currentHeight < previousHeight) {
  292. length = clusters.length;
  293. for (i = 0; i < length; ++i) {
  294. const cluster = clusters[i];
  295. if (!occluder.isPointVisible(cluster.position)) {
  296. continue;
  297. }
  298. const coord = Billboard._computeScreenSpacePosition(
  299. Matrix4.IDENTITY,
  300. cluster.position,
  301. Cartesian3.ZERO,
  302. Cartesian2.ZERO,
  303. scene
  304. );
  305. if (!defined(coord)) {
  306. continue;
  307. }
  308. const factor = 1.0 - currentHeight / previousHeight;
  309. let width = (cluster.width = cluster.width * factor);
  310. let height = (cluster.height = cluster.height * factor);
  311. width = Math.max(width, cluster.minimumWidth);
  312. height = Math.max(height, cluster.minimumHeight);
  313. const minX = coord.x - width * 0.5;
  314. const minY = coord.y - height * 0.5;
  315. const maxX = coord.x + width;
  316. const maxY = coord.y + height;
  317. neighbors = index.range(minX, minY, maxX, maxY);
  318. neighborLength = neighbors.length;
  319. numPoints = 0;
  320. ids = [];
  321. for (j = 0; j < neighborLength; ++j) {
  322. neighborIndex = neighbors[j];
  323. neighborPoint = points[neighborIndex];
  324. if (!neighborPoint.clustered) {
  325. ++numPoints;
  326. collection = neighborPoint.collection;
  327. collectionIndex = neighborPoint.index;
  328. ids.push(collection.get(collectionIndex).id);
  329. }
  330. }
  331. if (numPoints >= minimumClusterSize) {
  332. addCluster(cluster.position, numPoints, ids, entityCluster);
  333. newClusters.push(cluster);
  334. for (j = 0; j < neighborLength; ++j) {
  335. points[neighbors[j]].clustered = true;
  336. }
  337. }
  338. }
  339. }
  340. length = points.length;
  341. for (i = 0; i < length; ++i) {
  342. const point = points[i];
  343. if (point.clustered) {
  344. continue;
  345. }
  346. point.clustered = true;
  347. collection = point.collection;
  348. collectionIndex = point.index;
  349. const item = collection.get(collectionIndex);
  350. bbox = getBoundingBox(
  351. item,
  352. point.coord,
  353. pixelRange,
  354. entityCluster,
  355. pointBoundinRectangleScratch
  356. );
  357. const totalBBox = BoundingRectangle.clone(
  358. bbox,
  359. totalBoundingRectangleScratch
  360. );
  361. neighbors = index.range(
  362. bbox.x,
  363. bbox.y,
  364. bbox.x + bbox.width,
  365. bbox.y + bbox.height
  366. );
  367. neighborLength = neighbors.length;
  368. const clusterPosition = Cartesian3.clone(item.position);
  369. numPoints = 1;
  370. ids = [item.id];
  371. for (j = 0; j < neighborLength; ++j) {
  372. neighborIndex = neighbors[j];
  373. neighborPoint = points[neighborIndex];
  374. if (!neighborPoint.clustered) {
  375. const neighborItem = neighborPoint.collection.get(
  376. neighborPoint.index
  377. );
  378. const neighborBBox = getBoundingBox(
  379. neighborItem,
  380. neighborPoint.coord,
  381. pixelRange,
  382. entityCluster,
  383. neighborBoundingRectangleScratch
  384. );
  385. Cartesian3.add(
  386. neighborItem.position,
  387. clusterPosition,
  388. clusterPosition
  389. );
  390. BoundingRectangle.union(totalBBox, neighborBBox, totalBBox);
  391. ++numPoints;
  392. ids.push(neighborItem.id);
  393. }
  394. }
  395. if (numPoints >= minimumClusterSize) {
  396. const position = Cartesian3.multiplyByScalar(
  397. clusterPosition,
  398. 1.0 / numPoints,
  399. clusterPosition
  400. );
  401. addCluster(position, numPoints, ids, entityCluster);
  402. newClusters.push({
  403. position: position,
  404. width: totalBBox.width,
  405. height: totalBBox.height,
  406. minimumWidth: bbox.width,
  407. minimumHeight: bbox.height,
  408. });
  409. for (j = 0; j < neighborLength; ++j) {
  410. points[neighbors[j]].clustered = true;
  411. }
  412. } else {
  413. addNonClusteredItem(item, entityCluster);
  414. }
  415. }
  416. if (clusteredLabelCollection.length === 0) {
  417. clusteredLabelCollection.destroy();
  418. entityCluster._clusterLabelCollection = undefined;
  419. }
  420. if (clusteredBillboardCollection.length === 0) {
  421. clusteredBillboardCollection.destroy();
  422. entityCluster._clusterBillboardCollection = undefined;
  423. }
  424. if (clusteredPointCollection.length === 0) {
  425. clusteredPointCollection.destroy();
  426. entityCluster._clusterPointCollection = undefined;
  427. }
  428. entityCluster._previousClusters = newClusters;
  429. entityCluster._previousHeight = currentHeight;
  430. };
  431. }
  432. EntityCluster.prototype._initialize = function (scene) {
  433. this._scene = scene;
  434. const cluster = createDeclutterCallback(this);
  435. this._cluster = cluster;
  436. this._removeEventListener = scene.camera.changed.addEventListener(cluster);
  437. };
  438. Object.defineProperties(EntityCluster.prototype, {
  439. /**
  440. * Gets or sets whether clustering is enabled.
  441. * @memberof EntityCluster.prototype
  442. * @type {Boolean}
  443. */
  444. enabled: {
  445. get: function () {
  446. return this._enabled;
  447. },
  448. set: function (value) {
  449. this._enabledDirty = value !== this._enabled;
  450. this._enabled = value;
  451. },
  452. },
  453. /**
  454. * Gets or sets the pixel range to extend the screen space bounding box.
  455. * @memberof EntityCluster.prototype
  456. * @type {Number}
  457. */
  458. pixelRange: {
  459. get: function () {
  460. return this._pixelRange;
  461. },
  462. set: function (value) {
  463. this._clusterDirty = this._clusterDirty || value !== this._pixelRange;
  464. this._pixelRange = value;
  465. },
  466. },
  467. /**
  468. * Gets or sets the minimum number of screen space objects that can be clustered.
  469. * @memberof EntityCluster.prototype
  470. * @type {Number}
  471. */
  472. minimumClusterSize: {
  473. get: function () {
  474. return this._minimumClusterSize;
  475. },
  476. set: function (value) {
  477. this._clusterDirty =
  478. this._clusterDirty || value !== this._minimumClusterSize;
  479. this._minimumClusterSize = value;
  480. },
  481. },
  482. /**
  483. * Gets the event that will be raised when a new cluster will be displayed. The signature of the event listener is {@link EntityCluster.newClusterCallback}.
  484. * @memberof EntityCluster.prototype
  485. * @type {Event<EntityCluster.newClusterCallback>}
  486. */
  487. clusterEvent: {
  488. get: function () {
  489. return this._clusterEvent;
  490. },
  491. },
  492. /**
  493. * Gets or sets whether clustering billboard entities is enabled.
  494. * @memberof EntityCluster.prototype
  495. * @type {Boolean}
  496. */
  497. clusterBillboards: {
  498. get: function () {
  499. return this._clusterBillboards;
  500. },
  501. set: function (value) {
  502. this._clusterDirty =
  503. this._clusterDirty || value !== this._clusterBillboards;
  504. this._clusterBillboards = value;
  505. },
  506. },
  507. /**
  508. * Gets or sets whether clustering labels entities is enabled.
  509. * @memberof EntityCluster.prototype
  510. * @type {Boolean}
  511. */
  512. clusterLabels: {
  513. get: function () {
  514. return this._clusterLabels;
  515. },
  516. set: function (value) {
  517. this._clusterDirty = this._clusterDirty || value !== this._clusterLabels;
  518. this._clusterLabels = value;
  519. },
  520. },
  521. /**
  522. * Gets or sets whether clustering point entities is enabled.
  523. * @memberof EntityCluster.prototype
  524. * @type {Boolean}
  525. */
  526. clusterPoints: {
  527. get: function () {
  528. return this._clusterPoints;
  529. },
  530. set: function (value) {
  531. this._clusterDirty = this._clusterDirty || value !== this._clusterPoints;
  532. this._clusterPoints = value;
  533. },
  534. },
  535. });
  536. function createGetEntity(
  537. collectionProperty,
  538. CollectionConstructor,
  539. unusedIndicesProperty,
  540. entityIndexProperty
  541. ) {
  542. return function (entity) {
  543. let collection = this[collectionProperty];
  544. if (!defined(this._collectionIndicesByEntity)) {
  545. this._collectionIndicesByEntity = {};
  546. }
  547. let entityIndices = this._collectionIndicesByEntity[entity.id];
  548. if (!defined(entityIndices)) {
  549. entityIndices = this._collectionIndicesByEntity[entity.id] = {
  550. billboardIndex: undefined,
  551. labelIndex: undefined,
  552. pointIndex: undefined,
  553. };
  554. }
  555. if (defined(collection) && defined(entityIndices[entityIndexProperty])) {
  556. return collection.get(entityIndices[entityIndexProperty]);
  557. }
  558. if (!defined(collection)) {
  559. collection = this[collectionProperty] = new CollectionConstructor({
  560. scene: this._scene,
  561. });
  562. }
  563. let index;
  564. let entityItem;
  565. const unusedIndices = this[unusedIndicesProperty];
  566. if (unusedIndices.length > 0) {
  567. index = unusedIndices.pop();
  568. entityItem = collection.get(index);
  569. } else {
  570. entityItem = collection.add();
  571. index = collection.length - 1;
  572. }
  573. entityIndices[entityIndexProperty] = index;
  574. const that = this;
  575. Promise.resolve().then(function () {
  576. that._clusterDirty = true;
  577. });
  578. return entityItem;
  579. };
  580. }
  581. function removeEntityIndicesIfUnused(entityCluster, entityId) {
  582. const indices = entityCluster._collectionIndicesByEntity[entityId];
  583. if (
  584. !defined(indices.billboardIndex) &&
  585. !defined(indices.labelIndex) &&
  586. !defined(indices.pointIndex)
  587. ) {
  588. delete entityCluster._collectionIndicesByEntity[entityId];
  589. }
  590. }
  591. /**
  592. * Returns a new {@link Label}.
  593. * @param {Entity} entity The entity that will use the returned {@link Label} for visualization.
  594. * @returns {Label} The label that will be used to visualize an entity.
  595. *
  596. * @private
  597. */
  598. EntityCluster.prototype.getLabel = createGetEntity(
  599. "_labelCollection",
  600. LabelCollection,
  601. "_unusedLabelIndices",
  602. "labelIndex"
  603. );
  604. /**
  605. * Removes the {@link Label} associated with an entity so it can be reused by another entity.
  606. * @param {Entity} entity The entity that will uses the returned {@link Label} for visualization.
  607. *
  608. * @private
  609. */
  610. EntityCluster.prototype.removeLabel = function (entity) {
  611. const entityIndices =
  612. this._collectionIndicesByEntity &&
  613. this._collectionIndicesByEntity[entity.id];
  614. if (
  615. !defined(this._labelCollection) ||
  616. !defined(entityIndices) ||
  617. !defined(entityIndices.labelIndex)
  618. ) {
  619. return;
  620. }
  621. const index = entityIndices.labelIndex;
  622. entityIndices.labelIndex = undefined;
  623. removeEntityIndicesIfUnused(this, entity.id);
  624. const label = this._labelCollection.get(index);
  625. label.show = false;
  626. label.text = "";
  627. label.id = undefined;
  628. this._unusedLabelIndices.push(index);
  629. this._clusterDirty = true;
  630. };
  631. /**
  632. * Returns a new {@link Billboard}.
  633. * @param {Entity} entity The entity that will use the returned {@link Billboard} for visualization.
  634. * @returns {Billboard} The label that will be used to visualize an entity.
  635. *
  636. * @private
  637. */
  638. EntityCluster.prototype.getBillboard = createGetEntity(
  639. "_billboardCollection",
  640. BillboardCollection,
  641. "_unusedBillboardIndices",
  642. "billboardIndex"
  643. );
  644. /**
  645. * Removes the {@link Billboard} associated with an entity so it can be reused by another entity.
  646. * @param {Entity} entity The entity that will uses the returned {@link Billboard} for visualization.
  647. *
  648. * @private
  649. */
  650. EntityCluster.prototype.removeBillboard = function (entity) {
  651. const entityIndices =
  652. this._collectionIndicesByEntity &&
  653. this._collectionIndicesByEntity[entity.id];
  654. if (
  655. !defined(this._billboardCollection) ||
  656. !defined(entityIndices) ||
  657. !defined(entityIndices.billboardIndex)
  658. ) {
  659. return;
  660. }
  661. const index = entityIndices.billboardIndex;
  662. entityIndices.billboardIndex = undefined;
  663. removeEntityIndicesIfUnused(this, entity.id);
  664. const billboard = this._billboardCollection.get(index);
  665. billboard.id = undefined;
  666. billboard.show = false;
  667. billboard.image = undefined;
  668. this._unusedBillboardIndices.push(index);
  669. this._clusterDirty = true;
  670. };
  671. /**
  672. * Returns a new {@link Point}.
  673. * @param {Entity} entity The entity that will use the returned {@link Point} for visualization.
  674. * @returns {Point} The label that will be used to visualize an entity.
  675. *
  676. * @private
  677. */
  678. EntityCluster.prototype.getPoint = createGetEntity(
  679. "_pointCollection",
  680. PointPrimitiveCollection,
  681. "_unusedPointIndices",
  682. "pointIndex"
  683. );
  684. /**
  685. * Removes the {@link Point} associated with an entity so it can be reused by another entity.
  686. * @param {Entity} entity The entity that will uses the returned {@link Point} for visualization.
  687. *
  688. * @private
  689. */
  690. EntityCluster.prototype.removePoint = function (entity) {
  691. const entityIndices =
  692. this._collectionIndicesByEntity &&
  693. this._collectionIndicesByEntity[entity.id];
  694. if (
  695. !defined(this._pointCollection) ||
  696. !defined(entityIndices) ||
  697. !defined(entityIndices.pointIndex)
  698. ) {
  699. return;
  700. }
  701. const index = entityIndices.pointIndex;
  702. entityIndices.pointIndex = undefined;
  703. removeEntityIndicesIfUnused(this, entity.id);
  704. const point = this._pointCollection.get(index);
  705. point.show = false;
  706. point.id = undefined;
  707. this._unusedPointIndices.push(index);
  708. this._clusterDirty = true;
  709. };
  710. function disableCollectionClustering(collection) {
  711. if (!defined(collection)) {
  712. return;
  713. }
  714. const length = collection.length;
  715. for (let i = 0; i < length; ++i) {
  716. collection.get(i).clusterShow = true;
  717. }
  718. }
  719. function updateEnable(entityCluster) {
  720. if (entityCluster.enabled) {
  721. return;
  722. }
  723. if (defined(entityCluster._clusterLabelCollection)) {
  724. entityCluster._clusterLabelCollection.destroy();
  725. }
  726. if (defined(entityCluster._clusterBillboardCollection)) {
  727. entityCluster._clusterBillboardCollection.destroy();
  728. }
  729. if (defined(entityCluster._clusterPointCollection)) {
  730. entityCluster._clusterPointCollection.destroy();
  731. }
  732. entityCluster._clusterLabelCollection = undefined;
  733. entityCluster._clusterBillboardCollection = undefined;
  734. entityCluster._clusterPointCollection = undefined;
  735. disableCollectionClustering(entityCluster._labelCollection);
  736. disableCollectionClustering(entityCluster._billboardCollection);
  737. disableCollectionClustering(entityCluster._pointCollection);
  738. }
  739. /**
  740. * Gets the draw commands for the clustered billboards/points/labels if enabled, otherwise,
  741. * queues the draw commands for billboards/points/labels created for entities.
  742. * @private
  743. */
  744. EntityCluster.prototype.update = function (frameState) {
  745. if (!this.show) {
  746. return;
  747. }
  748. // If clustering is enabled before the label collection is updated,
  749. // the glyphs haven't been created so the screen space bounding boxes
  750. // are incorrect.
  751. let commandList;
  752. if (
  753. defined(this._labelCollection) &&
  754. this._labelCollection.length > 0 &&
  755. this._labelCollection.get(0)._glyphs.length === 0
  756. ) {
  757. commandList = frameState.commandList;
  758. frameState.commandList = [];
  759. this._labelCollection.update(frameState);
  760. frameState.commandList = commandList;
  761. }
  762. // If clustering is enabled before the billboard collection is updated,
  763. // the images haven't been added to the image atlas so the screen space bounding boxes
  764. // are incorrect.
  765. if (
  766. defined(this._billboardCollection) &&
  767. this._billboardCollection.length > 0 &&
  768. !defined(this._billboardCollection.get(0).width)
  769. ) {
  770. commandList = frameState.commandList;
  771. frameState.commandList = [];
  772. this._billboardCollection.update(frameState);
  773. frameState.commandList = commandList;
  774. }
  775. if (this._enabledDirty) {
  776. this._enabledDirty = false;
  777. updateEnable(this);
  778. this._clusterDirty = true;
  779. }
  780. if (this._clusterDirty) {
  781. this._clusterDirty = false;
  782. this._cluster();
  783. }
  784. if (defined(this._clusterLabelCollection)) {
  785. this._clusterLabelCollection.update(frameState);
  786. }
  787. if (defined(this._clusterBillboardCollection)) {
  788. this._clusterBillboardCollection.update(frameState);
  789. }
  790. if (defined(this._clusterPointCollection)) {
  791. this._clusterPointCollection.update(frameState);
  792. }
  793. if (defined(this._labelCollection)) {
  794. this._labelCollection.update(frameState);
  795. }
  796. if (defined(this._billboardCollection)) {
  797. this._billboardCollection.update(frameState);
  798. }
  799. if (defined(this._pointCollection)) {
  800. this._pointCollection.update(frameState);
  801. }
  802. };
  803. /**
  804. * Destroys the WebGL resources held by this object. Destroying an object allows for deterministic
  805. * release of WebGL resources, instead of relying on the garbage collector to destroy this object.
  806. * <p>
  807. * Unlike other objects that use WebGL resources, this object can be reused. For example, if a data source is removed
  808. * from a data source collection and added to another.
  809. * </p>
  810. */
  811. EntityCluster.prototype.destroy = function () {
  812. this._labelCollection =
  813. this._labelCollection && this._labelCollection.destroy();
  814. this._billboardCollection =
  815. this._billboardCollection && this._billboardCollection.destroy();
  816. this._pointCollection =
  817. this._pointCollection && this._pointCollection.destroy();
  818. this._clusterLabelCollection =
  819. this._clusterLabelCollection && this._clusterLabelCollection.destroy();
  820. this._clusterBillboardCollection =
  821. this._clusterBillboardCollection &&
  822. this._clusterBillboardCollection.destroy();
  823. this._clusterPointCollection =
  824. this._clusterPointCollection && this._clusterPointCollection.destroy();
  825. if (defined(this._removeEventListener)) {
  826. this._removeEventListener();
  827. this._removeEventListener = undefined;
  828. }
  829. this._labelCollection = undefined;
  830. this._billboardCollection = undefined;
  831. this._pointCollection = undefined;
  832. this._clusterBillboardCollection = undefined;
  833. this._clusterLabelCollection = undefined;
  834. this._clusterPointCollection = undefined;
  835. this._collectionIndicesByEntity = undefined;
  836. this._unusedLabelIndices = [];
  837. this._unusedBillboardIndices = [];
  838. this._unusedPointIndices = [];
  839. this._previousClusters = [];
  840. this._previousHeight = undefined;
  841. this._enabledDirty = false;
  842. this._pixelRangeDirty = false;
  843. this._minimumClusterSizeDirty = false;
  844. return undefined;
  845. };
  846. /**
  847. * A event listener function used to style clusters.
  848. * @callback EntityCluster.newClusterCallback
  849. *
  850. * @param {Entity[]} clusteredEntities An array of the entities contained in the cluster.
  851. * @param {Object} cluster An object containing the Billboard, Label, and Point
  852. * primitives that represent this cluster of entities.
  853. * @param {Billboard} cluster.billboard
  854. * @param {Label} cluster.label
  855. * @param {PointPrimitive} cluster.point
  856. *
  857. * @example
  858. * // The default cluster values.
  859. * dataSource.clustering.clusterEvent.addEventListener(function(entities, cluster) {
  860. * cluster.label.show = true;
  861. * cluster.label.text = entities.length.toLocaleString();
  862. * });
  863. */
  864. export default EntityCluster;