Source: DataSources/CompositeEntityCollection.js

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