CompositeEntityCollection.js 21 KB

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