CompositeEntityCollection.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. /*global define*/
  2. define([
  3. '../Core/createGuid',
  4. '../Core/defined',
  5. '../Core/defineProperties',
  6. '../Core/DeveloperError',
  7. '../Core/Math',
  8. './Entity',
  9. './EntityCollection'
  10. ], function(
  11. createGuid,
  12. defined,
  13. defineProperties,
  14. DeveloperError,
  15. CesiumMath,
  16. Entity,
  17. EntityCollection) {
  18. "use strict";
  19. var entityIdScratch = new Array(2);
  20. function clean(entity) {
  21. var propertyNames = entity.propertyNames;
  22. var propertyNamesLength = propertyNames.length;
  23. for (var i = 0; i < propertyNamesLength; i++) {
  24. entity[propertyNames[i]] = undefined;
  25. }
  26. }
  27. function subscribeToEntity(that, eventHash, collectionId, entity) {
  28. entityIdScratch[0] = collectionId;
  29. entityIdScratch[1] = entity.id;
  30. eventHash[JSON.stringify(entityIdScratch)] = entity.definitionChanged.addEventListener(CompositeEntityCollection.prototype._onDefinitionChanged, that);
  31. }
  32. function unsubscribeFromEntity(that, eventHash, collectionId, entity) {
  33. entityIdScratch[0] = collectionId;
  34. entityIdScratch[1] = entity.id;
  35. var id = JSON.stringify(entityIdScratch);
  36. eventHash[id]();
  37. eventHash[id] = undefined;
  38. }
  39. function recomposite(that) {
  40. that._shouldRecomposite = true;
  41. if (that._suspendCount !== 0) {
  42. return;
  43. }
  44. var collections = that._collections;
  45. var collectionsLength = collections.length;
  46. var collectionsCopy = that._collectionsCopy;
  47. var collectionsCopyLength = collectionsCopy.length;
  48. var i;
  49. var entity;
  50. var entities;
  51. var iEntities;
  52. var collection;
  53. var composite = that._composite;
  54. var newEntities = new EntityCollection();
  55. var eventHash = that._eventHash;
  56. var collectionId;
  57. for (i = 0; i < collectionsCopyLength; i++) {
  58. collection = collectionsCopy[i];
  59. collection.collectionChanged.removeEventListener(CompositeEntityCollection.prototype._onCollectionChanged, that);
  60. entities = collection.entities;
  61. collectionId = collection.id;
  62. for (iEntities = entities.length - 1; iEntities > -1; iEntities--) {
  63. entity = entities[iEntities];
  64. unsubscribeFromEntity(that, eventHash, collectionId, entity);
  65. }
  66. }
  67. for (i = collectionsLength - 1; i >= 0; i--) {
  68. collection = collections[i];
  69. collection.collectionChanged.addEventListener(CompositeEntityCollection.prototype._onCollectionChanged, that);
  70. //Merge all of the existing entities.
  71. entities = collection.entities;
  72. collectionId = collection.id;
  73. for (iEntities = entities.length - 1; iEntities > -1; iEntities--) {
  74. entity = entities[iEntities];
  75. subscribeToEntity(that, eventHash, collectionId, entity);
  76. var compositeEntity = newEntities.getById(entity.id);
  77. if (!defined(compositeEntity)) {
  78. compositeEntity = composite.getById(entity.id);
  79. if (!defined(compositeEntity)) {
  80. compositeEntity = new Entity(entity.id);
  81. } else {
  82. clean(compositeEntity);
  83. }
  84. newEntities.add(compositeEntity);
  85. }
  86. compositeEntity.merge(entity);
  87. }
  88. }
  89. that._collectionsCopy = collections.slice(0);
  90. composite.suspendEvents();
  91. composite.removeAll();
  92. var newEntitiesArray = newEntities.entities;
  93. for (i = 0; i < newEntitiesArray.length; i++) {
  94. composite.add(newEntitiesArray[i]);
  95. }
  96. composite.resumeEvents();
  97. }
  98. /**
  99. * Non-destructively composites multiple {@link EntityCollection} instances into a single collection.
  100. * If a Entity with the same ID exists in multiple collections, it is non-destructively
  101. * merged into a single new entity instance. If an entity has the same property in multiple
  102. * collections, the property of the Entity in the last collection of the list it
  103. * belongs to is used. CompositeEntityCollection can be used almost anywhere that a
  104. * EntityCollection is used.
  105. *
  106. * @alias CompositeEntityCollection
  107. * @constructor
  108. *
  109. * @param {EntityCollection[]} [collections] The initial list of EntityCollection instances to merge.
  110. */
  111. var CompositeEntityCollection = function(collections) {
  112. this._composite = new EntityCollection();
  113. this._suspendCount = 0;
  114. this._collections = defined(collections) ? collections.slice() : [];
  115. this._collectionsCopy = [];
  116. this._id = createGuid();
  117. this._eventHash = {};
  118. recomposite(this);
  119. this._shouldRecomposite = false;
  120. };
  121. defineProperties(CompositeEntityCollection.prototype, {
  122. /**
  123. * Gets the event that is fired when entities are added or removed from the collection.
  124. * The generated event is a {@link EntityCollection.collectionChangedEventCallback}.
  125. * @memberof CompositeEntityCollection.prototype
  126. * @readonly
  127. * @type {Event}
  128. */
  129. collectionChanged : {
  130. get : function() {
  131. return this._composite._collectionChanged;
  132. }
  133. },
  134. /**
  135. * Gets a globally unique identifier for this collection.
  136. * @memberof CompositeEntityCollection.prototype
  137. * @readonly
  138. * @type {String}
  139. */
  140. id : {
  141. get : function() {
  142. return this._id;
  143. }
  144. },
  145. /**
  146. * Gets the array of Entity instances in the collection.
  147. * This array should not be modified directly.
  148. * @memberof CompositeEntityCollection.prototype
  149. * @readonly
  150. * @type {Entity[]}
  151. */
  152. entities : {
  153. get : function() {
  154. return this._composite.entities;
  155. }
  156. }
  157. });
  158. /**
  159. * Adds a collection to the composite.
  160. *
  161. * @param {EntityCollection} collection the collection to add.
  162. * @param {Number} [index] the index to add the collection at. If omitted, the collection will
  163. * added on top of all existing collections.
  164. *
  165. * @exception {DeveloperError} index, if supplied, must be greater than or equal to zero and less than or equal to the number of collections.
  166. */
  167. CompositeEntityCollection.prototype.addCollection = function(collection, index) {
  168. var hasIndex = defined(index);
  169. //>>includeStart('debug', pragmas.debug);
  170. if (!defined(collection)) {
  171. throw new DeveloperError('collection is required.');
  172. }
  173. if (hasIndex) {
  174. if (index < 0) {
  175. throw new DeveloperError('index must be greater than or equal to zero.');
  176. } else if (index > this._collections.length) {
  177. throw new DeveloperError('index must be less than or equal to the number of collections.');
  178. }
  179. }
  180. //>>includeEnd('debug');
  181. if (!hasIndex) {
  182. index = this._collections.length;
  183. this._collections.push(collection);
  184. } else {
  185. this._collections.splice(index, 0, collection);
  186. }
  187. recomposite(this);
  188. };
  189. /**
  190. * Removes a collection from this composite, if present.
  191. *
  192. * @param {EntityCollection} collection The collection to remove.
  193. * @returns {Boolean} true if the collection was in the composite and was removed,
  194. * false if the collection was not in the composite.
  195. */
  196. CompositeEntityCollection.prototype.removeCollection = function(collection) {
  197. var index = this._collections.indexOf(collection);
  198. if (index !== -1) {
  199. this._collections.splice(index, 1);
  200. recomposite(this);
  201. return true;
  202. }
  203. return false;
  204. };
  205. /**
  206. * Removes all collections from this composite.
  207. */
  208. CompositeEntityCollection.prototype.removeAllCollections = function() {
  209. this._collections.length = 0;
  210. recomposite(this);
  211. };
  212. /**
  213. * Checks to see if the composite contains a given collection.
  214. *
  215. * @param {EntityCollection} collection the collection to check for.
  216. * @returns {Boolean} true if the composite contains the collection, false otherwise.
  217. */
  218. CompositeEntityCollection.prototype.containsCollection = function(collection) {
  219. return this._collections.indexOf(collection) !== -1;
  220. };
  221. /**
  222. * Determines the index of a given collection in the composite.
  223. *
  224. * @param {EntityCollection} collection The collection to find the index of.
  225. * @returns {Number} The index of the collection in the composite, or -1 if the collection does not exist in the composite.
  226. */
  227. CompositeEntityCollection.prototype.indexOfCollection = function(collection) {
  228. return this._collections.indexOf(collection);
  229. };
  230. /**
  231. * Gets a collection by index from the composite.
  232. *
  233. * @param {Number} index the index to retrieve.
  234. */
  235. CompositeEntityCollection.prototype.getCollection = function(index) {
  236. //>>includeStart('debug', pragmas.debug);
  237. if (!defined(index)) {
  238. throw new DeveloperError('index is required.', 'index');
  239. }
  240. //>>includeEnd('debug');
  241. return this._collections[index];
  242. };
  243. /**
  244. * Gets the number of collections in this composite.
  245. */
  246. CompositeEntityCollection.prototype.getCollectionsLength = function() {
  247. return this._collections.length;
  248. };
  249. function getCollectionIndex(collections, collection) {
  250. //>>includeStart('debug', pragmas.debug);
  251. if (!defined(collection)) {
  252. throw new DeveloperError('collection is required.');
  253. }
  254. //>>includeEnd('debug');
  255. var index = collections.indexOf(collection);
  256. //>>includeStart('debug', pragmas.debug);
  257. if (index === -1) {
  258. throw new DeveloperError('collection is not in this composite.');
  259. }
  260. //>>includeEnd('debug');
  261. return index;
  262. }
  263. function swapCollections(composite, i, j) {
  264. var arr = composite._collections;
  265. i = CesiumMath.clamp(i, 0, arr.length - 1);
  266. j = CesiumMath.clamp(j, 0, arr.length - 1);
  267. if (i === j) {
  268. return;
  269. }
  270. var temp = arr[i];
  271. arr[i] = arr[j];
  272. arr[j] = temp;
  273. recomposite(composite);
  274. }
  275. /**
  276. * Raises a collection up one position in the composite.
  277. *
  278. * @param {EntityCollection} collection the collection to move.
  279. *
  280. * @exception {DeveloperError} collection is not in this composite.
  281. */
  282. CompositeEntityCollection.prototype.raiseCollection = function(collection) {
  283. var index = getCollectionIndex(this._collections, collection);
  284. swapCollections(this, index, index + 1);
  285. };
  286. /**
  287. * Lowers a collection down one position in the composite.
  288. *
  289. * @param {EntityCollection} collection the collection to move.
  290. *
  291. * @exception {DeveloperError} collection is not in this composite.
  292. */
  293. CompositeEntityCollection.prototype.lowerCollection = function(collection) {
  294. var index = getCollectionIndex(this._collections, collection);
  295. swapCollections(this, index, index - 1);
  296. };
  297. /**
  298. * Raises a collection to the top of the composite.
  299. *
  300. * @param {EntityCollection} collection the collection to move.
  301. *
  302. * @exception {DeveloperError} collection is not in this composite.
  303. */
  304. CompositeEntityCollection.prototype.raiseCollectionToTop = function(collection) {
  305. var index = getCollectionIndex(this._collections, collection);
  306. if (index === this._collections.length - 1) {
  307. return;
  308. }
  309. this._collections.splice(index, 1);
  310. this._collections.push(collection);
  311. recomposite(this);
  312. };
  313. /**
  314. * Lowers a collection to the bottom of the composite.
  315. *
  316. * @param {EntityCollection} collection the collection to move.
  317. *
  318. * @exception {DeveloperError} collection is not in this composite.
  319. */
  320. CompositeEntityCollection.prototype.lowerCollectionToBottom = function(collection) {
  321. var index = getCollectionIndex(this._collections, collection);
  322. if (index === 0) {
  323. return;
  324. }
  325. this._collections.splice(index, 1);
  326. this._collections.splice(0, 0, collection);
  327. recomposite(this);
  328. };
  329. /**
  330. * Prevents {@link EntityCollection#collectionChanged} events from being raised
  331. * until a corresponding call is made to {@link EntityCollection#resumeEvents}, at which
  332. * point a single event will be raised that covers all suspended operations.
  333. * This allows for many items to be added and removed efficiently.
  334. * While events are suspended, recompositing of the collections will
  335. * also be suspended, as this can be a costly operation.
  336. * This function can be safely called multiple times as long as there
  337. * are corresponding calls to {@link EntityCollection#resumeEvents}.
  338. */
  339. CompositeEntityCollection.prototype.suspendEvents = function() {
  340. this._suspendCount++;
  341. this._composite.suspendEvents();
  342. };
  343. /**
  344. * Resumes raising {@link EntityCollection#collectionChanged} events immediately
  345. * when an item is added or removed. Any modifications made while while events were suspended
  346. * will be triggered as a single event when this function is called. This function also ensures
  347. * the collection is recomposited if events are also resumed.
  348. * This function is reference counted and can safely be called multiple times as long as there
  349. * are corresponding calls to {@link EntityCollection#resumeEvents}.
  350. *
  351. * @exception {DeveloperError} resumeEvents can not be called before suspendEvents.
  352. */
  353. CompositeEntityCollection.prototype.resumeEvents = function() {
  354. //>>includeStart('debug', pragmas.debug);
  355. if (this._suspendCount === 0) {
  356. throw new DeveloperError('resumeEvents can not be called before suspendEvents.');
  357. }
  358. //>>includeEnd('debug');
  359. this._suspendCount--;
  360. // recomposite before triggering events (but only if required for performance) that might depend on a composited collection
  361. if (this._shouldRecomposite && this._suspendCount === 0) {
  362. recomposite(this);
  363. this._shouldRecomposite = false;
  364. }
  365. this._composite.resumeEvents();
  366. };
  367. /**
  368. * Computes the maximum availability of the entities in the collection.
  369. * If the collection contains a mix of infinitely available data and non-infinite data,
  370. * It will return the interval pertaining to the non-infinite data only. If all
  371. * data is infinite, an infinite interval will be returned.
  372. *
  373. * @returns {TimeInterval} The availability of entities in the collection.
  374. */
  375. CompositeEntityCollection.prototype.computeAvailability = function() {
  376. return this._composite.computeAvailability();
  377. };
  378. /**
  379. * Gets an entity with the specified id.
  380. *
  381. * @param {Object} id The id of the entity to retrieve.
  382. * @returns {Entity} The entity with the provided id or undefined if the id did not exist in the collection.
  383. */
  384. CompositeEntityCollection.prototype.getById = function(id) {
  385. return this._composite.getById(id);
  386. };
  387. CompositeEntityCollection.prototype._onCollectionChanged = function(collection, added, removed) {
  388. var collections = this._collectionsCopy;
  389. var collectionsLength = collections.length;
  390. var composite = this._composite;
  391. composite.suspendEvents();
  392. var i;
  393. var q;
  394. var entity;
  395. var compositeEntity;
  396. var removedLength = removed.length;
  397. var eventHash = this._eventHash;
  398. var collectionId = collection.id;
  399. for (i = 0; i < removedLength; i++) {
  400. var removedEntity = removed[i];
  401. unsubscribeFromEntity(this, eventHash, collectionId, removedEntity);
  402. var removedId = removedEntity.id;
  403. //Check if the removed entity exists in any of the remaining collections
  404. //If so, we clean and remerge it.
  405. for (q = collectionsLength - 1; q >= 0; q--) {
  406. entity = collections[q].getById(removedId);
  407. if (defined(entity)) {
  408. if (!defined(compositeEntity)) {
  409. compositeEntity = composite.getById(removedId);
  410. clean(compositeEntity);
  411. }
  412. compositeEntity.merge(entity);
  413. }
  414. }
  415. //We never retrieved the compositeEntity, which means it no longer
  416. //exists in any of the collections, remove it from the composite.
  417. if (!defined(compositeEntity)) {
  418. composite.removeById(removedId);
  419. }
  420. compositeEntity = undefined;
  421. }
  422. var addedLength = added.length;
  423. for (i = 0; i < addedLength; i++) {
  424. var addedEntity = added[i];
  425. subscribeToEntity(this, eventHash, collectionId, addedEntity);
  426. var addedId = addedEntity.id;
  427. //We know the added entity exists in at least one collection,
  428. //but we need to check all collections and re-merge in order
  429. //to maintain the priority of properties.
  430. for (q = collectionsLength - 1; q >= 0; q--) {
  431. entity = collections[q].getById(addedId);
  432. if (defined(entity)) {
  433. if (!defined(compositeEntity)) {
  434. compositeEntity = composite.getById(addedId);
  435. if (!defined(compositeEntity)) {
  436. compositeEntity = new Entity(addedId);
  437. composite.add(compositeEntity);
  438. } else {
  439. clean(compositeEntity);
  440. }
  441. }
  442. compositeEntity.merge(entity);
  443. }
  444. }
  445. compositeEntity = undefined;
  446. }
  447. composite.resumeEvents();
  448. };
  449. CompositeEntityCollection.prototype._onDefinitionChanged = function(entity, propertyName, newValue, oldValue) {
  450. var collections = this._collections;
  451. var composite = this._composite;
  452. var collectionsLength = collections.length;
  453. var id = entity.id;
  454. var compositeEntity = composite.getById(id);
  455. var compositeProperty = compositeEntity[propertyName];
  456. var newProperty = !defined(compositeProperty);
  457. var firstTime = true;
  458. for (var q = collectionsLength - 1; q >= 0; q--) {
  459. var innerEntity = collections[q].getById(entity.id);
  460. if (defined(innerEntity)) {
  461. var property = innerEntity[propertyName];
  462. if (defined(property)) {
  463. if (firstTime) {
  464. firstTime = false;
  465. //We only want to clone if the property is also mergeable.
  466. //This ensures that leaf properties are referenced and not copied,
  467. //which is the entire point of compositing.
  468. if (defined(property.merge) && defined(property.clone)) {
  469. compositeProperty = property.clone(compositeProperty);
  470. } else {
  471. compositeProperty = property;
  472. break;
  473. }
  474. }
  475. compositeProperty.merge(property);
  476. }
  477. }
  478. }
  479. if (newProperty && compositeEntity.propertyNames.indexOf(propertyName) === -1) {
  480. compositeEntity.addProperty(propertyName);
  481. }
  482. compositeEntity[propertyName] = compositeProperty;
  483. };
  484. return CompositeEntityCollection;
  485. });