CompositeEntityCollection.js 18 KB

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