EntityCluster.js 31 KB

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