EntityCluster.js 28 KB

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